diff --git a/packages/wasm-ast-types/src/jsonschema/__snapshots__/battle-test.spec.ts.snap b/packages/wasm-ast-types/src/jsonschema/__snapshots__/battle-test.spec.ts.snap new file mode 100644 index 00000000..205c5f62 --- /dev/null +++ b/packages/wasm-ast-types/src/jsonschema/__snapshots__/battle-test.spec.ts.snap @@ -0,0 +1,173 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`minter /Users/pyramation/code/cosmwasm/ts-codegen/__fixtures__/minter/execute_msg.json 1`] = ` +"export type ExecuteMsg = { + mint: object; +} | { + set_whitelist: { + whitelist: string; + }; +} | { + update_start_time: Timestamp; +} | { + update_per_address_limit: { + per_address_limit: number; + }; +} | { + mint_to: { + recipient: string; + }; +} | { + mint_for: { + recipient: string; + token_id: number; + }; +} | { + withdraw: object; +};" +`; + +exports[`minter /Users/pyramation/code/cosmwasm/ts-codegen/__fixtures__/minter/execute_msg.json 2`] = `"export type Timestamp = Uint64;"`; + +exports[`minter /Users/pyramation/code/cosmwasm/ts-codegen/__fixtures__/minter/execute_msg.json 3`] = `"export type Uint64 = string;"`; + +exports[`minter /Users/pyramation/code/cosmwasm/ts-codegen/__fixtures__/minter/query_msg.json 1`] = ` +"export type QueryMsg = { + config: object; +} | { + mintable_num_tokens: object; +} | { + start_time: object; +} | { + mint_price: object; +} | { + mint_count: { + address: string; + }; +};" +`; + +exports[`minter /minter/execute_msg.json 1`] = ` +"export type ExecuteMsg = { + mint: object; +} | { + set_whitelist: { + whitelist: string; + }; +} | { + update_start_time: Timestamp; +} | { + update_per_address_limit: { + per_address_limit: number; + }; +} | { + mint_to: { + recipient: string; + }; +} | { + mint_for: { + recipient: string; + token_id: number; + }; +} | { + withdraw: object; +};" +`; + +exports[`minter /minter/execute_msg.json 2`] = `"export type Timestamp = Uint64;"`; + +exports[`minter /minter/execute_msg.json 3`] = `"export type Uint64 = string;"`; + +exports[`minter /minter/query_msg.json 1`] = ` +"export type QueryMsg = { + config: object; +} | { + mintable_num_tokens: object; +} | { + start_time: object; +} | { + mint_price: object; +} | { + mint_count: { + address: string; + }; +};" +`; + +exports[`minter case: 3 1`] = ` +"export type ExecuteMsg = { + mint: object; +} | { + set_whitelist: { + whitelist: string; + }; +} | { + update_start_time: Timestamp; +} | { + update_per_address_limit: { + per_address_limit: number; + }; +} | { + mint_to: { + recipient: string; + }; +} | { + mint_for: { + recipient: string; + token_id: number; + }; +} | { + withdraw: object; +};" +`; + +exports[`minter case: 3 2`] = `"export type Timestamp = Uint64;"`; + +exports[`minter case: 3 3`] = `"export type Uint64 = string;"`; + +exports[`minter case: 5 1`] = ` +"export type QueryMsg = { + config: object; +} | { + mintable_num_tokens: object; +} | { + start_time: object; +} | { + mint_price: object; +} | { + mint_count: { + address: string; + }; +};" +`; + +exports[`processTypes 1`] = ` +"export type ExecuteMsg = { + mint: object; +} | { + set_whitelist: { + whitelist: string; + }; +} | { + update_start_time: Timestamp; +} | { + update_per_address_limit: { + per_address_limit: number; + }; +} | { + mint_to: { + recipient: string; + }; +} | { + mint_for: { + recipient: string; + token_id: number; + }; +} | { + withdraw: object; +};" +`; + +exports[`processTypes 2`] = `"export type Timestamp = Uint64;"`; + +exports[`processTypes 3`] = `"export type Uint64 = string;"`; diff --git a/packages/wasm-ast-types/src/jsonschema/__snapshots__/jsonschema-types.spec.ts.snap b/packages/wasm-ast-types/src/jsonschema/__snapshots__/jsonschema-types.spec.ts.snap new file mode 100644 index 00000000..b6152537 --- /dev/null +++ b/packages/wasm-ast-types/src/jsonschema/__snapshots__/jsonschema-types.spec.ts.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`arrays 1`] = `"edges: [] | number[];"`; + +exports[`createType 1`] = ` +"export type HelloInterface = { + update_edges: { + edges: [] | ([] | number[][]); + nested: [] | ([] | [number, number][]); + supernested: [] | ([] | ([] | ([] | string[][])[])[]); + }; +};" +`; + +exports[`createType 2`] = ` +"export type HelloInterface = { + mint: object; +} | { + set_whitelist: { + whitelist: string; + }; +} | { + update_start_time: Timestamp; +} | { + update_per_address_limit: { + per_address_limit: number; + }; +} | { + mint_to: { + recipient: string; + }; +} | { + mint_for: { + recipient: string; + token_id: number; + }; +} | { + withdraw: object; +};" +`; + +exports[`createType 3`] = `"export type HelloInterface = Uint64;"`; + +exports[`min max items 1`] = `"edges: [] | [number, number];"`; + +exports[`mint 1`] = `"mint: object;"`; + +exports[`nested 1`] = `"nested: [] | ([] | [number, number][]);"`; + +exports[`processTypes 1`] = ` +"export type ExecuteMsg = { + mint: object; +} | { + set_whitelist: { + whitelist: string; + }; +} | { + update_start_time: Timestamp; +} | { + update_per_address_limit: { + per_address_limit: number; + }; +} | { + mint_to: { + recipient: string; + }; +} | { + mint_for: { + recipient: string; + token_id: number; + }; +} | { + withdraw: object; +};" +`; + +exports[`processTypes 2`] = `"export type Timestamp = Uint64;"`; + +exports[`processTypes 3`] = `"export type Uint64 = string;"`; + +exports[`set_whitelist 1`] = ` +"set_whitelist: { + whitelist: string; +};" +`; + +exports[`supernested 1`] = `"supernested: [] | ([] | ([] | ([] | string[][])[])[]);"`; + +exports[`update_edges 1`] = ` +"update_edges: { + edges: [] | ([] | number[][]); + nested: [] | ([] | [number, number][]); + supernested: [] | ([] | ([] | ([] | string[][])[])[]); +};" +`; + +exports[`whitelist 1`] = `"whitelist: string;"`; diff --git a/packages/wasm-ast-types/src/jsonschema/battle-test.spec.ts b/packages/wasm-ast-types/src/jsonschema/battle-test.spec.ts new file mode 100644 index 00000000..2a6b8a6f --- /dev/null +++ b/packages/wasm-ast-types/src/jsonschema/battle-test.spec.ts @@ -0,0 +1,43 @@ +import generate from '@babel/generator'; +import { sync as glob } from 'glob'; +import { readFileSync } from 'fs'; +import { + RenderContext, + processTypes, +} from './jsonschema-types' +import cases from 'jest-in-case'; + +const minter = glob(__dirname + '/../../../../__fixtures__/minter/*.json').map(file => { + return { name: file.split('__fixtures__')[1], content: JSON.parse(readFileSync(file, 'utf-8')) } +}); + +const expectCode = (ast) => { + expect( + generate(ast).code + ).toMatchSnapshot(); +} + +const printCode = (ast) => { + console.log( + generate(ast).code + ); +} + +const context: RenderContext = { + options: { + optionalArrays: true + } +} + +cases('minter', opts => { + processTypes( + context, + opts.content + ).map(ast => { + expectCode(ast); + }) +}, minter); + +it('processTypes', () => { + +}); \ No newline at end of file diff --git a/packages/wasm-ast-types/src/jsonschema/index.ts b/packages/wasm-ast-types/src/jsonschema/index.ts new file mode 100644 index 00000000..5efc93ec --- /dev/null +++ b/packages/wasm-ast-types/src/jsonschema/index.ts @@ -0,0 +1,4 @@ +export { + createType, + processTypes +} from './jsonschema-types' \ No newline at end of file diff --git a/packages/wasm-ast-types/src/jsonschema/jsonschema-types.spec.ts b/packages/wasm-ast-types/src/jsonschema/jsonschema-types.spec.ts new file mode 100644 index 00000000..21445a67 --- /dev/null +++ b/packages/wasm-ast-types/src/jsonschema/jsonschema-types.spec.ts @@ -0,0 +1,282 @@ +import generate from '@babel/generator'; +import execute_msg from '../../../../__fixtures__/minter/execute_msg.json'; +import arrays from '../../../../__fixtures__/arrays/schema/schema.json'; + +import { + createType, + RenderContext, + processTypes, + renderProperty, +} from './jsonschema-types' + +const expectCode = (ast) => { + expect( + generate(ast).code + ).toMatchSnapshot(); +} + +const printCode = (ast) => { + console.log( + generate(ast).code + ); +} + +const context: RenderContext = { + options: { + optionalArrays: true + } +} + +it('createType', () => { + expectCode(createType( + context, + arrays, + 'HelloInterface' + )) + expectCode(createType( + context, + execute_msg, + 'HelloInterface' + )) + expectCode(createType( + context, + { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + 'HelloInterface' + )) +}); + +it('min max items', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "edges" + ], + "properties": { + "edges": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "integer", + "format": "int32" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + 'edges' + )) +}); + +it('arrays', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "edges" + ], + "properties": { + "edges": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "integer", + "format": "int32" + } + ] + } + } + }, + 'edges' + )) +}); + +it('nested', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "nested" + ], + "properties": { + "nested": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "integer", + "format": "int32" + }, + { + "type": "integer", + "format": "int32" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + 'nested' + )) +}); + +it('supernested', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "supernested" + ], + "properties": { + "supernested": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + }, + "maxItems": 2, + "minItems": 2 + } + }, + "maxItems": 2, + "minItems": 2 + }, + "maxItems": 2, + "minItems": 2 + } + } + } + }, + 'supernested' + )) +}); + + +it('update_edges', () => { + expectCode(renderProperty( + context, + arrays.oneOf[0], + 'update_edges' + )) +}); + +it('mint', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object" + } + }, + "additionalProperties": false + }, + 'mint' + )) +}); + +it('whitelist', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "string" + } + } + }, + 'whitelist' + )) +}); + +it('set_whitelist', () => { + expectCode(renderProperty( + context, + { + "type": "object", + "required": [ + "set_whitelist" + ], + "properties": { + "set_whitelist": { + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "string" + } + } + } + }, + "additionalProperties": false + }, + 'set_whitelist' + )) +}); + + + +it('processTypes', () => { + processTypes( + context, + execute_msg + ).map(ast => { + expectCode(ast); + }) +}); \ No newline at end of file diff --git a/packages/wasm-ast-types/src/jsonschema/jsonschema-types.ts b/packages/wasm-ast-types/src/jsonschema/jsonschema-types.ts new file mode 100644 index 00000000..2a8311f7 --- /dev/null +++ b/packages/wasm-ast-types/src/jsonschema/jsonschema-types.ts @@ -0,0 +1,309 @@ +import * as t from '@babel/types'; +import { identifier, tsPropertySignature } from '../utils'; +export interface JsonSchemaObject { + $ref?: string; + type?: string; + description?: string; + required?: string[]; + properties?: object; + additionalProperties?: boolean; +} +export interface JsonSchemaDefnObject { + $schema?: string; + title?: string; + type?: string; + description?: string; + oneOf?: JsonSchemaObject[]; + anyOf?: JsonSchemaObject[]; + allOf?: JsonSchemaObject[]; + definitions?: Record; +} +export interface RenderType { + type: string; +} +export interface JsonArrayObj { + type: 'array', + items: { type: string, format: string }[] | { type: string, [k: string]: any }; + maxItems?: number; + minItems?: number; +} + +export interface RenderOptions { + optionalArrays: boolean; +} +export interface RenderContext { + definitions?: any; + options: RenderOptions; +} + +const schemaPropertyKeys = (schema: JsonSchemaObject) => { + return Object.keys(schema.properties ?? {}); +} + +export const createType = ( + context: RenderContext, + schema: JsonSchemaDefnObject, + name: string +) => { + let key = null; + if (schema.anyOf) key = 'anyOf'; + if (schema.oneOf) key = 'oneOf'; + if (schema.allOf) key = 'allOf'; + + if (!key) { + if (schema.type) { + return t.exportNamedDeclaration( + t.tsTypeAliasDeclaration( + t.identifier(name), + null, + renderType(schema) + ) + ) + } + } + const members = schema[key].map(childSchema => { + return renderSchema( + context, + childSchema + ); + }); + + if (key) { + return t.exportNamedDeclaration( + t.tsTypeAliasDeclaration( + t.identifier(name), + null, + key === 'allOf' ? + t.tsIntersectionType(members) : + t.tsUnionType(members) + ) + ); + } + + throw new Error('createType() schema') +}; + +export const additionalPropertiesUnknownType = ( + context: RenderContext, + schema: JsonSchemaObject, + prop: string +) => { + return t.tsPropertySignature( + t.identifier(prop), + t.tsTypeAnnotation( + t.tsTypeLiteral( + [ + t.tsIndexSignature( + [ + identifier( + prop, + t.tsTypeAnnotation( + t.tsStringKeyword() + ) + ) + ], + t.tsTypeAnnotation( + t.tsUnknownKeyword() + ) + ) + ] + ) + ) + ); +}; + +export const renderType = (obj: RenderType) => { + switch (obj.type) { + case 'string': + return t.tsStringKeyword(); + case 'boolean': + return t.tSBooleanKeyword(); + case 'integer': + return t.tsNumberKeyword(); + default: + throw new Error('contact maintainers [unknown type]: ' + obj.type); + } +} + +export const renderObjectType = ( + context: RenderContext, + schema: JsonSchemaObject, + name: string +) => { + const properties = schema.properties ?? {}; + if (properties[name].properties) { + const childSchema: JsonSchemaObject = properties[name]; + return t.tsTypeLiteral( + schemaPropertyKeys(childSchema) + .map(property => { + return renderProperty( + context, + childSchema, + property + ); + }) + ); + } + return t.tsObjectKeyword(); +} + +export const renderSchema = ( + context: RenderContext, + schema: JsonSchemaObject +): t.TSTypeLiteral | t.TSTypeReference => { + + if (schema.$ref) { + return getTypeFromRef(schema.$ref); + } + + return t.tsTypeLiteral(schemaPropertyKeys(schema).map(key => { + return renderProperty( + context, + schema, + key + ); + })); +} + +const renderItemsTuple = (items: { type: string }[]) => { + return t.tsTupleType(items.map(item => { + return renderType(item) + })) +}; + +const renderArrayType = ( + context: RenderContext, + schema: JsonArrayObj +) => { + if (Array.isArray(schema.items)) { + if ((schema.minItems || schema.maxItems) && schema.maxItems === schema.minItems) { + return renderItemsTuple(schema.items); + } + return t.tsArrayType( + renderType( + schema.items[0] + ) + ); + } + if (Array.isArray(schema)) { + if ((schema.minItems || schema.maxItems) && schema.maxItems === schema.minItems) { + return renderItemsTuple(schema); + } + return t.tsArrayType( + renderType( + schema[0] + ) + ); + } + + switch (schema.items.type) { + case 'array': + if (context.options.optionalArrays) { + return t.tsUnionType([ + t.tsTupleType([]), + t.tsArrayType( + renderArrayType( + context, + schema.items.items + ) + ) + ]); + } + return t.tsArrayType( + renderArrayType( + context, + schema.items.items + ) + ); + } + throw new Error('renderArrayType() contact maintainer: unknown type') +} + + +const getTypeFromRef = ($ref) => { + if ($ref?.startsWith('#/definitions/')) { + return t.tsTypeReference(t.identifier($ref.replace('#/definitions/', ''))) + } + throw new Error('what is $ref: ' + $ref); +} + + +export const renderTypeObjectProperty = ( + context: RenderContext, + schema: JsonSchemaObject, + name: string +) => { + const properties = schema.properties ?? {}; + const prop = properties[name]; + const type = prop.type; + + switch (type) { + case 'string': + return t.tsStringKeyword(); + case 'boolean': + return t.tSBooleanKeyword(); + case 'integer': + return t.tsNumberKeyword(); + case 'object': + return renderObjectType( + context, schema, name + ); + case 'array': + if (context.options.optionalArrays) { + return t.tsUnionType([ + t.tsTupleType([]), + renderArrayType(context, prop) + ]); + } + return renderArrayType(context, prop); + } + + if (prop.$ref) { + return getTypeFromRef(prop.$ref); + } + + throw new Error('renderTypeObjectProperty() contact maintainers [unknown type]: ' + type); +} + +export const renderProperty = ( + context: RenderContext, + schema: JsonSchemaObject, + prop: string +) => { + return tsPropertySignature( + t.identifier(prop), + t.tsTypeAnnotation( + renderTypeObjectProperty( + context, + schema, + prop + ) + ), + !schema.required?.includes?.(prop) + ); +}; + +export const processTypes = ( + context: RenderContext, + schema: JsonSchemaDefnObject +) => { + const definitions = schema.definitions ?? {}; + const defns = Object.keys(definitions).map(name => { + return createType( + context, + definitions[name], + name + ); + }); + + const main = createType( + context, + schema, + schema.title + ); + + return [ + main, + ...defns + ]; +}; \ No newline at end of file