Skip to content

Commit

Permalink
Add cli (#15)
Browse files Browse the repository at this point in the history
Add cli
  • Loading branch information
NathanJAdams authored Oct 29, 2024
1 parent 2815dd8 commit 4926f27
Show file tree
Hide file tree
Showing 17 changed files with 136 additions and 31 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,28 @@ yarn add json-schema-typescript-generator -D

## Usage

To generate .ts files from the command line, run the command `json-schema-typescript-generator`.

Usage: `json-schema-typescript-generator [options]`

| Option | Description |
|:---------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| -c, --cwd <DIRECTORY> | Working directory (default: `process.cwd()`) |
| -s, --source <DIRECTORY> | Source directory of schema files, relative to working directory (default: `src/schemas`) |
| -d, --destination <DIRECTORY> | Destination directory of generated .ts files, relative to working directory (default: `src/generated`) |
| -e, --encoding <ENCODING> | Encoding of source files, one of: [`ascii`, `utf8`, `utf-8`, `utf16le`, `ucs2`, `ucs-2`, `base64`, `base64url`, `latin1`, `binary`, `hex`] (default: `utf-8`) |
| -r, --recursive | Recurse into source directory and generate from nested files (default: `true`) |
| -R, --no-recursive | Only generate from schema files directly in source directory, do not recurse deeper |
| -p, --pre-clean | Delete destination directory before generating files (default: `false`) |
| -P, --no-pre-clean | Overwrite destination directory when generating files |
| -i, --index-files | Add index files (default: `true`) |
| -I, --no-index-files | Do not add index files |
| -o, --optional-field-pattern <PATTERN> | The pattern to use for optional fields, one of: [`QUESTION`, `PIPE_UNDEFINED`] (default: `QUESTION`) |
| -u, --untyped-type <TYPE> | The untyped field type, one of: [`ANY`, `NEVER`, `UNDEFINED`, `UNKNOWN`] (default: `UNKNOWN`) |
| -h, --help | Display help for command |


It is also possible to run the command within code by importing from the library.
To generate .ts files, invoke the `generateFiles` function with an `Options` object like so

```ts
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json-schema-typescript-generator",
"version": "2.1.1",
"version": "2.2.0",
"author": {
"name": "Nathan Adams",
"url": "https://github.com/NathanJAdams"
Expand All @@ -21,13 +21,18 @@
],
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"json-schema-typescript-generator": "./dist/cli/generate.js"
},
"scripts": {
"generate": "ts-node src/cli/generate.ts generate",
"build": "yarn lint && yarn tsc",
"lint": "eslint . --fix",
"test": "jest --silent=false",
"tsc": "rimraf ./dist && tsc"
},
"dependencies": {
"commander": "12.1.0",
"rimraf": "3.0.2"
},
"devDependencies": {
Expand Down
73 changes: 73 additions & 0 deletions src/cli/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
#!/usr/bin/env node

import { Command } from 'commander';
import { OptionalFieldPattern, Options, UntypedType } from '../options';
import { generateFiles } from '../generateFiles';

// These are valid values for the type BufferEncoding
const validEncodings = ['ascii', 'utf8', 'utf-8', 'utf16le', 'ucs2', 'ucs-2', 'base64', 'base64url', 'latin1', 'binary', 'hex'];
const validPatternNames = Object.keys(OptionalFieldPattern);
const validTypeNames = Object.keys(UntypedType);

const validateEncoding = (encoding: string): string => {
if (validEncodings.includes(encoding)) {
return encoding;
}
throw Error(`Invalid source file encoding: '${encoding}'. Valid options are: ${arrayToString(validEncodings)}`);
};

const validatePattern = (patternName: string): string => {
if (validPatternNames.includes(patternName)) {
return patternName;
}
throw Error(`Invalid optional field pattern: '${patternName}'. Valid options are: ${arrayToString(validPatternNames)}`);
};

const validateType = (typeName: string): string => {
if (validTypeNames.includes(typeName)) {
return typeName;
}
throw Error(`Invalid untyped type: '${typeName}'. Valid options are: ${arrayToString(validTypeNames)}`);
};

const arrayToString = (array: string[]) => `['${array.join('\', \'')}']`;

const program = new Command();

program
.option('-c, --cwd <DIRECTORY>', 'Working directory', process.cwd())
.option('-s, --source <DIRECTORY>', 'Source directory of schema files, relative to working directory', 'src/schemas')
.option('-d, --destination <DIRECTORY>', 'Destination directory of generated .ts files, relative to working directory', 'src/generated')
.option('-e, --encoding <ENCODING>', `Encoding of source files, one of: ${arrayToString(validEncodings)}`, validateEncoding, 'utf-8')
.option('-r, --recursive', 'Recurse into source directory and generate from nested files', true)
.option('-R, --no-recursive', 'Only generate from schema files directly in source directory, do not recurse deeper')
.option('-p, --pre-clean', 'Delete destination directory before generating files', false)
.option('-P, --no-pre-clean', 'Overwrite destination directory when generating files')
.option('-i, --index-files', 'Add index files', true)
.option('-I, --no-index-files', 'Do not add index files')
.option('-o, --optional-field-pattern <PATTERN>', `The pattern to use for optional fields, one of: ${arrayToString(validPatternNames)}`, validatePattern, 'QUESTION')
.option('-u, --untyped-type <TYPE>', `The untyped field type, one of: ${arrayToString(validTypeNames)}`, validateType, 'UNKNOWN')
.action(async (opts) => {
const options: Options = {
files: {
cwd: opts.cwd,
destination: {
dir: opts.destination,
indexFiles: opts.indexFiles,
preClean: opts.preClean
},
source: {
dir: opts.source,
encoding: opts.encoding,
recursive: opts.recursive
}
},
ts: {
optionalFields: opts.optionalFieldPattern as OptionalFieldPattern,
untyped: opts.untypedType as UntypedType
}
};
await generateFiles(options);
});

program.parse(process.argv);
2 changes: 1 addition & 1 deletion src/generate/OneOfN-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const SuppressNGenerator = (suppressCount: number): string => {

export const OneOfNGenerator = (typeCount: number): string | undefined => {
if (!Number.isInteger(typeCount) || typeCount < 2) {
return undefined;
return;
}
const typeArgs: string[] = [];
for (let i = 0; i < typeCount; i++) {
Expand Down
4 changes: 2 additions & 2 deletions src/generate/all-of-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { filtered } from '../util';
export const allOfGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
const schema = locatedSchema.schema;
if (!schema.allOf || schema.allOf.length === 0) {
return undefined;
return;
}
const lines: (string | undefined)[] = [];
schema.allOf.forEach((elementSchema: Schema) => {
Expand All @@ -16,7 +16,7 @@ export const allOfGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gath
});
const filteredLines: string[] = filtered(lines);
if (filteredLines.length === 0) {
return undefined;
return;
}
return filteredLines.join('\n& ');
};
4 changes: 2 additions & 2 deletions src/generate/any-of-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { typeGenerator } from './type-generator';
export const anyOfGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
const schema = locatedSchema.schema;
if (!schema.anyOf || schema.anyOf.length === 0) {
return undefined;
return;
}
const lines: (string | undefined)[] = [];
schema.anyOf.forEach((elementSchema: Schema) => {
Expand All @@ -16,7 +16,7 @@ export const anyOfGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gath
});
const filteredLines: string[] = filtered(lines);
if (filteredLines.length === 0) {
return undefined;
return;
} else if (filteredLines.length === 1) {
return filteredLines[0];
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/generate/basic-type-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PRIMITIVE_TYPES.set('string', 'string');
export const basicTypeGenerator: TypeGenerator = (locatedSchema: LocatedSchema, _gatheredInfo: SchemaGatheredInfo, _inputInfo: SchemaInputInfo): string | undefined => {
const schemaTypes = locatedSchema.schema.type;
if (!schemaTypes || schemaTypes.size === 0) {
return undefined;
return;
}
const tsTypesSet: Set<string> = new Set();
Array.from(PRIMITIVE_TYPES.entries())
Expand All @@ -20,7 +20,7 @@ export const basicTypeGenerator: TypeGenerator = (locatedSchema: LocatedSchema,
.forEach((tsType) => tsTypesSet.add(tsType));
const tsTypes: string[] = Array.from(tsTypesSet);
if (tsTypes.length === 0) {
return undefined;
return;
}
if (tsTypes.length === 1) {
return tsTypes[0];
Expand Down
2 changes: 1 addition & 1 deletion src/generate/collection-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { filteredJoin } from '../util';
export const collectionGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
const schema = locatedSchema.schema;
if (!schema.type || !schema.type.has('array') || !schema.collection) {
return undefined;
return;
}
const collection = schema.collection;
const collectionItems = collection.items;
Expand Down
2 changes: 1 addition & 1 deletion src/generate/constant-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LocatedSchema, TypeGenerator } from './TypeGenerator';
export const constantGenerator: TypeGenerator = (locatedSchema: LocatedSchema): string | undefined => {
const constant = locatedSchema.schema.const;
if (constant === undefined) {
return undefined;
return;
}
return (typeof constant === 'string')
? `'${constant}'`
Expand Down
2 changes: 1 addition & 1 deletion src/generate/enum-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { LocatedSchema, TypeGenerator } from './TypeGenerator';
export const enumGenerator: TypeGenerator = (locatedSchema: LocatedSchema): string | undefined => {
const _enum = locatedSchema.schema.enum;
if (!_enum || _enum.size === 0) {
return undefined;
return;
}
const enumTypes: string[] = [];
_enum.forEach((primitive: SchemaPrimitive) => {
Expand Down
10 changes: 5 additions & 5 deletions src/generate/file-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const schemaContentGenerator = (locatedSchema: LocatedSchema, gatheredInfo: Sche

const importsGenerator = (fileLocation: FileLocation, references: References): string | undefined => {
if (references.schema.size === 0) {
return undefined;
return;
}
const content: (string | undefined)[] = [];
content.push(importMapGenerator(fileLocation, references.schema));
Expand All @@ -50,7 +50,7 @@ const importsGenerator = (fileLocation: FileLocation, references: References): s

const importMapGenerator = (fileLocation: FileLocation, references: Map<FileLocation, Set<string>>): string | undefined => {
if (references.size === 0) {
return undefined;
return;
}
const imports: string[] = [];
references.forEach((names, referenceFileLocation) => {
Expand All @@ -68,7 +68,7 @@ const importMapGenerator = (fileLocation: FileLocation, references: Map<FileLoca

const namedGenerator = (fileLocation: FileLocation, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
if (gatheredInfo.namedSchemas.size === 0) {
return undefined;
return;
}
const content: string[] = [];

Expand All @@ -92,7 +92,7 @@ const namedGenerator = (fileLocation: FileLocation, gatheredInfo: SchemaGathered

const oneOfTypesGenerator = (typeCounts: Set<number>): string | undefined => {
if (typeCounts.size === 0) {
return undefined;
return;
}
const oneOfTypeLines: string[] = [];
typeCounts.forEach((typeCount: number) => {
Expand All @@ -106,7 +106,7 @@ const oneOfTypesGenerator = (typeCounts: Set<number>): string | undefined => {

const schemaMapGenerator = (fileLocation: FileLocation, map: Map<string, Schema> | undefined, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
if (!map || map.size === 0) {
return undefined;
return;
}
const content: string[] = [];
map.forEach((namedSchema, name) => {
Expand Down
2 changes: 1 addition & 1 deletion src/generate/object-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { typeGenerator } from './type-generator';
export const objectGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
const schema = locatedSchema.schema;
if (!schema.type || !schema.type.has('object') || !(schema.object)) {
return undefined;
return;
}
const { properties, required, additionalProperties } = schema.object;
const lines: string[] = [];
Expand Down
4 changes: 2 additions & 2 deletions src/generate/one-of-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { typeGenerator } from './type-generator';
export const oneOfGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gatheredInfo: SchemaGatheredInfo, inputInfo: SchemaInputInfo): string | undefined => {
const schema = locatedSchema.schema;
if (!schema.oneOf || schema.oneOf.length === 0) {
return undefined;
return;
}
const lines: (string | undefined)[] = [];
schema.oneOf.forEach((elementSchema: Schema) => {
Expand All @@ -16,7 +16,7 @@ export const oneOfGenerator: TypeGenerator = (locatedSchema: LocatedSchema, gath
});
const filteredLines: string[] = filtered(lines);
if (filteredLines.length === 0) {
return undefined;
return;
} else if (filteredLines.length === 1) {
return filteredLines[0];
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/generate/reference-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const referenceGenerator: TypeGenerator = (locatedSchema: LocatedSchema,
const id = schema.$id;
const ref = schema.$ref;
if (!ref) {
return undefined;
return;
}
if (isLocal(ref)) {
return ref.fragment;
Expand All @@ -41,7 +41,7 @@ export const referenceGenerator: TypeGenerator = (locatedSchema: LocatedSchema,
return createFromRelativeRef(references, idFileLocations, ref);
}
}
return undefined;
return;
};

const createFromAbsoluteRef = (references: References, idFileLocations: Map<SchemaId, FileLocation>, ref: AbsoluteSchemaRef | AbsoluteFragmentSchemaRef): string | undefined => {
Expand Down
14 changes: 7 additions & 7 deletions src/ids/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type SchemaPath = {
const parseAuthority = (url: string): string | undefined => {
const protocolMatch = url.match(/^([^:/]+:\/\/)/);
if (!protocolMatch) {
return undefined;
return;
}
const protocol = protocolMatch[1];
const withoutProtocol = url.substring(protocol.length);
Expand All @@ -24,7 +24,7 @@ const parseAuthority = (url: string): string | undefined => {
const parsePath = (endpoint: string): SchemaPath | undefined => {
const pathMatch = endpoint.match(/^\/([^#]+)/);
if (!pathMatch) {
return undefined;
return;
}
const match = pathMatch[1];
const lastSlashIndex = match.lastIndexOf('/');
Expand All @@ -44,25 +44,25 @@ const parsePath = (endpoint: string): SchemaPath | undefined => {
const parseFragment = (url: string): string | undefined => {
const pathMatch = url.match(/^[^#]*#\/?(?:(?:\$defs|definitions)\/)?(.*)$/);
if (!pathMatch) {
return undefined;
return;
}
return pathMatch[1];
};

export const parseSchemaId = (id?: string): SchemaId | undefined => {
if (!id) {
return undefined;
return;
}
const authority = parseAuthority(id);
if (authority === undefined && !id.startsWith('/')) {
return undefined;
return;
}
const endpoint = authority === undefined
? id
: id.substring(authority.length);
const path = parsePath(endpoint);
if (!path) {
return undefined;
return;
}
return {
authority,
Expand All @@ -73,7 +73,7 @@ export const parseSchemaId = (id?: string): SchemaId | undefined => {

export const parseSchemaRef = (ref?: string): SchemaRef | undefined => {
if (!ref) {
return undefined;
return;
}
const authority = parseAuthority(ref);
const endpoint = authority === undefined
Expand Down
6 changes: 3 additions & 3 deletions src/schema/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const parseSchema = (rawSchema: RawSchema): Schema => {

const parseType = (type?: string | string[]): Set<SchemaBasicType> | undefined => {
if (!type) {
return undefined;
return;
}
const typeArray = (typeof type === 'string')
? [type]
Expand Down Expand Up @@ -88,7 +88,7 @@ const parseCollection = (rawSchema: RawSchema): SchemaCollection | undefined =>

const parseItems = (items?: RawSchema | RawSchema[]): Schema | Schema[] | undefined => {
if (!items) {
return undefined;
return;
}
if (Array.isArray(items)) {
return items.map(parseSchema);
Expand All @@ -108,7 +108,7 @@ const parseArray = (array?: RawSchema[]): Schema[] | undefined =>

const parseRecord = (record?: Record<string, RawSchema>): Map<string, Schema> | undefined => {
if (!record) {
return undefined;
return;
}
const parsed: Map<string, Schema> = new Map();
for (const key in record) {
Expand Down
Loading

0 comments on commit 4926f27

Please sign in to comment.