diff --git a/package.json b/package.json index 9dce1346..7e80cc8e 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,13 @@ "lint": "npx eslint 'ts/**/*.{js,ts}' --fix", "test": "npm run build:jsonschema && jest", "test:cover": "npm run build:jsonschema && jest --coverage", - "build": "npm run build:jsonschema && npm run build:jsonld && npm run build:ts && npm run build:py && npm run build:r", + "build": "npm run build:jsonschema && npm run build:jsonld && npm run build:ts && npm run build:py && npm run build:r && npm run build:dist", "build:jsonschema": "ts-node ts/schema.ts", "build:jsonld": "ts-node ts/bindings/jsonld.ts", - "build:ts": "ts-node ts/bindings/typescript.ts && microbundle --tsconfig tsconfig.lib.json && cp public/*.schema.json dist && cp public/*.jsonld dist", + "build:ts": "ts-node ts/bindings/typescript.ts", "build:py": "ts-node ts/bindings/python.ts", "build:r": "ts-node ts/bindings/r.ts", + "build:dist": "microbundle --tsconfig tsconfig.lib.json && cp public/*.schema.json dist && cp public/*.jsonld dist", "docs": "npm run docs:readme && npm run docs:build && npm run docs:api", "docs:readme": "markdown-toc -i --maxdepth=4 README.md", "docs:build": "ts-node ts/docs.ts", diff --git a/schema/Array.schema.yaml b/schema/Array.schema.yaml new file mode 100644 index 00000000..1b6d8217 --- /dev/null +++ b/schema/Array.schema.yaml @@ -0,0 +1,4 @@ +title: Array +'@id': stencila:Array +description: A value comprised of several other values. +type: array diff --git a/schema/Boolean.schema.yaml b/schema/Boolean.schema.yaml new file mode 100644 index 00000000..bcd72861 --- /dev/null +++ b/schema/Boolean.schema.yaml @@ -0,0 +1,4 @@ +title: Boolean +'@id': schema:Boolean +description: A value that is either true or false +type: boolean diff --git a/schema/Null.schema.yaml b/schema/Null.schema.yaml new file mode 100644 index 00000000..84a31c1d --- /dev/null +++ b/schema/Null.schema.yaml @@ -0,0 +1,4 @@ +title: 'Null' +'@id': stencila:Null +description: The null value +type: 'null' diff --git a/schema/Number.schema.yaml b/schema/Number.schema.yaml new file mode 100644 index 00000000..81f8648a --- /dev/null +++ b/schema/Number.schema.yaml @@ -0,0 +1,4 @@ +title: Number +'@id': schema:Number +description: A value that is a number +type: number diff --git a/schema/Object.schema.yaml b/schema/Object.schema.yaml new file mode 100644 index 00000000..14e50b26 --- /dev/null +++ b/schema/Object.schema.yaml @@ -0,0 +1,4 @@ +title: Object +'@id': stencila:Object +description: A value comprised of keyed values. +type: object diff --git a/schema/Text.schema.yaml b/schema/Text.schema.yaml new file mode 100644 index 00000000..c2f173e2 --- /dev/null +++ b/schema/Text.schema.yaml @@ -0,0 +1,4 @@ +title: Text +'@id': schema:Text +description: A value comprised of a string of characters +type: string diff --git a/ts/bindings/python.ts b/ts/bindings/python.ts index ee028f80..2a7c5ea1 100644 --- a/ts/bindings/python.ts +++ b/ts/bindings/python.ts @@ -7,7 +7,7 @@ import fs from 'fs-extra' import path from 'path' import { autogeneratedHeader, - filterTypeSchemas, + filterInterfaceSchemas, filterUnionSchemas, getSchemaProperties, readSchemas, @@ -39,7 +39,7 @@ async function build(): Promise { const schemas = await readSchemas() globals = [] - const classesCode = filterTypeSchemas(schemas) + const classesCode = filterInterfaceSchemas(schemas) .map(classGenerator) .join('\n\n') const unionsCode = filterUnionSchemas(schemas) diff --git a/ts/bindings/r.ts b/ts/bindings/r.ts index 726e27be..7cab21dd 100644 --- a/ts/bindings/r.ts +++ b/ts/bindings/r.ts @@ -6,7 +6,7 @@ import fs from 'fs-extra' import path from 'path' import { autogeneratedHeader, - filterTypeSchemas, + filterInterfaceSchemas, filterUnionSchemas, getSchemaProperties, readSchemas, @@ -25,7 +25,7 @@ if (module.parent === null) build() async function build(): Promise { const schemas = await readSchemas() - const classesCode = filterTypeSchemas(schemas) + const classesCode = filterInterfaceSchemas(schemas) .map(classGenerator) .join('\n') const unionsCode = filterUnionSchemas(schemas) diff --git a/ts/bindings/typescript.test.ts b/ts/bindings/typescript.test.ts index 6f0db14c..5d1d406c 100644 --- a/ts/bindings/typescript.test.ts +++ b/ts/bindings/typescript.test.ts @@ -13,12 +13,12 @@ import * as typescript from 'typescript' import { schema, snapshot } from '../__tests__/helpers' import { generateTypeDefinitions, - typeGenerator, + interfaceGenerator, unionGenerator } from './typescript' test('generators', async () => { - expect(typeGenerator(await schema('Person.schema.json'))).toMatchFile( + expect(interfaceGenerator(await schema('Person.schema.json'))).toMatchFile( snapshot(__dirname, 'Person.ts') ) diff --git a/ts/bindings/typescript.ts b/ts/bindings/typescript.ts index 77db5e8c..98ec3134 100644 --- a/ts/bindings/typescript.ts +++ b/ts/bindings/typescript.ts @@ -8,7 +8,7 @@ import path from 'path' import prettier from 'prettier' import { autogeneratedHeader, - filterTypeSchemas, + filterInterfaceSchemas, filterUnionSchemas, getSchemaProperties, readSchemas, @@ -35,8 +35,8 @@ const prettify = async (contents: string): Promise => { export const generateTypeDefinitions = async (): Promise => { const schemas = await readSchemas() - const typesCode = filterTypeSchemas(schemas) - .map(typeGenerator) + const interfacesCode = filterInterfaceSchemas(schemas) + .map(interfaceGenerator) .join('') const unionsCode = filterUnionSchemas(schemas) @@ -57,7 +57,7 @@ const compact = (o: O): O => ${typesInterface(schemas)} -${typesCode} +${interfacesCode} ${unionsCode} ` @@ -74,14 +74,32 @@ ${unionsCode} */ export const typesInterface = (schemas: Schema[]): string => { return `export interface Types {\n${schemas - .map(({ title }) => ` ${title}: ${title}`) + .map(({ title }) => ` ${title}: ${titleToType(title ?? '')}`) .join('\n')}\n}` } +/** + * Convert the `title` of a JSON Schema to the name of a Typescript + * type, including the interfaces and unions generated below. + */ +export const titleToType = (title: string): string => { + switch (title) { + case 'Null': + case 'Boolean': + case 'Number': + case 'Object': + return title.toLowerCase() + case 'Array': + return 'Array' + default: + return title + } +} + /** * Generate a `interface` and a factory function for each type. */ -export const typeGenerator = (schema: Schema): string => { +export const interfaceGenerator = (schema: Schema): string => { const { title = 'Undefined', extends: parent, @@ -171,7 +189,7 @@ const docComment = (description?: string, tags: string[] = []): string => { } /** - * Convert a schema definition to a Typescript type + * Convert a JSON Schema definition to a Typescript type */ const schemaToType = (schema: Schema): string => { const { type, anyOf, allOf, $ref } = schema @@ -193,7 +211,7 @@ const schemaToType = (schema: Schema): string => { } /** - * Convert a schema `$ref` (reference) to a Typescript type + * Convert a JSON Schema `$ref` (reference) to a Typescript type * * Assume that any `$ref`s refer to a type defined in the file. */ @@ -202,7 +220,7 @@ const $refToType = ($ref: string): string => { } /** - * Convert a schema with the `anyOf` property to a Typescript `Union` type. + * Convert a JSON Schema with the `anyOf` property to a Typescript `Union` type. */ const anyOfToType = (anyOf: Schema[]): string => { const types = anyOf @@ -217,7 +235,7 @@ const anyOfToType = (anyOf: Schema[]): string => { } /** - * Convert a schema with the `allOf` property to a Typescript type. + * Convert a JSON Schema with the `allOf` property to a Typescript type. * * If the `allOf` is singular then just use that (this usually arises * because the `allOf` is used for a property with a `$ref`). Otherwise, @@ -231,7 +249,7 @@ const allOfToType = (allOf: Schema[]): string => { } /** - * Convert a schema with the `array` property to a Typescript `Array` type. + * Convert a JSON Schema with the `array` property to a Typescript `Array` type. * * Uses the more explicity `Array<>` syntax over the shorter`[]` syntax * because the latter necessitates the use of, sometime superfluous, parentheses. @@ -246,7 +264,7 @@ const arrayToType = (schema: Schema): string => { } /** - * Convert a schema with the `enum` property to Typescript "or values". + * Convert a JSON Schema with the `enum` property to Typescript "or values". */ export const enumToType = (enu: (string | number)[]): string => { return enu @@ -305,8 +323,9 @@ export const generateTypeMaps = async (): Promise => { } /** Generate Type Definitions and Type Maps files */ -export const build = async (): Promise => { - return generateTypeDefinitions().then(generateTypeMaps) +export const build = async (): Promise => { + await generateTypeDefinitions() + await generateTypeMaps() } /** diff --git a/ts/helpers.ts b/ts/helpers.ts index 9de9f9cb..7a68b006 100644 --- a/ts/helpers.ts +++ b/ts/helpers.ts @@ -34,13 +34,27 @@ export async function readSchemas( } /** - * Get the 'type' schemas (i.e. not union schema, not property schemas) which are - * usually translated into classes or similar for the language. + * Is a schema for a primitive type. + */ +export function isPrimitiveSchema(schema: Schema): boolean { + return schema.properties === undefined && schema.anyOf === undefined +} + +/** + * Get the 'primitive' schemas + */ +export function filterPrimitiveSchemas(schemas: Schema[]): Schema[] { + return schemas.filter(isPrimitiveSchema) +} + +/** + * Get the 'interface' schemas (i.e. not union schema, not property schemas) which are + * usually translated into `interface`s, `class`es or similar for the language. * * Types are sorted topologically so that schemas come before * any of their descendants. */ -export function filterTypeSchemas(schemas: Schema[]): Schema[] { +export function filterInterfaceSchemas(schemas: Schema[]): Schema[] { const types = schemas.filter(schema => schema.properties !== undefined) const map = new Map(schemas.map(schema => [schema.title, schema])) diff --git a/ts/util/node-type.test.ts b/ts/util/node-type.test.ts index fe2db6a9..abcb60f4 100644 --- a/ts/util/node-type.test.ts +++ b/ts/util/node-type.test.ts @@ -1,13 +1,13 @@ import { nodeType } from './node-type' test('nodeType', () => { - expect(nodeType(null)).toBe('null') - expect(nodeType(true)).toBe('boolean') - expect(nodeType(false)).toBe('boolean') - expect(nodeType(42)).toBe('number') - expect(nodeType(3.14)).toBe('number') - expect(nodeType('str')).toBe('string') - expect(nodeType([])).toBe('array') - expect(nodeType({})).toBe('object') + expect(nodeType(null)).toBe('Null') + expect(nodeType(true)).toBe('Boolean') + expect(nodeType(false)).toBe('Boolean') + expect(nodeType(42)).toBe('Number') + expect(nodeType(3.14)).toBe('Number') + expect(nodeType('str')).toBe('Text') + expect(nodeType([])).toBe('Array') + expect(nodeType({})).toBe('Object') expect(nodeType({ type: 'Person' })).toBe('Person') }) diff --git a/ts/util/node-type.ts b/ts/util/node-type.ts index 84949fd8..79acd034 100644 --- a/ts/util/node-type.ts +++ b/ts/util/node-type.ts @@ -1,16 +1,17 @@ -import { Node } from '../types' +import { Types, Node } from '../types' import { isEntity } from './guards' /** * Get the type of a node + * * @param {Node} node The schema node to get the type for */ -export const nodeType = (node: Node): string => { - if (node === null) return 'null' - if (typeof node === 'boolean') return 'boolean' - if (typeof node === 'number') return 'number' - if (typeof node === 'string') return 'string' - if (Array.isArray(node)) return 'array' +export const nodeType = (node: Node): keyof Types => { + if (node === null) return 'Null' + if (typeof node === 'boolean') return 'Boolean' + if (typeof node === 'number') return 'Number' + if (typeof node === 'string') return 'Text' + if (Array.isArray(node)) return 'Array' if (isEntity(node)) return node.type - return typeof node + return 'Object' }