From c55f6b67b9b048815d94d4b0166bac86389e5842 Mon Sep 17 00:00:00 2001 From: Max Anurin Date: Fri, 9 Aug 2024 21:24:25 +0300 Subject: [PATCH] Add ability to override protocol from http:// to file:// in schema ids --- src/cli.ts | 28 +++++++++++++++++--- src/index.ts | 11 ++++++++ src/resolver.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 104 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 1206d475..ee661081 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,6 +15,7 @@ main( help: ['h'], input: ['i'], output: ['o'], + overrideHttpId: ['m'], }, boolean: [ 'additionalProperties', @@ -40,6 +41,8 @@ async function main(argv: minimist.ParsedArgs) { const argIn: string = argv._[0] || argv.input const argOut: string | undefined = argv._[1] || argv.output // the output can be omitted so this can be undefined + const overrideHttpId: {[key: string]: string} = parseOverrideHttpIdArgs(argv.overrideHttpId) + const ISGLOB = isGlob(argIn) const ISDIR = isDir(argIn) @@ -52,11 +55,11 @@ async function main(argv: minimist.ParsedArgs) { try { // Process input as either glob, directory, or single file if (ISGLOB) { - await processGlob(argIn, argOut, argv as Partial) + await processGlob(argIn, argOut, {...argv, overrideHttpId} as Partial) } else if (ISDIR) { - await processDir(argIn, argOut, argv as Partial) + await processDir(argIn, argOut, {...argv, overrideHttpId} as Partial) } else { - const result = await processFile(argIn, argv as Partial) + const result = await processFile(argIn, {...argv, overrideHttpId} as Partial) outputResult(result, argOut) } } catch (e) { @@ -160,6 +163,19 @@ async function readStream(stream: NodeJS.ReadStream): Promise { return Buffer.concat(chunks).toString('utf8') } +function parseOverrideHttpIdArgs(overrideHttpId: any) { + const overrideHttpIdArg: {[key: string]: string} = + typeof overrideHttpId === 'string' ? {'0': overrideHttpId} : overrideHttpId + const result: {[key: string]: string} = {} + if (overrideHttpIdArg !== null) { + for (const [, value] of Object.entries(overrideHttpIdArg)) { + const [originalSchemaUrl, overrideSchemaUrl] = value.split(' ') + result[originalSchemaUrl] = overrideSchemaUrl + } + } + return Object.freeze(result) +} + function printHelp() { const pkg = require('../../package.json') @@ -192,6 +208,12 @@ Boolean values can be set to false using the 'no-' prefix. array types, before falling back to emitting unbounded arrays. Increase this to improve precision of emitted types, decrease it to improve performance, or set it to -1 to ignore minItems and maxItems. + --overrideHttpId + Define overrides for HTTP schema ids to map it to file system. + --overrideHttpId=http://schemas.example.org/ file:///home/me/my-project/src/schemas/ + or + --overrideHttpId.1=http://schemas1.example.org/ file:///home/me/my-project1/src/schemas/ + --overrideHttpId.2=http://schemas2.example.org/ file:///home/me/my-project2/src/schemas/ --style.XXX=YYY Prettier configuration --unknownAny diff --git a/src/index.ts b/src/index.ts index 1aa67be0..de70fab1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,16 @@ export interface Options { * Prepend enums with [`const`](https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)? */ enableConstEnums: boolean + /** + * Define overrides for HTTP schema ids to map it to file system. + * + * @example + * { + * "http://schemas1.example.org/": "file:///home/me/my-project1/src/schemas/", + * "http://schemas2.example.org/": "file:///home/me/my-project2/src/schemas/" + * } + */ + overrideHttpId: {[key: string]: string} | null /** * Create enums from JSON enums with eponymous keys */ @@ -101,6 +111,7 @@ export const DEFAULT_OPTIONS: Options = { enableConstEnums: true, inferStringEnumKeysFromValues: false, format: true, + overrideHttpId: null, ignoreMinAndMaxItems: false, maxItems: 20, strictIndexSignatures: false, diff --git a/src/resolver.ts b/src/resolver.ts index 94dc5a0e..36f49ae2 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -1,4 +1,10 @@ -import {$RefParser, ParserOptions as $RefOptions} from '@apidevtools/json-schema-ref-parser' +import {readFile} from 'fs/promises' +import { + $RefParser, + ParserOptions as $RefOptions, + HTTPResolverOptions, + FileInfo, +} from '@apidevtools/json-schema-ref-parser' import {JSONSchema} from './types/JSONSchema' import {log} from './utils' @@ -6,10 +12,15 @@ export type DereferencedPaths = WeakMap export async function dereference( schema: JSONSchema, - {cwd, $refOptions}: {cwd: string; $refOptions: $RefOptions}, + { + cwd, + $refOptions, + overrideHttpId, + }: {cwd: string; overrideHttpId: {[key: string]: string} | null; $refOptions: $RefOptions}, ): Promise<{dereferencedPaths: DereferencedPaths; dereferencedSchema: JSONSchema}> { log('green', 'dereferencer', 'Dereferencing input schema:', cwd, schema) const parser = new $RefParser() + const myResolver = new MyResolver(overrideHttpId) const dereferencedPaths: DereferencedPaths = new WeakMap() const dereferencedSchema = (await parser.dereference(cwd, schema, { ...$refOptions, @@ -19,6 +30,61 @@ export async function dereference( dereferencedPaths.set(schema, $ref) }, }, + resolve: { + httpOverride: { + order: 1, + canRead(file: FileInfo) { + return myResolver.canRead(file) + }, + read(file: FileInfo) { + return myResolver.read(file) + }, + }, + }, })) as any // TODO: fix types return {dereferencedPaths, dereferencedSchema} } + +class MyResolver implements HTTPResolverOptions { + private readonly overrideHttpId: ReadonlyArray<{originalSchemaUrl: URL; overrideSchemaUrl: URL}> | null + + public constructor(overrideHttpId: {[key: string]: string} | null) { + if (overrideHttpId !== null) { + const result: Array<{originalSchemaUrl: URL; overrideSchemaUrl: URL}> = [] + for (const [originalSchemaUrl, overrideSchemaUrl] of Object.entries(overrideHttpId)) { + result.push({originalSchemaUrl: new URL(originalSchemaUrl), overrideSchemaUrl: new URL(overrideSchemaUrl)}) + } + this.overrideHttpId = Object.freeze(result) + } else { + this.overrideHttpId = null + } + } + + public canRead(file: FileInfo): boolean { + if (this.overrideHttpId !== null) { + for (const {originalSchemaUrl} of this.overrideHttpId) { + const originalSchemaUrlStr: string = originalSchemaUrl.toString() + if (file.url.startsWith(originalSchemaUrlStr)) { + return true + } + } + } + return false + } + + public async read(file: FileInfo): Promise { + if (this.overrideHttpId !== null) { + for (const {originalSchemaUrl, overrideSchemaUrl} of this.overrideHttpId) { + const originalSchemaUrlStr: string = originalSchemaUrl.toString() + if (file.url.startsWith(originalSchemaUrlStr)) { + const overrideSchemaUrlStr: string = overrideSchemaUrl.toString() + const newUrl: URL = new URL(file.url.replace(originalSchemaUrlStr, overrideSchemaUrlStr)) + const fileData = await readFile(newUrl) + return fileData + } + } + } + + throw new Error('Шось пішло не так') + } +}