diff --git a/README.md b/README.md index 61721a6..76fb8c6 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,35 @@ const server = new Koa() .listen(); ``` +### Distributing Schemas + +Use the `generate-publishable-schema` command in concert with the `Meta.PackageJSON` entry to generate a ready-to-publish NPM artifact containing the schema. + +```yaml +# schema.yml +Meta: + PackageJSON: + name: desired-package-name + description: A description of the package + # ... any other desired package.json values +# ... +``` + +```bash +one-schema generate-publishable \ + --schema schema.yml \ + --output output-directory +``` + +The `output-directory` will have this file structure: + +``` +output-directory/ + package.json + schema.json + schema.yaml +``` + ### OpenAPI Spec generation Use the `generate-open-api-spec` command to generate an OpenAPI spec from a simple schema, which may be useful for interfacing with common OpenAPI tooling. diff --git a/src/bin/__snapshots__/cli.test.ts.snap b/src/bin/__snapshots__/cli.test.ts.snap index 95b7ffe..fb35250 100644 --- a/src/bin/__snapshots__/cli.test.ts.snap +++ b/src/bin/__snapshots__/cli.test.ts.snap @@ -4,12 +4,13 @@ exports[`input validation snapshots bogus command name 1`] = ` "cli.ts Commands: - cli.ts generate-axios-client Generates an Axios client using the specified - schema and options. - cli.ts generate-api-types Generates API types using the specified schema - and options. - cli.ts generate-open-api-spec Generates an OpenAPI v3.1.0 spec using the - specified schema and options. + cli.ts generate-axios-client Generates an Axios client using the + specified schema and options. + cli.ts generate-api-types Generates API types using the specified + schema and options. + cli.ts generate-open-api-spec Generates an OpenAPI v3.1.0 spec using the + specified schema and options. + cli.ts generate-publishable-schema Generates a publishable schema artifact. Options: --help Show help [boolean] @@ -22,12 +23,13 @@ exports[`input validation snapshots empty input 1`] = ` "cli.ts Commands: - cli.ts generate-axios-client Generates an Axios client using the specified - schema and options. - cli.ts generate-api-types Generates API types using the specified schema - and options. - cli.ts generate-open-api-spec Generates an OpenAPI v3.1.0 spec using the - specified schema and options. + cli.ts generate-axios-client Generates an Axios client using the + specified schema and options. + cli.ts generate-api-types Generates API types using the specified + schema and options. + cli.ts generate-open-api-spec Generates an OpenAPI v3.1.0 spec using the + specified schema and options. + cli.ts generate-publishable-schema Generates a publishable schema artifact. Options: --help Show help [boolean] diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 0e47927..4394445 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { writeFileSync } from 'fs'; +import { mkdirSync, writeFileSync } from 'fs'; import { extname } from 'path'; import yargs = require('yargs'); import { format, BuiltInParserName } from 'prettier'; @@ -9,6 +9,8 @@ import { generateAxiosClient } from '../generate-axios-client'; import { generateAPITypes } from '../generate-api-types'; import { loadSchemaFromFile, SchemaAssumptions } from '../meta-schema'; import { toOpenAPISpec } from '../openapi'; +import { generatePublishableSchema } from '../generate-publishable-schema'; +import path = require('path'); const getPrettierParser = (outputFilename: string): BuiltInParserName => { const extension = extname(outputFilename).replace('.', ''); @@ -25,7 +27,8 @@ const writeGeneratedFile = ( filepath: string, content: string, options: { format: boolean }, -) => +) => { + mkdirSync(path.dirname(filepath), { recursive: true }); writeFileSync( filepath, options.format @@ -33,6 +36,7 @@ const writeGeneratedFile = ( : content, { encoding: 'utf-8' }, ); +}; const VALID_ASSUMPTION_KEYS: (keyof SchemaAssumptions)[] = [ 'noAdditionalPropertiesOnObjects', @@ -173,6 +177,25 @@ const program = yargs(process.argv.slice(2)) writeGeneratedFile(argv.output, output, { format: argv.format }); }, ) + .command( + 'generate-publishable-schema', + 'Generates a publishable schema artifact.', + getCommonOptions, + (argv) => { + const spec = loadSchemaFromFile( + argv.schema, + parseAssumptions(argv.assumptions), + ); + + const { files } = generatePublishableSchema({ spec }); + + for (const [filename, content] of Object.entries(files)) { + writeGeneratedFile(path.resolve(argv.output, filename), content, { + format: true, + }); + } + }, + ) .demandCommand() .strict(); diff --git a/src/generate-publishable-schema.test.ts b/src/generate-publishable-schema.test.ts new file mode 100644 index 0000000..cf3c9a6 --- /dev/null +++ b/src/generate-publishable-schema.test.ts @@ -0,0 +1,96 @@ +import { generatePublishableSchema } from './generate-publishable-schema'; + +test('skips generating a package.json if there is no PackageJSON entry', () => { + const result = generatePublishableSchema({ + spec: { + Endpoints: { + 'GET /posts': { + Name: 'listPosts', + Response: {}, + Request: {}, + }, + }, + }, + }); + + expect(result.files['package.json']).toBeUndefined(); +}); + +test('generates the correct files when there is a PackageJSON entry', () => { + const result = generatePublishableSchema({ + spec: { + Meta: { + PackageJSON: { + name: '@lifeomic/test-service-schema', + description: 'The OneSchema for a test-service', + testObject: { + some: 'value', + }, + }, + }, + Endpoints: { + 'GET /posts': { + Name: 'listPosts', + Response: {}, + Request: {}, + }, + }, + }, + }); + + expect(Object.keys(result.files)).toStrictEqual([ + 'schema.json', + 'schema.yaml', + 'package.json', + ]); + + expect(result.files['schema.json']).toStrictEqual( + ` +{ + "Meta": { + "PackageJSON": { + "name": "@lifeomic/test-service-schema", + "description": "The OneSchema for a test-service", + "testObject": { + "some": "value" + } + } + }, + "Endpoints": { + "GET /posts": { + "Name": "listPosts", + "Response": {}, + "Request": {} + } + } +}`.trim(), + ); + + expect(result.files['schema.yaml']).toStrictEqual( + ` +Meta: + PackageJSON: + name: '@lifeomic/test-service-schema' + description: The OneSchema for a test-service + testObject: + some: value +Endpoints: + GET /posts: + Name: listPosts + Response: {} + Request: {} +`.trimStart(), + ); + + expect(result.files['package.json']).toStrictEqual( + ` +{ + "name": "@lifeomic/test-service-schema", + "description": "The OneSchema for a test-service", + "testObject": { + "some": "value" + } +} +`.trim(), + ); +}); diff --git a/src/generate-publishable-schema.ts b/src/generate-publishable-schema.ts new file mode 100644 index 0000000..979b1de --- /dev/null +++ b/src/generate-publishable-schema.ts @@ -0,0 +1,28 @@ +import * as jsyaml from 'js-yaml'; +import { OneSchemaDefinition } from '.'; + +export type GeneratePublishableSchemaInput = { + spec: OneSchemaDefinition; +}; + +export type GeneratePublishableSchemaOutput = { + /** + * A map of filename -> file content to generate. + */ + files: Record; +}; + +export const generatePublishableSchema = ({ + spec, +}: GeneratePublishableSchemaInput): GeneratePublishableSchemaOutput => { + const files: Record = { + 'schema.json': JSON.stringify(spec, null, 2), + 'schema.yaml': jsyaml.dump(spec), + }; + + if (spec.Meta?.PackageJSON) { + files['package.json'] = JSON.stringify(spec.Meta.PackageJSON, null, 2); + } + + return { files }; +}; diff --git a/src/meta-schema.ts b/src/meta-schema.ts index 46c53d4..ff0bf1c 100644 --- a/src/meta-schema.ts +++ b/src/meta-schema.ts @@ -14,10 +14,22 @@ export const getPathParams = (name: string) => .map((part) => part.replace(':', '')); const ONE_SCHEMA_META_SCHEMA: JSONSchema4 = { + definitions: { + MetaConfig: { + type: 'object', + additionalProperties: false, + properties: { + PackageJSON: { type: 'object' }, + }, + }, + }, type: 'object', additionalProperties: false, required: ['Endpoints'], properties: { + Meta: { + $ref: '#/definitions/MetaConfig', + }, Resources: { type: 'object', patternProperties: { diff --git a/src/types.ts b/src/types.ts index 4326043..04ccfb9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,13 @@ export type EndpointDefinition = { Response: JSONSchema4; }; +export type OneSchemaMetaDefinition = { + PackageJSON?: Record; +}; + export type OneSchemaDefinition = { + Meta?: OneSchemaMetaDefinition; + Resources?: { [key: string]: JSONSchema4; };