Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Avro Schema input processor #1753

Merged
merged 20 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/models/AvroSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Avro Schema model
*/

// clear unknown to make code more robust

export interface AvroField {
name?: string;
doc?: string;
type?: unknown;
default?: unknown;
}

export class AvroSchema {
type?: string | string[] | object;
name?: string;
namespace?: string;
doc?: string;
aliases?: string[];
symbols?: string[];
items?: unknown;
values?: unknown;
fields?: AvroField[];
order?: 'ascending' | 'descending' | 'ignore';
size?: number;
example?: string | number;
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
default?: unknown;
exclusiveMinimum?: unknown;
exclusiveMaximum?: unknown;
logicalType?: unknown;

/**
* Takes a deep copy of the input object and converts it to an instance of AvroSchema.
*
* @param object
*/
static toSchema(object: Record<string, unknown>): AvroSchema {
const convertedSchema = AvroSchema.internalToSchema(object);
if (convertedSchema instanceof AvroSchema) {
return convertedSchema;
}
throw new Error('Could not convert input to expected copy of AvroSchema');
}

private static internalToSchema(
object: any,
seenSchemas: Map<any, AvroSchema> = new Map()
): any {
// if primitive types return as is
if (null === object || 'object' !== typeof object) {
return object;
}

if (seenSchemas.has(object)) {
return seenSchemas.get(object) as any;
}

if (object instanceof Array) {
const copy: any = [];
for (let i = 0, len = object.length; i < len; i++) {
copy[Number(i)] = AvroSchema.internalToSchema(
object[Number(i)],
seenSchemas
);
}
return copy;
}
//Nothing else left then to create an object
const schema = new AvroSchema();
seenSchemas.set(object, schema);
for (const [propName, prop] of Object.entries(object)) {
let copyProp = prop;

// Ignore value properties (those with `any` type) as they should be saved as is regardless of value
if (propName !== 'default' && propName !== 'enum') {
copyProp = AvroSchema.internalToSchema(prop, seenSchemas);
}
(schema as any)[String(propName)] = copyProp;
}
return schema;
}
jonaslagoni marked this conversation as resolved.
Show resolved Hide resolved
}
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './OpenapiV3Schema';
export * from './MetaModel';
export * from './ConstrainedMetaModel';
export * from './InputMetaModel';
export * from './AvroSchema';
45 changes: 45 additions & 0 deletions src/processors/AvroSchemaInputProcessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AbstractInputProcessor } from './AbstractInputProcessor';
import { InputMetaModel } from '../models';
import { Logger } from '../utils';
import { convertToMetaModel } from '../helpers';

/**
* Class for processing Avro Schema input
*/
export class AvroSchemaInputProcessor extends AbstractInputProcessor {
/**
* Function processing an Avro Schema input
*
* @param input
*/

jonaslagoni marked this conversation as resolved.
Show resolved Hide resolved
shouldProcess(input?: any): boolean {
if (
input === '' ||
JSON.stringify(input) === '{}' ||
JSON.stringify(input) === '[]'
) {
return false;
}
if (!input.type) {
return false;
}
return true;
}
jonaslagoni marked this conversation as resolved.
Show resolved Hide resolved

process(input?: any): Promise<InputMetaModel> {
if (!this.shouldProcess(input)) {
return Promise.reject(
new Error('Input is not an Avro Schema, so it cannot be processed.')
);
}
Logger.debug('Processing input as Avro Schema document');
const inputModel = new InputMetaModel();
inputModel.originalInput = input;
const metaModel = convertToMetaModel(input);
jonaslagoni marked this conversation as resolved.
Show resolved Hide resolved
inputModel.models[metaModel.name] = metaModel;
Logger.debug('Completed processing input as Avro Schema document');

return Promise.resolve(inputModel);
}
}
2 changes: 2 additions & 0 deletions src/processors/InputProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProcessorOptions, InputMetaModel } from '../models';
import { SwaggerInputProcessor } from './SwaggerInputProcessor';
import { OpenAPIInputProcessor } from './OpenAPIInputProcessor';
import { TypeScriptInputProcessor } from './TypeScriptInputProcessor';
import { AvroSchemaInputProcessor } from './AvroSchemaInputProcessor';

/**
* Main input processor which figures out the type of input it receives and delegates the processing into separate individual processors.
Expand All @@ -19,6 +20,7 @@ export class InputProcessor {
this.setProcessor('openapi', new OpenAPIInputProcessor());
this.setProcessor('default', new JsonSchemaInputProcessor());
this.setProcessor('typescript', new TypeScriptInputProcessor());
this.setProcessor('avroSchema', new AvroSchemaInputProcessor());
jonaslagoni marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/processors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './JsonSchemaInputProcessor';
export * from './SwaggerInputProcessor';
export * from './OpenAPIInputProcessor';
export * from './TypeScriptInputProcessor';
export * from './AvroSchemaInputProcessor';
49 changes: 49 additions & 0 deletions test/processors/AvroSchemaInputProcessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { InputMetaModel } from '../../src/models';
import { AvroSchemaInputProcessor } from '../../src/processors';

describe('AvroSchemaInputProcessor', () => {
describe('shouldProcess()', () => {
test('should fail correctly for empty string input', () => {
const processor = new AvroSchemaInputProcessor();
expect(processor.shouldProcess('')).toBeFalsy();
});

test('should fail correctly for empty object input', () => {
const processor = new AvroSchemaInputProcessor();
expect(processor.shouldProcess({})).toBeFalsy();
});

test('should fail correctly for empty array input', () => {
const processor = new AvroSchemaInputProcessor();
expect(processor.shouldProcess([])).toBeFalsy();
});

test('should fail if input has no type property', () => {
const processor = new AvroSchemaInputProcessor();
expect(processor.shouldProcess({ someKey: 'someValue' })).toBeFalsy();
});

test('should return true for valid input', () => {
const processor = new AvroSchemaInputProcessor();
const result = processor.shouldProcess({ type: 'someType' });
expect(result).toBeTruthy();
});
});

describe('process()', () => {
test('should throw error when trying to process wrong schema', async () => {
const processor = new AvroSchemaInputProcessor();
// const invalidInput = { someKey: 'someValue' };
await expect(processor.process({ someKey: 'someValue' })).rejects.toThrow(
'Input is not an Avro Schema, so it cannot be processed.'
);
});

test('should process Avro Schema', async () => {
const processor = new AvroSchemaInputProcessor();
// const validInput = { type: 'someType' };
const result = await processor.process({ type: 'someType' });
expect(result).toBeInstanceOf(InputMetaModel);
});
});
});