Skip to content

Commit

Permalink
Merge pull request #15 from lifeomic/publish-schema
Browse files Browse the repository at this point in the history
[Proposal] Add basic tool for generating a publishable artifact
  • Loading branch information
swain authored May 31, 2022
2 parents 25cbcab + b3bb6e2 commit d219c72
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 14 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 14 additions & 12 deletions src/bin/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ exports[`input validation snapshots bogus command name 1`] = `
"cli.ts <command>
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]
Expand All @@ -22,12 +23,13 @@ exports[`input validation snapshots empty input 1`] = `
"cli.ts <command>
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]
Expand Down
27 changes: 25 additions & 2 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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('.', '');
Expand All @@ -25,14 +27,16 @@ const writeGeneratedFile = (
filepath: string,
content: string,
options: { format: boolean },
) =>
) => {
mkdirSync(path.dirname(filepath), { recursive: true });
writeFileSync(
filepath,
options.format
? format(content, { parser: getPrettierParser(filepath) })
: content,
{ encoding: 'utf-8' },
);
};

const VALID_ASSUMPTION_KEYS: (keyof SchemaAssumptions)[] = [
'noAdditionalPropertiesOnObjects',
Expand Down Expand Up @@ -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();

Expand Down
96 changes: 96 additions & 0 deletions src/generate-publishable-schema.test.ts
Original file line number Diff line number Diff line change
@@ -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(),
);
});
28 changes: 28 additions & 0 deletions src/generate-publishable-schema.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
};

export const generatePublishableSchema = ({
spec,
}: GeneratePublishableSchemaInput): GeneratePublishableSchemaOutput => {
const files: Record<string, string> = {
'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 };
};
12 changes: 12 additions & 0 deletions src/meta-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ export type EndpointDefinition = {
Response: JSONSchema4;
};

export type OneSchemaMetaDefinition = {
PackageJSON?: Record<string, unknown>;
};

export type OneSchemaDefinition = {
Meta?: OneSchemaMetaDefinition;

Resources?: {
[key: string]: JSONSchema4;
};
Expand Down

0 comments on commit d219c72

Please sign in to comment.