From 6a0f0851816e63a24fa36456069fd7122407b0bd Mon Sep 17 00:00:00 2001 From: JonLuca DeCaro Date: Thu, 4 Aug 2022 11:47:45 -0400 Subject: [PATCH 1/2] Add documentation, clean up some types, small fixes --- .eslintrc.js | 7 +- .github/workflows/release.yml | 12 +- .github/workflows/test.yml | 22 +- README.md | 10 + bin/json-schema-to-openapi-schema.js | 3 +- package.json | 20 +- src/const.ts | 43 + src/index.ts | 63 +- src/lib/openApiSchema.ts | 1481 -------------------------- src/types.ts | 5 +- test/dereference_schema.test.ts | 13 +- test/nullable.test.ts | 106 ++ test/tsconfig.json | 10 + tsconfig.json | 8 +- vite.config.ts | 6 +- 15 files changed, 283 insertions(+), 1526 deletions(-) create mode 100644 src/const.ts delete mode 100644 src/lib/openApiSchema.ts create mode 100644 test/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index 3997e90..b4d8dd6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -27,7 +27,10 @@ module.exports = { SwitchCase: 1, }, ], - 'linebreak-style': ['error', 'unix'], + 'linebreak-style': [ + 'error', + process.platform === 'win32' ? 'windows' : 'unix', + ], quotes: ['error', 'single'], semi: ['error', 'always'], '@typescript-eslint/ban-ts-comment': 'off', @@ -38,5 +41,7 @@ module.exports = { prefer: 'type-imports', }, ], + '@typescript-eslint/no-explicit-any': 'off', }, + ignorePatterns: ['dist/**', 'bin/**'], }; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbf0c27..bcedcf9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,13 +10,13 @@ jobs: name: Release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 'lts/*' - - run: npm ci - - run: npm run build --if-present - - run: npm test + node-version: 18 + - run: yarn install --frozen-lockfile + - run: yarn build + - run: yarn test - run: npx semantic-release --branches main env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1d28f12..7ff6cd5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,15 +5,23 @@ on: push jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - 14 + - 16 + - 18 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 with: - node-version: 'lts/*' - - name: npm install, build, and test + node-version: ${{ matrix.node-version }} + - name: yarn install, build, and test run: | - npm ci - npm run build --if-present - npm test + yarn --frozen-lockfile + yarn build --if-present + yarn lint + yarn test env: CI: true diff --git a/README.md b/README.md index ebc3520..5a1da2f 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,16 @@ If set to `false`, converts the provided schema in place. If `true`, clones the If set to `true`, all local and remote references (http/https and file) $refs will be dereferenced. Defaults to `false`. +#### `convertUnreferencedDefinitions` (boolean) + +Defaults to true. + +If a schema had a definitions property (which is valid in JSONSchema), and only some of those entries are referenced, we'll still try and convert the remaining definitions to OpenAPI. If you do not want this behavior, set this to `false`. + +#### `dereferenceOptions` (object = $RefParser.Options) + +Options to pass to the dereferencer (@apidevtools/json-schema-ref-parser). To prevent circular references, pass `{ dereference: { circular: 'ignore' } }`. + ## Command Line ```sh diff --git a/bin/json-schema-to-openapi-schema.js b/bin/json-schema-to-openapi-schema.js index 3f63627..1178137 100755 --- a/bin/json-schema-to-openapi-schema.js +++ b/bin/json-schema-to-openapi-schema.js @@ -2,7 +2,6 @@ 'use strict'; const yargs = require('yargs'); -const chalk = require('chalk'); const converter = require('../dist/cjs/index.js').default; const helpText = require('./help-text.json'); const fs = require('fs'); @@ -103,6 +102,6 @@ function getHelpText(commandName) { */ function errorHandler(err) { let errorMessage = process.env.DEBUG ? err.stack : err.message; - console.error(chalk.red(errorMessage)); + console.error(errorMessage); process.exit(1); } diff --git a/package.json b/package.json index f503e13..ce25905 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,23 @@ "bin": { "json-schema-to-openapi-schema": "bin/json-schema-to-openapi-schema.js" }, - "types": "dist/mjs/src/index.d.ts", + "types": "dist/mjs/index.d.ts", "files": [ "/dist" ], - "main": "dist/cjs/src/index.js", - "module": "dist/mjs/src/index.js", + "main": "dist/cjs/index.js", + "module": "dist/mjs/index.js", "exports": { ".": { - "import": "./dist/mjs/src/index.js", - "require": "./dist/cjs/src/index.js" + "import": "./dist/mjs/index.js", + "require": "./dist/cjs/index.js" } }, "scripts": { "prepublish": "yarn build", "build": "rm -fr dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node scripts/fixup.cjs", + "lint": "eslint . && prettier -c src", + "lint:fix": "eslint . --fix && prettier -c src -w", "typecheck": "tsc --noEmit", "test": "vitest", "coverage": "vitest --coverage" @@ -28,15 +30,15 @@ "author": "OpenAPI Contrib", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=14" }, "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.9", - "chalk": "^5.0.1", "json-schema-walker": "^0.0.4", "yargs": "^17.5.1" }, "devDependencies": { + "@types/json-schema": "^7.0.11", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", "c8": "^7.12.0", @@ -44,11 +46,11 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-unused-imports": "^2.0.0", - "mocha": "^10.0.0", "nock": "^13.2.9", + "openapi-typescript": "^5.4.1", "prettier": "^2.7.1", "typescript": "^4.7.4", - "vitest": "^0.20.2" + "vitest": "^0.20.3" }, "prettier": { "singleQuote": true, diff --git a/src/const.ts b/src/const.ts new file mode 100644 index 0000000..b6b4db2 --- /dev/null +++ b/src/const.ts @@ -0,0 +1,43 @@ +// TODO: having definitions inside an oas3 schema isn't exactly valid, +// maybe it is an idea to extract and split them into multiple oas3 schemas and reference to them. +// For now leaving as is. +export const allowedKeywords = [ + '$ref', + 'definitions', + // From Schema + 'title', + 'multipleOf', + 'maximum', + 'exclusiveMaximum', + 'minimum', + 'exclusiveMinimum', + 'maxLength', + 'minLength', + 'pattern', + 'maxItems', + 'minItems', + 'uniqueItems', + 'maxProperties', + 'minProperties', + 'required', + 'enum', + 'type', + 'not', + 'allOf', + 'oneOf', + 'anyOf', + 'items', + 'properties', + 'additionalProperties', + 'description', + 'format', + 'default', + 'nullable', + 'discriminator', + 'readOnly', + 'writeOnly', + 'example', + 'externalDocs', + 'deprecated', + 'xml', +]; diff --git a/src/index.ts b/src/index.ts index 71be02e..fb5a483 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,9 @@ import type { JSONSchema7Definition, } from 'json-schema'; import type { Options, SchemaType, SchemaTypeKeys } from './types'; -import { oas3schema } from './lib/openApiSchema'; import { Walker } from 'json-schema-walker'; +import { allowedKeywords } from './const'; +import type { OpenAPI3 } from 'openapi-typescript'; class InvalidTypeError extends Error { constructor(message: string) { @@ -18,23 +19,63 @@ class InvalidTypeError extends Error { const oasExtensionPrefix = 'x-'; -// TODO: having definitions inside an oas3 schema isn't exactly valid, -// maybe it is an idea to extract and split them into multiple oas3 schemas and reference to them. -// For now leaving as is. -const allowedKeywords = [ - '$ref', - 'definitions', - ...Object.keys(oas3schema.definitions.Schema.properties), -]; +const handleDefinition = async ( + def: JSONSchema7Definition | JSONSchema6Definition | JSONSchema4, + schema: T +) => { + if (typeof def !== 'object') { + return def; + } + + const type = def.type; + if (type) { + // Walk just the definitions types + const walker = new Walker(); + await walker.loadSchema({ ...def, $schema: schema['$schema'] } as any, { + dereference: true, + cloneSchema: true, + dereferenceOptions: { + dereference: { + circular: 'ignore', + }, + }, + }); + await walker.walk(convertSchema, walker.vocabularies.DRAFT_07); + return walker.rootSchema; + } else if (Array.isArray(def)) { + // if it's an array, we might want to reconstruct the type; + const typeArr = def; + const hasNull = typeArr.includes('null'); + if (hasNull) { + const actualTypes = typeArr.filter((l) => l !== 'null'); + return { + type: actualTypes.length === 1 ? actualTypes[0] : actualTypes, + nullable: true, + // this is incorrect but thats ok, we are in the inbetween phase here + } as JSONSchema7Definition | JSONSchema6Definition | JSONSchema4; + } + } + + return def; +}; const convert = async ( schema: T, options?: Options -): Promise => { +): Promise => { const walker = new Walker(); + const convertDefs = options?.convertUnreferencedDefinitions ?? true; await walker.loadSchema(schema, options); await walker.walk(convertSchema, walker.vocabularies.DRAFT_07); - return walker.rootSchema; + // if we want to convert unreferenced definitions, we need to do it iteratively here + const rootSchema = walker.rootSchema as unknown as JSONSchema; + if (convertDefs && rootSchema?.definitions) { + for (const defName in rootSchema.definitions) { + const def = rootSchema.definitions[defName]; + rootSchema.definitions[defName] = await handleDefinition(def, schema); + } + } + return rootSchema as OpenAPI3; }; function stripIllegalKeywords(schema: SchemaType) { diff --git a/src/lib/openApiSchema.ts b/src/lib/openApiSchema.ts deleted file mode 100644 index 9dfa837..0000000 --- a/src/lib/openApiSchema.ts +++ /dev/null @@ -1,1481 +0,0 @@ -export const oas3schema = { - id: 'https://spec.openapis.org/oas/3.0/schema/2019-04-02', - $schema: 'http://json-schema.org/draft-04/schema#', - description: 'Validation schema for OpenAPI Specification 3.0.X.', - type: 'object', - required: ['openapi', 'info', 'paths'], - properties: { - openapi: { - type: 'string', - pattern: '^3\\.0\\.\\d(-.+)?$', - }, - info: { - $ref: '#/definitions/Info', - }, - externalDocs: { - $ref: '#/definitions/ExternalDocumentation', - }, - servers: { - type: 'array', - items: { - $ref: '#/definitions/Server', - }, - }, - security: { - type: 'array', - items: { - $ref: '#/definitions/SecurityRequirement', - }, - }, - tags: { - type: 'array', - items: { - $ref: '#/definitions/Tag', - }, - uniqueItems: true, - }, - paths: { - $ref: '#/definitions/Paths', - }, - components: { - $ref: '#/definitions/Components', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - definitions: { - Reference: { - type: 'object', - required: ['$ref'], - patternProperties: { - '^\\$ref$': { - type: 'string', - format: 'uri-reference', - }, - }, - }, - Info: { - type: 'object', - required: ['title', 'version'], - properties: { - title: { - type: 'string', - }, - description: { - type: 'string', - }, - termsOfService: { - type: 'string', - format: 'uri-reference', - }, - contact: { - $ref: '#/definitions/Contact', - }, - license: { - $ref: '#/definitions/License', - }, - version: { - type: 'string', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Contact: { - type: 'object', - properties: { - name: { - type: 'string', - }, - url: { - type: 'string', - format: 'uri-reference', - }, - email: { - type: 'string', - format: 'email', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - License: { - type: 'object', - required: ['name'], - properties: { - name: { - type: 'string', - }, - url: { - type: 'string', - format: 'uri-reference', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Server: { - type: 'object', - required: ['url'], - properties: { - url: { - type: 'string', - }, - description: { - type: 'string', - }, - variables: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/ServerVariable', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - ServerVariable: { - type: 'object', - required: ['default'], - properties: { - enum: { - type: 'array', - items: { - type: 'string', - }, - }, - default: { - type: 'string', - }, - description: { - type: 'string', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Components: { - type: 'object', - properties: { - schemas: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - }, - responses: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/Response', - }, - ], - }, - }, - }, - parameters: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/Parameter', - }, - ], - }, - }, - }, - examples: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/Example', - }, - ], - }, - }, - }, - requestBodies: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/RequestBody', - }, - ], - }, - }, - }, - headers: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/Header', - }, - ], - }, - }, - }, - securitySchemes: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/SecurityScheme', - }, - ], - }, - }, - }, - links: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/Link', - }, - ], - }, - }, - }, - callbacks: { - type: 'object', - patternProperties: { - '^[a-zA-Z0-9\\.\\-_]+$': { - oneOf: [ - { - $ref: '#/definitions/Reference', - }, - { - $ref: '#/definitions/Callback', - }, - ], - }, - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Schema: { - type: 'object', - properties: { - title: { - type: 'string', - }, - multipleOf: { - type: 'number', - minimum: 0, - exclusiveMinimum: true, - }, - maximum: { - type: 'number', - }, - exclusiveMaximum: { - type: 'boolean', - default: false, - }, - minimum: { - type: 'number', - }, - exclusiveMinimum: { - type: 'boolean', - default: false, - }, - maxLength: { - type: 'integer', - minimum: 0, - }, - minLength: { - type: 'integer', - minimum: 0, - default: 0, - }, - pattern: { - type: 'string', - format: 'regex', - }, - maxItems: { - type: 'integer', - minimum: 0, - }, - minItems: { - type: 'integer', - minimum: 0, - default: 0, - }, - uniqueItems: { - type: 'boolean', - default: false, - }, - maxProperties: { - type: 'integer', - minimum: 0, - }, - minProperties: { - type: 'integer', - minimum: 0, - default: 0, - }, - required: { - type: 'array', - items: { - type: 'string', - }, - minItems: 1, - uniqueItems: true, - }, - enum: { - type: 'array', - items: {}, - minItems: 1, - uniqueItems: false, - }, - type: { - type: 'string', - enum: ['array', 'boolean', 'integer', 'number', 'object', 'string'], - }, - not: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - allOf: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - oneOf: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - anyOf: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - items: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - properties: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - { - type: 'boolean', - }, - ], - default: true, - }, - description: { - type: 'string', - }, - format: { - type: 'string', - }, - default: {}, - nullable: { - type: 'boolean', - default: false, - }, - discriminator: { - $ref: '#/definitions/Discriminator', - }, - readOnly: { - type: 'boolean', - default: false, - }, - writeOnly: { - type: 'boolean', - default: false, - }, - example: {}, - externalDocs: { - $ref: '#/definitions/ExternalDocumentation', - }, - deprecated: { - type: 'boolean', - default: false, - }, - xml: { - $ref: '#/definitions/XML', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Discriminator: { - type: 'object', - required: ['propertyName'], - properties: { - propertyName: { - type: 'string', - }, - mapping: { - type: 'object', - additionalProperties: { - type: 'string', - }, - }, - }, - }, - XML: { - type: 'object', - properties: { - name: { - type: 'string', - }, - namespace: { - type: 'string', - format: 'uri', - }, - prefix: { - type: 'string', - }, - attribute: { - type: 'boolean', - default: false, - }, - wrapped: { - type: 'boolean', - default: false, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Response: { - type: 'object', - required: ['description'], - properties: { - description: { - type: 'string', - }, - headers: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Header', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - content: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/MediaType', - }, - }, - links: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Link', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - MediaType: { - type: 'object', - properties: { - schema: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - example: {}, - examples: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Example', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - encoding: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/Encoding', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - allOf: [ - { - $ref: '#/definitions/ExampleXORExamples', - }, - ], - }, - Example: { - type: 'object', - properties: { - summary: { - type: 'string', - }, - description: { - type: 'string', - }, - value: {}, - externalValue: { - type: 'string', - format: 'uri-reference', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Header: { - type: 'object', - properties: { - description: { - type: 'string', - }, - required: { - type: 'boolean', - default: false, - }, - deprecated: { - type: 'boolean', - default: false, - }, - allowEmptyValue: { - type: 'boolean', - default: false, - }, - style: { - type: 'string', - enum: ['simple'], - default: 'simple', - }, - explode: { - type: 'boolean', - }, - allowReserved: { - type: 'boolean', - default: false, - }, - schema: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - content: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/MediaType', - }, - minProperties: 1, - maxProperties: 1, - }, - example: {}, - examples: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Example', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - allOf: [ - { - $ref: '#/definitions/ExampleXORExamples', - }, - { - $ref: '#/definitions/SchemaXORContent', - }, - ], - }, - Paths: { - type: 'object', - patternProperties: { - '^\\/': { - $ref: '#/definitions/PathItem', - }, - '^x-': {}, - }, - additionalProperties: false, - }, - PathItem: { - type: 'object', - properties: { - $ref: { - type: 'string', - }, - summary: { - type: 'string', - }, - description: { - type: 'string', - }, - servers: { - type: 'array', - items: { - $ref: '#/definitions/Server', - }, - }, - parameters: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#/definitions/Parameter', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - uniqueItems: true, - }, - }, - patternProperties: { - '^(get|put|post|delete|options|head|patch|trace)$': { - $ref: '#/definitions/Operation', - }, - '^x-': {}, - }, - additionalProperties: false, - }, - Operation: { - type: 'object', - required: ['responses'], - properties: { - tags: { - type: 'array', - items: { - type: 'string', - }, - }, - summary: { - type: 'string', - }, - description: { - type: 'string', - }, - externalDocs: { - $ref: '#/definitions/ExternalDocumentation', - }, - operationId: { - type: 'string', - }, - parameters: { - type: 'array', - items: { - oneOf: [ - { - $ref: '#/definitions/Parameter', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - uniqueItems: true, - }, - requestBody: { - oneOf: [ - { - $ref: '#/definitions/RequestBody', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - responses: { - $ref: '#/definitions/Responses', - }, - callbacks: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Callback', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - deprecated: { - type: 'boolean', - default: false, - }, - security: { - type: 'array', - items: { - $ref: '#/definitions/SecurityRequirement', - }, - }, - servers: { - type: 'array', - items: { - $ref: '#/definitions/Server', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Responses: { - type: 'object', - properties: { - default: { - oneOf: [ - { - $ref: '#/definitions/Response', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - patternProperties: { - '^[1-5](?:\\d{2}|XX)$': { - oneOf: [ - { - $ref: '#/definitions/Response', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - '^x-': {}, - }, - minProperties: 1, - additionalProperties: false, - }, - SecurityRequirement: { - type: 'object', - additionalProperties: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - Tag: { - type: 'object', - required: ['name'], - properties: { - name: { - type: 'string', - }, - description: { - type: 'string', - }, - externalDocs: { - $ref: '#/definitions/ExternalDocumentation', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - ExternalDocumentation: { - type: 'object', - required: ['url'], - properties: { - description: { - type: 'string', - }, - url: { - type: 'string', - format: 'uri-reference', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - ExampleXORExamples: { - description: 'Example and examples are mutually exclusive', - not: { - required: ['example', 'examples'], - }, - }, - SchemaXORContent: { - description: - 'Schema and content are mutually exclusive, at least one is required', - not: { - required: ['schema', 'content'], - }, - oneOf: [ - { - required: ['schema'], - }, - { - required: ['content'], - description: 'Some properties are not allowed if content is present', - allOf: [ - { - not: { - required: ['style'], - }, - }, - { - not: { - required: ['explode'], - }, - }, - { - not: { - required: ['allowReserved'], - }, - }, - { - not: { - required: ['example'], - }, - }, - { - not: { - required: ['examples'], - }, - }, - ], - }, - ], - }, - Parameter: { - type: 'object', - properties: { - name: { - type: 'string', - }, - in: { - type: 'string', - }, - description: { - type: 'string', - }, - required: { - type: 'boolean', - default: false, - }, - deprecated: { - type: 'boolean', - default: false, - }, - allowEmptyValue: { - type: 'boolean', - default: false, - }, - style: { - type: 'string', - }, - explode: { - type: 'boolean', - }, - allowReserved: { - type: 'boolean', - default: false, - }, - schema: { - oneOf: [ - { - $ref: '#/definitions/Schema', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - content: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/MediaType', - }, - minProperties: 1, - maxProperties: 1, - }, - example: {}, - examples: { - type: 'object', - additionalProperties: { - oneOf: [ - { - $ref: '#/definitions/Example', - }, - { - $ref: '#/definitions/Reference', - }, - ], - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - required: ['name', 'in'], - allOf: [ - { - $ref: '#/definitions/ExampleXORExamples', - }, - { - $ref: '#/definitions/SchemaXORContent', - }, - { - $ref: '#/definitions/ParameterLocation', - }, - ], - }, - ParameterLocation: { - description: 'Parameter location', - oneOf: [ - { - description: 'Parameter in path', - required: ['required'], - properties: { - in: { - enum: ['path'], - }, - style: { - enum: ['matrix', 'label', 'simple'], - default: 'simple', - }, - required: { - enum: [true], - }, - }, - }, - { - description: 'Parameter in query', - properties: { - in: { - enum: ['query'], - }, - style: { - enum: ['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], - default: 'form', - }, - }, - }, - { - description: 'Parameter in header', - properties: { - in: { - enum: ['header'], - }, - style: { - enum: ['simple'], - default: 'simple', - }, - }, - }, - { - description: 'Parameter in cookie', - properties: { - in: { - enum: ['cookie'], - }, - style: { - enum: ['form'], - default: 'form', - }, - }, - }, - ], - }, - RequestBody: { - type: 'object', - required: ['content'], - properties: { - description: { - type: 'string', - }, - content: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/MediaType', - }, - }, - required: { - type: 'boolean', - default: false, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - SecurityScheme: { - oneOf: [ - { - $ref: '#/definitions/APIKeySecurityScheme', - }, - { - $ref: '#/definitions/HTTPSecurityScheme', - }, - { - $ref: '#/definitions/OAuth2SecurityScheme', - }, - { - $ref: '#/definitions/OpenIdConnectSecurityScheme', - }, - ], - }, - APIKeySecurityScheme: { - type: 'object', - required: ['type', 'name', 'in'], - properties: { - type: { - type: 'string', - enum: ['apiKey'], - }, - name: { - type: 'string', - }, - in: { - type: 'string', - enum: ['header', 'query', 'cookie'], - }, - description: { - type: 'string', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - HTTPSecurityScheme: { - type: 'object', - required: ['scheme', 'type'], - properties: { - scheme: { - type: 'string', - }, - bearerFormat: { - type: 'string', - }, - description: { - type: 'string', - }, - type: { - type: 'string', - enum: ['http'], - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - oneOf: [ - { - description: 'Bearer', - properties: { - scheme: { - enum: ['bearer'], - }, - }, - }, - { - description: 'Non Bearer', - not: { - required: ['bearerFormat'], - }, - properties: { - scheme: { - not: { - enum: ['bearer'], - }, - }, - }, - }, - ], - }, - OAuth2SecurityScheme: { - type: 'object', - required: ['type', 'flows'], - properties: { - type: { - type: 'string', - enum: ['oauth2'], - }, - flows: { - $ref: '#/definitions/OAuthFlows', - }, - description: { - type: 'string', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - OpenIdConnectSecurityScheme: { - type: 'object', - required: ['type', 'openIdConnectUrl'], - properties: { - type: { - type: 'string', - enum: ['openIdConnect'], - }, - openIdConnectUrl: { - type: 'string', - format: 'uri-reference', - }, - description: { - type: 'string', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - OAuthFlows: { - type: 'object', - properties: { - implicit: { - $ref: '#/definitions/ImplicitOAuthFlow', - }, - password: { - $ref: '#/definitions/PasswordOAuthFlow', - }, - clientCredentials: { - $ref: '#/definitions/ClientCredentialsFlow', - }, - authorizationCode: { - $ref: '#/definitions/AuthorizationCodeOAuthFlow', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - ImplicitOAuthFlow: { - type: 'object', - required: ['authorizationUrl', 'scopes'], - properties: { - authorizationUrl: { - type: 'string', - format: 'uri-reference', - }, - refreshUrl: { - type: 'string', - format: 'uri-reference', - }, - scopes: { - type: 'object', - additionalProperties: { - type: 'string', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - PasswordOAuthFlow: { - type: 'object', - required: ['tokenUrl'], - properties: { - tokenUrl: { - type: 'string', - format: 'uri-reference', - }, - refreshUrl: { - type: 'string', - format: 'uri-reference', - }, - scopes: { - type: 'object', - additionalProperties: { - type: 'string', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - ClientCredentialsFlow: { - type: 'object', - required: ['tokenUrl'], - properties: { - tokenUrl: { - type: 'string', - format: 'uri-reference', - }, - refreshUrl: { - type: 'string', - format: 'uri-reference', - }, - scopes: { - type: 'object', - additionalProperties: { - type: 'string', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - AuthorizationCodeOAuthFlow: { - type: 'object', - required: ['authorizationUrl', 'tokenUrl'], - properties: { - authorizationUrl: { - type: 'string', - format: 'uri-reference', - }, - tokenUrl: { - type: 'string', - format: 'uri-reference', - }, - refreshUrl: { - type: 'string', - format: 'uri-reference', - }, - scopes: { - type: 'object', - additionalProperties: { - type: 'string', - }, - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - }, - Link: { - type: 'object', - properties: { - operationId: { - type: 'string', - }, - operationRef: { - type: 'string', - format: 'uri-reference', - }, - parameters: { - type: 'object', - additionalProperties: {}, - }, - requestBody: {}, - description: { - type: 'string', - }, - server: { - $ref: '#/definitions/Server', - }, - }, - patternProperties: { - '^x-': {}, - }, - additionalProperties: false, - not: { - description: 'Operation Id and Operation Ref are mutually exclusive', - required: ['operationId', 'operationRef'], - }, - }, - Callback: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/PathItem', - }, - patternProperties: { - '^x-': {}, - }, - }, - Encoding: { - type: 'object', - properties: { - contentType: { - type: 'string', - }, - headers: { - type: 'object', - additionalProperties: { - $ref: '#/definitions/Header', - }, - }, - style: { - type: 'string', - enum: ['form', 'spaceDelimited', 'pipeDelimited', 'deepObject'], - }, - explode: { - type: 'boolean', - }, - allowReserved: { - type: 'boolean', - default: false, - }, - }, - additionalProperties: false, - }, - }, -}; diff --git a/src/types.ts b/src/types.ts index d12d0c5..42e1fb8 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,17 @@ import type { JSONSchema } from '@apidevtools/json-schema-ref-parser'; import type $RefParser from '@apidevtools/json-schema-ref-parser'; -export type addPrefixToObject = { +export type addPrefixToObject = { [K in keyof JSONSchema as `x-${K}`]: JSONSchema[K]; }; export interface Options { cloneSchema?: boolean; dereference?: boolean; + convertUnreferencedDefinitions?: boolean; dereferenceOptions?: $RefParser.Options; } -type ExtendedJSONSchema = addPrefixToObject & JSONSchema; +type ExtendedJSONSchema = addPrefixToObject & JSONSchema; export type SchemaType = ExtendedJSONSchema & { example?: JSONSchema['examples'][number]; 'x-patternProperties'?: JSONSchema['patternProperties']; diff --git a/test/dereference_schema.test.ts b/test/dereference_schema.test.ts index d3ec1fe..73967da 100644 --- a/test/dereference_schema.test.ts +++ b/test/dereference_schema.test.ts @@ -1,6 +1,7 @@ import convert from '../src'; import { join } from 'path'; import nock from 'nock'; +import * as path from 'path'; it('not dereferencing schema by default', async ({ expect }) => { const schema = { @@ -21,6 +22,12 @@ it('not dereferencing schema by default', async ({ expect }) => { if ('$schema' in expected) { delete expected.$schema; } + expected.definitions = { + foo: { + type: 'string', + nullable: true, + }, + }; expect(result).toEqual(expected); }); @@ -42,7 +49,7 @@ it('dereferencing schema with deference option', async ({ expect }) => { type: 'string', nullable: true, definitions: { - foo: ['string', 'null'], + foo: { type: 'string', nullable: true }, }, }; @@ -218,12 +225,14 @@ it('dereferencing schema with remote http and https references', async ({ }); it('dereferencing schema with file references', async ({ expect }) => { + const fileRef = join(__dirname, 'fixtures/definitions.yaml#/definitions/bar'); + const unixStyle = path.resolve(fileRef).split(path.sep).join('/'); const schema = { $schema: 'http://json-schema.org/draft-04/schema#', allOf: [ // points to current working directory, hence the `test` prefix { $ref: './test/fixtures/definitions.yaml#/definitions/foo' }, - { $ref: join(__dirname, 'fixtures/definitions.yaml#/definitions/bar') }, + { $ref: unixStyle }, ], }; diff --git a/test/nullable.test.ts b/test/nullable.test.ts index 33a402e..3c43284 100644 --- a/test/nullable.test.ts +++ b/test/nullable.test.ts @@ -26,6 +26,112 @@ it('supports nullables inside sub-schemas', async ({ expect }) => { oneOf: [{ type: 'string' }, { nullable: true }], }); }); +it('supports nullables inside definitions', async ({ expect }) => { + const schema = { + $schema: 'http://json-schema.org/draft-07/schema#', + definitions: { + Product: { + type: 'object', + properties: { + name: { + type: 'string', + }, + price: { + type: 'number', + }, + rating: { + type: ['null', 'number'], + }, + }, + required: ['name', 'price', 'rating'], + }, + ProductList: { + type: 'object', + properties: { + name: { + type: 'string', + }, + version: { + type: 'string', + }, + products: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + price: { + type: 'number', + }, + rating: { + type: ['null', 'number'], + }, + }, + required: ['name', 'price', 'rating'], + }, + }, + }, + required: ['name', 'products', 'version'], + }, + }, + }; + + const result = await convert(schema); + + expect(result).toEqual({ + definitions: { + Product: { + type: 'object', + properties: { + name: { + type: 'string', + }, + price: { + type: 'number', + }, + rating: { + type: 'number', + nullable: true, + }, + }, + required: ['name', 'price', 'rating'], + }, + ProductList: { + type: 'object', + properties: { + name: { + type: 'string', + }, + version: { + type: 'string', + }, + products: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string', + }, + price: { + type: 'number', + }, + rating: { + type: 'number', + nullable: true, + }, + }, + required: ['name', 'price', 'rating'], + }, + }, + }, + required: ['name', 'products', 'version'], + }, + }, + }); +}); it('does not add nullable for non null types', async ({ expect }) => { const schema = { diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..f6fec51 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "ES2015", + "lib": ["es2015", "dom"], + "types": ["vitest/globals"], + "rootDir": "." + }, + "include": ["."] +} diff --git a/tsconfig.json b/tsconfig.json index 2cff80e..5e3eaa4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,14 +16,14 @@ "noFallthroughCasesInSwitch": true, "pretty": true, "resolveJsonModule": true, - "rootDir": ".", + "rootDir": "src", "skipLibCheck": true, "noImplicitAny": false, "strict": true, - "sourceMap": true, - "types": ["vitest/globals"] + "noUnusedParameters": true, + "sourceMap": true }, "compileOnSave": false, "exclude": ["node_modules", "dist", "coverage", "bin"], - "include": ["src", "test"] + "include": ["src"] } diff --git a/vite.config.ts b/vite.config.ts index 1c49dac..e9afdad 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,8 +3,12 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, - environment: 'node', watch: false, threads: false, + isolate: false, + reporters: 'verbose', + }, + esbuild: { + target: 'node10', }, }); From bffd34229bd0dd1d398815fa210da34c94ab27b8 Mon Sep 17 00:00:00 2001 From: JonLuca DeCaro Date: Fri, 5 Aug 2022 10:54:13 -0400 Subject: [PATCH 2/2] re-add binn --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ce25905..c1dc747 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ }, "types": "dist/mjs/index.d.ts", "files": [ + "/bin", "/dist" ], "main": "dist/cjs/index.js",