diff --git a/packages/avro-ts/src/helpers.ts b/packages/avro-ts/src/helpers.ts index 0135893..438096c 100644 --- a/packages/avro-ts/src/helpers.ts +++ b/packages/avro-ts/src/helpers.ts @@ -6,7 +6,20 @@ import { Context } from './types'; export const fullName = ( context: Context, schema: avroSchema.RecordType | avroSchema.EnumType, -): string => `${schema.namespace ?? context.namespace}.${schema.name}`; +): string => { + const isFullName = schema.name.includes('.'); + const namespace = schema.namespace ?? context.namespace; + + // According to the avro specs, if the namespace is included in the full name + // the schema namespace should be ignored. + // We can also specify a "null" namespace and use simple names everywhere. + // https://avro.apache.org/docs/1.11.1/specification/#names + if (isFullName || !namespace) { + return schema.name; + } + + return `${namespace}.${schema.name}`; +}; export const firstUpperCase = (name: string): string => name ? name[0].toUpperCase() + name.slice(1) : name; diff --git a/packages/avro-ts/test/__snapshots__/namespacing.spec.ts.snap b/packages/avro-ts/test/__snapshots__/namespacing.spec.ts.snap new file mode 100644 index 0000000..35f1bac --- /dev/null +++ b/packages/avro-ts/test/__snapshots__/namespacing.spec.ts.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Avro simple and full name related tests Should properly generate union type names when inheriting a namespace 1`] = ` +RootRecord { + "unionField": Branch$ { + "RootNamespace.SomeRecordA": SomeRecordA { + "someRecordAField": "test", + }, + }, +} +`; + +exports[`Avro simple and full name related tests Should properly generate union type names when inheriting a namespace 2`] = ` +"/* eslint-disable @typescript-eslint/no-namespace */ + +export type RootRecord = RootNamespace.RootRecord; + +export namespace RootNamespace { + export const SomeRecordASchema = \\"{\\\\\\"type\\\\\\":\\\\\\"record\\\\\\",\\\\\\"name\\\\\\":\\\\\\"SomeRecordA\\\\\\",\\\\\\"fields\\\\\\":[{\\\\\\"name\\\\\\":\\\\\\"someRecordAField\\\\\\",\\\\\\"type\\\\\\":\\\\\\"string\\\\\\"}]}\\"; + export const SomeRecordAName = \\"RootNamespace.SomeRecordA\\"; + export interface SomeRecordA { + someRecordAField: string; + } + export const SomeRecordBSchema = \\"{\\\\\\"type\\\\\\":\\\\\\"record\\\\\\",\\\\\\"name\\\\\\":\\\\\\"SomeRecordB\\\\\\",\\\\\\"fields\\\\\\":[{\\\\\\"name\\\\\\":\\\\\\"someRecordBField\\\\\\",\\\\\\"type\\\\\\":\\\\\\"string\\\\\\"}]}\\"; + export const SomeRecordBName = \\"RootNamespace.SomeRecordB\\"; + export interface SomeRecordB { + someRecordBField: string; + } + export const RootRecordSchema = \\"{\\\\\\"type\\\\\\":\\\\\\"record\\\\\\",\\\\\\"name\\\\\\":\\\\\\"RootRecord\\\\\\",\\\\\\"namespace\\\\\\":\\\\\\"RootNamespace\\\\\\",\\\\\\"fields\\\\\\":[{\\\\\\"name\\\\\\":\\\\\\"unionField\\\\\\",\\\\\\"type\\\\\\":[{\\\\\\"type\\\\\\":\\\\\\"record\\\\\\",\\\\\\"name\\\\\\":\\\\\\"SomeRecordA\\\\\\",\\\\\\"fields\\\\\\":[{\\\\\\"name\\\\\\":\\\\\\"someRecordAField\\\\\\",\\\\\\"type\\\\\\":\\\\\\"string\\\\\\"}]},{\\\\\\"type\\\\\\":\\\\\\"record\\\\\\",\\\\\\"name\\\\\\":\\\\\\"SomeRecordB\\\\\\",\\\\\\"fields\\\\\\":[{\\\\\\"name\\\\\\":\\\\\\"someRecordBField\\\\\\",\\\\\\"type\\\\\\":\\\\\\"string\\\\\\"}]}]}]}\\"; + export const RootRecordName = \\"RootNamespace.RootRecord\\"; + export interface RootRecord { + unionField: { + \\"RootNamespace.SomeRecordA\\": RootNamespace.SomeRecordA; + \\"RootNamespace.SomeRecordB\\"?: never; + } | { + \\"RootNamespace.SomeRecordA\\"?: never; + \\"RootNamespace.SomeRecordB\\": RootNamespace.SomeRecordB; + }; + } +} +" +`; + +exports[`Avro simple and full name related tests Should properly generate union type names when specifying a full name 1`] = ` +RootRecord { + "unionField": Branch$ { + "RecordANamespace.SomeRecordA": SomeRecordA { + "someRecordAField": "test", + }, + }, +} +`; + +exports[`Avro simple and full name related tests Should properly generate union type names when specifying a full name 2`] = ` +"export type AvroType = RootRecord; + +export interface RecordANamespaceSomeRecordA { + someRecordAField: string; +} + +export interface RecordBNamespaceSomeRecordB { + someRecordBField: string; +} + +export interface RootRecord { + unionField: { + \\"RecordANamespace.SomeRecordA\\": RecordANamespaceSomeRecordA; + \\"RecordBNamespace.SomeRecordB\\"?: never; + } | { + \\"RecordANamespace.SomeRecordA\\"?: never; + \\"RecordBNamespace.SomeRecordB\\": RecordBNamespaceSomeRecordB; + }; +} +" +`; + +exports[`Avro simple and full name related tests Should properly generate union type names when there are no namespaces 1`] = ` +RootRecord { + "unionField": Branch$ { + "SomeRecordA": SomeRecordA { + "someRecordAField": "test", + }, + }, +} +`; + +exports[`Avro simple and full name related tests Should properly generate union type names when there are no namespaces 2`] = ` +"export type AvroType = RootRecord; + +export interface SomeRecordA { + someRecordAField: string; +} + +export interface SomeRecordB { + someRecordBField: string; +} + +export interface RootRecord { + unionField: { + SomeRecordA: SomeRecordA; + SomeRecordB?: never; + } | { + SomeRecordA?: never; + SomeRecordB: SomeRecordB; + }; +} +" +`; diff --git a/packages/avro-ts/test/namespacing.spec.ts b/packages/avro-ts/test/namespacing.spec.ts new file mode 100644 index 0000000..2abbe44 --- /dev/null +++ b/packages/avro-ts/test/namespacing.spec.ts @@ -0,0 +1,147 @@ +import { schema, Type } from 'avsc'; +import { toTypeScript } from '../src'; + +describe('Avro simple and full name related tests', () => { + it.each<[string, { schema: schema.RecordType; testObject: unknown }]>([ + [ + 'there are no namespaces', + { + schema: { + type: 'record', + name: 'RootRecord', + fields: [ + { + name: 'unionField', + type: [ + { + type: 'record', + name: 'SomeRecordA', + fields: [ + { + name: 'someRecordAField', + type: 'string', + }, + ], + }, + { + type: 'record', + name: 'SomeRecordB', + fields: [ + { + name: 'someRecordBField', + type: 'string', + }, + ], + }, + ], + }, + ], + } as schema.RecordType, + testObject: { + unionField: { + SomeRecordA: { + someRecordAField: 'test', + }, + }, + }, + }, + ], + [ + 'inheriting a namespace', + { + schema: { + type: 'record', + name: 'RootRecord', + namespace: 'RootNamespace', + fields: [ + { + name: 'unionField', + type: [ + { + type: 'record', + name: 'SomeRecordA', + fields: [ + { + name: 'someRecordAField', + type: 'string', + }, + ], + }, + { + type: 'record', + name: 'SomeRecordB', + fields: [ + { + name: 'someRecordBField', + type: 'string', + }, + ], + }, + ], + }, + ], + }, + testObject: { + unionField: { + ['RootNamespace.SomeRecordA']: { + someRecordAField: 'test', + }, + }, + }, + }, + ], + [ + 'specifying a full name', + { + schema: { + type: 'record', + name: 'RootRecord', + fields: [ + { + name: 'unionField', + type: [ + { + type: 'record', + name: 'RecordANamespace.SomeRecordA', + fields: [ + { + name: 'someRecordAField', + type: 'string', + }, + ], + }, + { + type: 'record', + name: 'RecordBNamespace.SomeRecordB', + fields: [ + { + name: 'someRecordBField', + type: 'string', + }, + ], + }, + ], + }, + ], + }, + testObject: { + unionField: { + ['RecordANamespace.SomeRecordA']: { + someRecordAField: 'test', + }, + }, + }, + }, + ], + ])('Should properly generate union type names when %s', (description, { schema, testObject }) => { + const tsCode = toTypeScript(schema); + + const type = Type.forSchema(schema); + const result = type.fromBuffer(type.toBuffer(testObject)); + + // Adding the actual decoding result to visually make sure the types actually match the + // avro decoder behavior. + expect(result).toMatchSnapshot(); + expect(tsCode).toMatchSnapshot(); + }); +});