@ovotech/avro-ts
Advanced tools
Comparing version 2.0.0 to 3.0.0
@@ -6,6 +6,11 @@ import { Schema, schema } from 'avsc'; | ||
} | ||
declare type UnionRegistry = { | ||
[key: string]: string[]; | ||
}; | ||
export interface Context { | ||
recordAlias: string; | ||
namesAlias: string; | ||
namespacedPrefix: string; | ||
registry: Registry; | ||
unionRegistry: UnionRegistry; | ||
unionMember: boolean; | ||
@@ -54,4 +59,5 @@ namespace?: string; | ||
namespacedPrefix?: string; | ||
namesAlias?: string; | ||
}; | ||
export declare function avroTs(recordType: schema.RecordType, options?: AvroTsOptions): string; | ||
export {}; |
@@ -51,4 +51,12 @@ "use strict"; | ||
const namespaced = fullyQualifiedName(context, type); | ||
const prop = ts.createPropertySignature(undefined, ts.createStringLiteral(namespaced), undefined, ts.createTypeReferenceNode(type.name, undefined), undefined); | ||
const namespacedInterfaceType = ts.createInterfaceDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], `${context.namespacedPrefix}${type.name}`, undefined, undefined, [prop]); | ||
const currentNamespace = type.namespace || context.namespace; | ||
const props = [ | ||
ts.createPropertySignature(undefined, ts.createStringLiteral(namespaced), undefined, ts.createTypeReferenceNode(type.name, undefined), undefined), | ||
]; | ||
if (currentNamespace) { | ||
props.push(...(context.unionRegistry[currentNamespace] || []) | ||
.filter((name) => name !== type.name) | ||
.map(name => ts.createPropertySignature(undefined, ts.createStringLiteral(`${currentNamespace}.${name}`), ts.createToken(ts.SyntaxKind.QuestionToken), ts.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), undefined))); | ||
} | ||
const namespacedInterfaceType = ts.createInterfaceDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], `${context.namespacedPrefix}${type.name}`, undefined, undefined, props); | ||
return exports.result(exports.withEntry(recordContext, namespacedInterfaceType), ts.createTypeReferenceNode(namespacedInterfaceType.name.text, undefined)); | ||
@@ -139,2 +147,4 @@ } | ||
const isUnion = (type) => typeof type === 'object' && Array.isArray(type); | ||
const isRecordParent = (type) => typeof type === 'object' && typeof type.type === 'object'; | ||
const isUnionParent = (type) => Array.isArray(type.type); | ||
const isOptional = (type) => { | ||
@@ -153,11 +163,6 @@ if (isUnion(type)) { | ||
}; | ||
const printAstNode = (node, extras = {}) => { | ||
const printAstNode = (nodes, { importLines }) => { | ||
const resultFile = ts.createSourceFile('someFileName.ts', '', ts.ScriptTarget.Latest); | ||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); | ||
const entries = Object.values(node.context.registry); | ||
const fullSourceFile = ts.updateSourceFileNode(resultFile, entries); | ||
const importLines = extras.importLines || []; | ||
return importLines | ||
.concat(printer.printNode(ts.EmitHint.Unspecified, node.type, fullSourceFile), entries.map(entry => printer.printNode(ts.EmitHint.Unspecified, entry, fullSourceFile))) | ||
.join('\n\n'); | ||
return importLines.concat(nodes.map(n => printer.printNode(ts.EmitHint.Unspecified, n, resultFile))).join('\n\n'); | ||
}; | ||
@@ -167,10 +172,15 @@ const defaultOptions = { | ||
namespacedPrefix: 'Namespaced', | ||
namesAlias: 'Names', | ||
}; | ||
function avroTs(recordType, options = {}) { | ||
const logicalTypes = options.logicalTypes || {}; | ||
const isRootUnion = Array.isArray(recordType); | ||
const context = { | ||
...defaultOptions, | ||
...options, | ||
unionMember: Array.isArray(recordType), | ||
recordAlias: options.recordAlias || defaultOptions.recordAlias, | ||
namesAlias: options.namesAlias || defaultOptions.namesAlias, | ||
namespacedPrefix: options.namespacedPrefix || defaultOptions.namespacedPrefix, | ||
unionMember: isRootUnion, | ||
registry: {}, | ||
unionRegistry: buildUnionRegistry({}, recordType, { namespace: recordType.namespace, unionMember: isRootUnion }), | ||
namespaces: {}, | ||
@@ -186,12 +196,47 @@ visitedLogicalTypes: [], | ||
}; | ||
const nodes = convertType(context, recordType); | ||
const mainNode = convertType(context, recordType); | ||
const importLines = context.visitedLogicalTypes | ||
.map(visitedType => logicalTypes[visitedType].import) | ||
.filter(Boolean); | ||
return printAstNode({ | ||
context: nodes.context, | ||
type: ts.createTypeAliasDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], context.recordAlias, undefined, nodes.type), | ||
}, { importLines }); | ||
const nodes = [ | ||
ts.createTypeAliasDeclaration(undefined, [ts.createToken(ts.SyntaxKind.ExportKeyword)], context.recordAlias, undefined, mainNode.type), | ||
].concat(Object.values(mainNode.context.registry)); | ||
const namesNamespace = unionRegisryToNamespace(context.unionRegistry, context.namesAlias); | ||
if (namesNamespace) { | ||
nodes.unshift(namesNamespace); | ||
} | ||
return printAstNode(nodes, { importLines }); | ||
} | ||
exports.avroTs = avroTs; | ||
function unionRegisryToNamespace(registry, namespaceName) { | ||
const names = Object.keys(registry).reduce((nodes, namespace) => nodes.concat(registry[namespace].map(name => ts.createVariableStatement([ts.createToken(ts.SyntaxKind.ExportKeyword)], ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, ts.createLiteral(`${namespace}.${name}`))], ts.NodeFlags.Const)))), []); | ||
if (!names.length) { | ||
return; | ||
} | ||
const nsNode = ts.createModuleDeclaration([], [ts.createToken(ts.SyntaxKind.ExportKeyword)], ts.createIdentifier(namespaceName), ts.createModuleBlock(names), ts.NodeFlags.Namespace); | ||
return nsNode; | ||
} | ||
function buildUnionRegistry(registry, schema, context) { | ||
if (Array.isArray(schema)) { | ||
return schema.reduce((acc, schema) => buildUnionRegistry(acc, schema, context), registry); | ||
} | ||
if (isRecordParent(schema)) { | ||
return buildUnionRegistry(registry, schema.type, { ...context, unionMember: isUnionParent(schema) }); | ||
} | ||
if (isRecordType(schema)) { | ||
const { name, fields } = schema; | ||
const currentNamespace = schema.namespace || context.namespace; | ||
if (currentNamespace && context.unionMember) { | ||
(registry[currentNamespace] = registry[currentNamespace] || []).push(name); | ||
} | ||
fields.reduce((acc, field) => | ||
// @ts-ignore This works, but a bit too dynamic for TS | ||
buildUnionRegistry(acc, field, { | ||
...context, | ||
unionMember: false, | ||
namespace: schema.namespace || context.namespace, | ||
}), registry); | ||
} | ||
return registry; | ||
} | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@ovotech/avro-ts", | ||
"description": "Convert avro schemas into typescript interfaces", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"main": "dist/index.js", | ||
@@ -12,3 +12,3 @@ "source": "src/index.ts", | ||
"test-js": "jest --runInBand", | ||
"test-ts": "tsc test/integration.ts --strict --noEmit", | ||
"test-ts": "tsc test/integration.ts --strict --noEmit && ! tsc test/integration-should-fail.ts --strict --noEmit", | ||
"test": "yarn test-js && yarn test-ts", | ||
@@ -37,3 +37,3 @@ "lint-prettier": "prettier --list-different {src,test}/**/*.ts", | ||
}, | ||
"gitHead": "0b6a82e10b0cfd897d9d215d078cff399ad49fdf" | ||
"gitHead": "2a3ad08e9fb4427a9bb5f384ac66095678b7e3fe" | ||
} |
@@ -30,2 +30,3 @@ # Avro TS | ||
recordAlias: 'Record', | ||
namesAlias: 'Names', | ||
namespacedPrefix: 'Namespaced', | ||
@@ -50,2 +51,16 @@ }); | ||
## Union types helpers. | ||
When complex union types are defined, the output will include a namespace (named `Names` by default), containing the namespaced address of properties. | ||
This allows usecases as such: | ||
```typescript | ||
import { Names, WeatherEvent } from './my-type'; | ||
const event: WeatherEvent = {}; | ||
event[Names.RainEvent]; | ||
``` | ||
The union members uses [`never`](https://www.typescriptlang.org/docs/handbook/basic-types.html#never) to prevent erroneously creating/accessing a union member with mutiple keys. | ||
## Running the tests | ||
@@ -52,0 +67,0 @@ |
166
src/index.ts
@@ -8,6 +8,12 @@ import { Schema, schema } from 'avsc'; | ||
type UnionRegistry = { | ||
[key: string]: string[]; | ||
}; | ||
export interface Context { | ||
recordAlias: string; | ||
namesAlias: string; | ||
namespacedPrefix: string; | ||
registry: Registry; | ||
unionRegistry: UnionRegistry; | ||
unionMember: boolean; | ||
@@ -25,2 +31,4 @@ namespace?: string; | ||
type TypeRecordType = { type: schema.RecordType }; | ||
export type Convert<TType = Schema> = (context: Context, type: TType) => Result<any>; | ||
@@ -118,9 +126,29 @@ | ||
const namespaced = fullyQualifiedName(context, type); | ||
const prop = ts.createPropertySignature( | ||
undefined, | ||
ts.createStringLiteral(namespaced), | ||
undefined, | ||
ts.createTypeReferenceNode(type.name, undefined), | ||
undefined, | ||
); | ||
const currentNamespace = type.namespace || context.namespace; | ||
const props = [ | ||
ts.createPropertySignature( | ||
undefined, | ||
ts.createStringLiteral(namespaced), | ||
undefined, | ||
ts.createTypeReferenceNode(type.name, undefined), | ||
undefined, | ||
), | ||
]; | ||
if (currentNamespace) { | ||
props.push( | ||
...(context.unionRegistry[currentNamespace] || []) | ||
.filter((name: string) => name !== type.name) | ||
.map(name => | ||
ts.createPropertySignature( | ||
undefined, | ||
ts.createStringLiteral(`${currentNamespace}.${name}`), | ||
ts.createToken(ts.SyntaxKind.QuestionToken), | ||
ts.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword), | ||
undefined, | ||
), | ||
), | ||
); | ||
} | ||
const namespacedInterfaceType = ts.createInterfaceDeclaration( | ||
@@ -132,4 +160,5 @@ undefined, | ||
undefined, | ||
[prop], | ||
props, | ||
); | ||
return result( | ||
@@ -254,2 +283,6 @@ withEntry(recordContext, namespacedInterfaceType), | ||
const isRecordParent = (type: any): type is TypeRecordType => typeof type === 'object' && typeof type.type === 'object'; | ||
const isUnionParent = (type: any): type is { type: Array<schema.AvroSchema> } => Array.isArray(type.type); | ||
const isOptional = (type: Schema): boolean => { | ||
@@ -270,16 +303,7 @@ if (isUnion(type)) { | ||
const printAstNode = (node: Result<ts.Node>, extras: { importLines?: Array<string> } = {}): string => { | ||
const printAstNode = (nodes: Array<ts.Node>, { importLines }: { importLines: Array<string> }): string => { | ||
const resultFile = ts.createSourceFile('someFileName.ts', '', ts.ScriptTarget.Latest); | ||
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); | ||
const entries = Object.values(node.context.registry); | ||
const fullSourceFile = ts.updateSourceFileNode(resultFile, entries); | ||
const importLines = extras.importLines || []; | ||
return importLines | ||
.concat( | ||
printer.printNode(ts.EmitHint.Unspecified, node.type, fullSourceFile), | ||
entries.map(entry => printer.printNode(ts.EmitHint.Unspecified, entry, fullSourceFile)), | ||
) | ||
.join('\n\n'); | ||
return importLines.concat(nodes.map(n => printer.printNode(ts.EmitHint.Unspecified, n, resultFile))).join('\n\n'); | ||
}; | ||
@@ -294,2 +318,3 @@ | ||
namespacedPrefix?: string; | ||
namesAlias?: string; | ||
}; | ||
@@ -299,2 +324,3 @@ const defaultOptions = { | ||
namespacedPrefix: 'Namespaced', | ||
namesAlias: 'Names', | ||
}; | ||
@@ -304,7 +330,12 @@ | ||
const logicalTypes = options.logicalTypes || {}; | ||
const isRootUnion = Array.isArray(recordType); | ||
const context: Context = { | ||
...defaultOptions, | ||
...options, | ||
unionMember: Array.isArray(recordType), | ||
recordAlias: options.recordAlias || defaultOptions.recordAlias, | ||
namesAlias: options.namesAlias || defaultOptions.namesAlias, | ||
namespacedPrefix: options.namespacedPrefix || defaultOptions.namespacedPrefix, | ||
unionMember: isRootUnion, | ||
registry: {}, | ||
unionRegistry: buildUnionRegistry({}, recordType, { namespace: recordType.namespace, unionMember: isRootUnion }), | ||
namespaces: {}, | ||
@@ -321,3 +352,3 @@ visitedLogicalTypes: [], | ||
const nodes = convertType(context, recordType); | ||
const mainNode = convertType(context, recordType); | ||
@@ -328,16 +359,85 @@ const importLines = context.visitedLogicalTypes | ||
return printAstNode( | ||
{ | ||
context: nodes.context, | ||
const nodes: Array<ts.Node> = [ | ||
ts.createTypeAliasDeclaration( | ||
undefined, | ||
[ts.createToken(ts.SyntaxKind.ExportKeyword)], | ||
context.recordAlias, | ||
undefined, | ||
mainNode.type, | ||
) as ts.Node, | ||
].concat(Object.values(mainNode.context.registry)); | ||
type: ts.createTypeAliasDeclaration( | ||
undefined, | ||
[ts.createToken(ts.SyntaxKind.ExportKeyword)], | ||
context.recordAlias, | ||
undefined, | ||
nodes.type, | ||
const namesNamespace = unionRegisryToNamespace(context.unionRegistry, context.namesAlias); | ||
if (namesNamespace) { | ||
nodes.unshift(namesNamespace); | ||
} | ||
return printAstNode(nodes, { importLines }); | ||
} | ||
function unionRegisryToNamespace(registry: UnionRegistry, namespaceName: string): ts.Node | undefined { | ||
const names = Object.keys(registry).reduce<Array<ts.Statement>>( | ||
(nodes, namespace) => | ||
nodes.concat( | ||
registry[namespace].map(name => | ||
ts.createVariableStatement( | ||
[ts.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.createVariableDeclarationList( | ||
[ts.createVariableDeclaration(name, undefined, ts.createLiteral(`${namespace}.${name}`))], | ||
ts.NodeFlags.Const, | ||
), | ||
), | ||
), | ||
), | ||
}, | ||
{ importLines }, | ||
[], | ||
); | ||
if (!names.length) { | ||
return; | ||
} | ||
const nsNode = ts.createModuleDeclaration( | ||
[], | ||
[ts.createToken(ts.SyntaxKind.ExportKeyword)], | ||
ts.createIdentifier(namespaceName), | ||
ts.createModuleBlock(names), | ||
ts.NodeFlags.Namespace, | ||
); | ||
return nsNode; | ||
} | ||
function buildUnionRegistry( | ||
registry: UnionRegistry, | ||
schema: schema.AvroSchema, | ||
context: { namespace?: string; unionMember: boolean }, | ||
): UnionRegistry { | ||
if (Array.isArray(schema)) { | ||
return schema.reduce((acc, schema) => buildUnionRegistry(acc, schema, context), registry); | ||
} | ||
if (isRecordParent(schema)) { | ||
return buildUnionRegistry(registry, schema.type, { ...context, unionMember: isUnionParent(schema) }); | ||
} | ||
if (isRecordType(schema)) { | ||
const { name, fields } = schema; | ||
const currentNamespace = schema.namespace || context.namespace; | ||
if (currentNamespace && context.unionMember) { | ||
(registry[currentNamespace] = registry[currentNamespace] || []).push(name); | ||
} | ||
fields.reduce( | ||
(acc, field) => | ||
// @ts-ignore This works, but a bit too dynamic for TS | ||
buildUnionRegistry(acc, field, { | ||
...context, | ||
unionMember: false, | ||
namespace: schema.namespace || context.namespace, | ||
}), | ||
registry, | ||
); | ||
} | ||
return registry; | ||
} |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
43914
670
93