-
Notifications
You must be signed in to change notification settings - Fork 32
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
Feature: Extension loader #25
Changes from 1 commit
743ef6b
5c6061b
73e5ab9
b2df344
628fc8d
ad5c7d0
4b8f34b
80df5d9
77a8ac7
50c420b
da7f20d
1605c74
528bc59
4da91ce
b36c62f
b2cfd97
9bad6f1
a8c80a6
4b905db
1ef7c5d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- expose spec generator and schema parser. - add project level option - add document generator
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import 'reflect-metadata'; | ||
export * from './types'; | ||
export * from './container'; | ||
export * from './modules'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { IDocumentGeneratorOptions } from '@vulcan-sql/build/models'; | ||
import { AsyncContainerModule } from 'inversify'; | ||
import { DocumentGenerator } from '../../lib/document-generator'; | ||
import { DocumentGeneratorOptions } from '../../options/documentGenerator'; | ||
import { TYPES } from '../types'; | ||
|
||
export const documentGeneratorModule = (options?: IDocumentGeneratorOptions) => | ||
new AsyncContainerModule(async (bind) => { | ||
// Options | ||
bind<IDocumentGeneratorOptions>( | ||
TYPES.DocumentGeneratorInputOptions | ||
).toConstantValue(options || ({} as any)); | ||
bind(TYPES.DocumentGeneratorOptions) | ||
.to(DocumentGeneratorOptions) | ||
.inSingletonScope(); | ||
|
||
// Document generator | ||
bind(TYPES.DocumentGenerator).to(DocumentGenerator); | ||
bind(TYPES.Factory_SpecGenerator).toAutoNamedFactory( | ||
TYPES.Extension_SpecGenerator | ||
); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { builtInSchemaReader } from '@vulcan-sql/build/schema-parser'; | ||
import { ExtensionLoader } from '@vulcan-sql/core'; | ||
import { AsyncContainerModule } from 'inversify'; | ||
import { builtInSpecGenerator } from '../../lib/document-generator'; | ||
import { IBuildOptions } from '../../models/buildOptions'; | ||
|
||
export const extensionModule = (options: IBuildOptions) => | ||
new AsyncContainerModule(async (bind) => { | ||
const loader = new ExtensionLoader(options); | ||
// Internal extension modules | ||
|
||
// Schema reader | ||
loader.loadInternalExtensionModule(builtInSchemaReader); | ||
|
||
// Spec generator | ||
loader.loadInternalExtensionModule(builtInSpecGenerator); | ||
|
||
loader.bindExtensions(bind); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
export * from './schemaParser'; | ||
export * from './extension'; | ||
export * from './documentGenerator'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,16 @@ | ||
export const TYPES = { | ||
// Schema | ||
SchemaParserInputOptions: Symbol.for('SchemaParserInputOptions'), | ||
SchemaParserOptions: Symbol.for('SchemaParserOptions'), | ||
SchemaReader: Symbol.for('SchemaReader'), | ||
Factory_SchemaReader: Symbol.for('Factory_SchemaReader'), | ||
SchemaParser: Symbol.for('SchemaParser'), | ||
SchemaParserMiddleware: Symbol.for('SchemaParserMiddleware'), | ||
// Document | ||
DocumentGenerator: Symbol.for('DocumentGenerator'), | ||
DocumentGeneratorInputOptions: Symbol.for('DocumentGeneratorInputOptions'), | ||
DocumentGeneratorOptions: Symbol.for('DocumentGeneratorOptions'), | ||
Factory_SpecGenerator: Symbol.for('Factory_SpecGenerator'), | ||
// Extension | ||
Extension_SchemaReader: Symbol.for('Extension_SchemaReader'), | ||
Extension_SpecGenerator: Symbol.for('Extension_SpecGenerator'), | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { APISchema } from '@vulcan-sql/core'; | ||
import { inject, injectable, interfaces } from 'inversify'; | ||
import { TYPES } from '../../containers/types'; | ||
import { SpecGenerator } from '../../models/extensions'; | ||
import { DocumentGeneratorOptions } from '../../options/documentGenerator'; | ||
import * as jsYAML from 'js-yaml'; | ||
import * as path from 'path'; | ||
import { promises as fs } from 'fs'; | ||
|
||
@injectable() | ||
export class DocumentGenerator { | ||
private specGenerators: SpecGenerator[]; | ||
private folderPath: string; | ||
|
||
constructor( | ||
@inject(TYPES.Factory_SpecGenerator) | ||
specGeneratorFactory: interfaces.AutoNamedFactory<SpecGenerator>, | ||
@inject(TYPES.DocumentGeneratorOptions) options: DocumentGeneratorOptions | ||
) { | ||
this.specGenerators = []; | ||
for (const spec of options.specs) { | ||
this.specGenerators.push(specGeneratorFactory(spec)); | ||
} | ||
this.folderPath = options.folderPath; | ||
} | ||
|
||
public async generateDocuments(schemas: APISchema[]) { | ||
for (const generator of this.specGenerators) { | ||
const spec = generator.getSpec(schemas); | ||
const filePath = path.resolve( | ||
this.folderPath, | ||
`spec-${generator.getExtensionId()}.yaml` | ||
); | ||
await fs.writeFile(filePath, jsYAML.dump(spec), 'utf-8'); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './spec-generator'; | ||
export * from './documentGenerator'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { OAS3SpecGenerator } from './oas3'; | ||
export const builtInSpecGenerator = [OAS3SpecGenerator]; | ||
|
||
export * from './oas3'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
export * from './schemaReader'; | ||
import { FileSchemaReader } from './fileSchemaReader'; | ||
export const builtInSchemaReader = [FileSchemaReader]; | ||
|
||
export * from './fileSchemaReader'; |
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,8 @@ | ||
import { ICoreOptions } from '@vulcan-sql/core'; | ||
import { IDocumentGeneratorOptions } from './documentGeneratorOptions'; | ||
import { ISchemaParserOptions } from './schemaParserOptions'; | ||
|
||
export interface IBuildOptions extends ICoreOptions { | ||
name?: string; | ||
description?: string; | ||
version?: string; | ||
schemaParser: ISchemaParserOptions; | ||
'schema-parser'?: ISchemaParserOptions; | ||
'document-generator'?: IDocumentGeneratorOptions; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export interface IDocumentGeneratorOptions { | ||
specs?: string[]; | ||
folderPath: string; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './schemaReader'; | ||
export * from './specGenerator'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { ExtensionBase, VulcanExtension } from '@vulcan-sql/core'; | ||
import { TYPES } from '../../containers/types'; | ||
|
||
export enum SchemaFormat { | ||
YAML = 'YAML', | ||
} | ||
|
||
export interface SchemaData { | ||
/** The identifier of this schema, we might use this name to mapping SQL sources. */ | ||
sourceName: string; | ||
content: string; | ||
type: SchemaFormat; | ||
} | ||
|
||
@VulcanExtension(TYPES.Extension_SchemaReader, { enforcedId: true }) | ||
export abstract class SchemaReader<C = any> extends ExtensionBase<C> { | ||
abstract readSchema(): AsyncGenerator<SchemaData>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { | ||
APISchema, | ||
ExtensionBase, | ||
ProjectOptions, | ||
TYPES as CORE_TYPES, | ||
VulcanExtension, | ||
} from '@vulcan-sql/core'; | ||
import { inject } from 'inversify'; | ||
import { TYPES } from '../../containers/types'; | ||
|
||
@VulcanExtension(TYPES.Extension_SpecGenerator, { enforcedId: true }) | ||
export abstract class SpecGenerator<T = any, C = any> extends ExtensionBase<C> { | ||
abstract getSpec(schemas: APISchema[]): T; | ||
|
||
private projectOption: ProjectOptions; | ||
|
||
constructor( | ||
@inject(CORE_TYPES.ProjectOptions) projectOption: ProjectOptions, | ||
@inject(CORE_TYPES.ExtensionName) moduleName: string, | ||
@inject(CORE_TYPES.ExtensionConfig) config: C | ||
) { | ||
super(config, moduleName); | ||
this.projectOption = projectOption; | ||
} | ||
|
||
protected getName() { | ||
return this.projectOption.name || 'API Server'; | ||
} | ||
|
||
protected getDescription() { | ||
return this.projectOption.description; | ||
} | ||
|
||
protected getVersion() { | ||
return this.projectOption.version || '0.0.1'; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
export * from './buildOptions'; | ||
export * from './schemaParserOptions'; | ||
export * from './extensions'; | ||
export * from './documentGeneratorOptions'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,4 @@ | ||
export interface ISchemaParserOptions { | ||
reader: SchemaReaderType; | ||
reader: string; | ||
folderPath: string; | ||
} | ||
|
||
export enum SchemaReaderType { | ||
LocalFile = 'LocalFile', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { injectable, inject, optional } from 'inversify'; | ||
import { TYPES } from '@vulcan-sql/build/containers'; | ||
import { IDocumentGeneratorOptions } from '@vulcan-sql/build/models'; | ||
import { IsOptional, IsArray, validateSync, IsString } from 'class-validator'; | ||
|
||
@injectable() | ||
export class DocumentGeneratorOptions implements IDocumentGeneratorOptions { | ||
@IsArray() | ||
@IsOptional() | ||
public readonly specs = ['oas3']; | ||
|
||
@IsString() | ||
public readonly folderPath!: string; | ||
|
||
constructor( | ||
@inject(TYPES.DocumentGeneratorInputOptions) | ||
@optional() | ||
options: Partial<IDocumentGeneratorOptions> = {} | ||
) { | ||
Object.assign(this, options); | ||
const errors = validateSync(this); | ||
if (errors.length > 0) { | ||
throw new Error( | ||
'Invalid document generator options: ' + errors.join(', ') | ||
); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from './schemaParser'; | ||
export * from './documentGenerator'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
result.json | ||
result.json | ||
spec-*.yaml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,19 @@ | ||
import { VulcanBuilder } from '../../src'; | ||
import * as path from 'path'; | ||
import { IBuildOptions, SchemaReaderType } from '@vulcan-sql/build/models'; | ||
import { IBuildOptions } from '@vulcan-sql/build/models'; | ||
|
||
it('Builder.build should work', async () => { | ||
// Arrange | ||
const builder = new VulcanBuilder(); | ||
const options: IBuildOptions = { | ||
schemaParser: { | ||
reader: SchemaReaderType.LocalFile, | ||
'schema-parser': { | ||
reader: 'LocalFile', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think maybe you could still keep There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason I removed the enum for these properties is to avoid casting when using customer extension like below: const options: IBuildOptions = {
'schema-parser': {
reader: 's3' as SchemaReaderType,
},
} For avoiding typos in our code, I'll add these enums back, but I prefer to keep the interface the same (string type), so these enums only protect our internal code. I'll put these enums into This is the final result: const options: IBuildOptions = {
'schema-parser': {
reader: SchemaReaderType.LocalFile,
folderPath: path.resolve(__dirname, 'source'),
},
'document-generator': {
specs: [DocumentGeneratorSpec.oas3],
folderPath: path.resolve(__dirname),
},
artifact: {
provider: ArtifactBuilderProviderType.LocalFile,
serializer: ArtifactBuilderSerializerType.JSON,
filePath: path.resolve(__dirname, 'result.json'),
},
template: {
provider: TemplateProviderType.LocalFile,
folderPath: path.resolve(__dirname, 'source'),
},
extensions: {},
}; |
||
folderPath: path.resolve(__dirname, 'source'), | ||
}, | ||
'document-generator': { | ||
specs: ['oas3'], | ||
folderPath: path.resolve(__dirname), | ||
}, | ||
artifact: { | ||
provider: 'LocalFile', | ||
serializer: 'JSON', | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
spec-*.yaml |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { SpecGenerator } from '@vulcan-sql/build'; | ||
import { DocumentGenerator } from '@vulcan-sql/build/doc-generator'; | ||
import * as sinon from 'ts-sinon'; | ||
import * as path from 'path'; | ||
import { promises as fs } from 'fs'; | ||
import faker from '@faker-js/faker'; | ||
|
||
it('Document generator should write YAML files while generating documents', async () => { | ||
// Arrange | ||
const mockSpec = { someSpec: faker.datatype.number() }; | ||
const documentGenerator = new DocumentGenerator( | ||
(id: string) => { | ||
const mockSpecGenerator = sinon.stubInterface<SpecGenerator>(); | ||
mockSpecGenerator.getSpec.returns(mockSpec); | ||
mockSpecGenerator.getExtensionId.returns(id); | ||
return mockSpecGenerator; | ||
}, | ||
{ | ||
specs: ['spec1', 'spec2'], | ||
folderPath: __dirname, | ||
} | ||
); | ||
|
||
// Act | ||
await documentGenerator.generateDocuments([]); | ||
|
||
// Arrange | ||
expect( | ||
await fs.readFile(path.resolve(__dirname, 'spec-spec1.yaml'), 'utf-8') | ||
).toEqual(`someSpec: ${mockSpec.someSpec}\n`); | ||
expect( | ||
await fs.readFile(path.resolve(__dirname, 'spec-spec2.yaml'), 'utf-8') | ||
).toEqual(`someSpec: ${mockSpec.someSpec}\n`); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,9 @@ import { ITemplateEngineOptions } from './templateEngineOptions'; | |
export type ExtensionAliases = Record<string, string | string[]>; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest adding a comment to provide the example and explain the record key and value represented. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure, I've add comments about module gorups. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks alot ! |
||
|
||
export interface ICoreOptions { | ||
name?: string; | ||
description?: string; | ||
version?: string; | ||
artifact: IArtifactBuilderOptions; | ||
template?: ITemplateEngineOptions; | ||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './artifactBuilder'; | ||
export * from './templateEngine'; | ||
export * from './project'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { IsOptional, IsString, validateSync } from 'class-validator'; | ||
import { inject, injectable, optional } from 'inversify'; | ||
import { TYPES } from '../containers'; | ||
import { ICoreOptions } from '../models'; | ||
|
||
/** Root level options */ | ||
@injectable() | ||
export class ProjectOptions implements Partial<ICoreOptions> { | ||
@IsString() | ||
@IsOptional() | ||
public readonly name?: string; | ||
|
||
@IsString() | ||
@IsOptional() | ||
public readonly description?: string; | ||
|
||
@IsString() | ||
@IsOptional() | ||
public readonly version?: string; | ||
|
||
constructor( | ||
@inject(TYPES.ProjectInputOptions) | ||
@optional() | ||
options: Partial<ICoreOptions> = {} | ||
) { | ||
Object.assign(this, options); | ||
const errors = validateSync(this); | ||
if (errors.length > 0) { | ||
throw new Error('Invalid root options: ' + errors.join(', ')); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could add some comment to expose what the data provide to
spec
and give some sample, e.g: oas3There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've set the built-in enum as a union type, it might help us to know more about the property.
BTW, it takes no effect when using the interface:
