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

Fix/explicit validation message zod #27

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"license": "ISC",
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.7",
"@types/node": "^20.16.1",
"prettier": "^3.3.3",
Expand All @@ -37,7 +38,9 @@
"@changesets/cli": "^2.27.7",
"@eventcatalog/sdk": "^0.1.4",
"chalk": "^4",
"js-yaml": "^4.1.0",
"openapi-types": "^12.1.3",
"slugify": "^1.6.6"
"slugify": "^1.6.6",
"zod": "^3.23.8"
}
}
32 changes: 32 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 42 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,42 @@ import { defaultMarkdown as generateMarkdownForDomain } from './utils/domains';
import { buildService } from './utils/services';
import { buildMessage } from './utils/messages';
import { getOperationsByType } from './utils/openapi';
import { Domain, Service } from './types';
import { Service } from './types';
import { getMessageTypeUtils } from './utils/catalog-shorthand';
import { OpenAPI } from 'openapi-types';
import checkLicense from './utils/checkLicense';

type Props = {
services: Service[];
domain?: Domain;
debug?: boolean;
import yaml from 'js-yaml';
import { z } from 'zod';

const optionsSchema = z.object({
services: z.array(
z.object({
id: z.string({ required_error: 'The service id is required. please provide the service id' }),
path: z.string({ required_error: 'The service path is required. please provide the path to specification file' }),
name: z.string().optional(),
}),
{ message: 'Please provide correct services configuration' }
),
domain: z
.object({
id: z.string({ required_error: 'The domain id is required. please provide a domain id' }),
name: z.string({ required_error: 'The domain name is required. please provide a domain name' }),
version: z.string({ required_error: 'The domain version is required. please provide a domain version' }),
})
.optional(),
debug: z.boolean().optional(),
saveParsedSpecFile: z.boolean({ invalid_type_error: 'The saveParsedSpecFile is not a boolean in options' }).optional(),
});

type Props = z.infer<typeof optionsSchema>;

const validateOptions = (options: Props) => {
try {
optionsSchema.parse(options);
} catch (error: any) {
if (error instanceof z.ZodError) throw new Error(JSON.stringify(error.issues, null, 2));
}
};

export default async (_: any, options: Props) => {
if (!process.env.PROJECT_DIR) {
throw new Error('Please provide catalog url (env variable PROJECT_DIR)');
Expand All @@ -36,7 +61,8 @@ export default async (_: any, options: Props) => {
getSpecificationFilesForService,
} = utils(process.env.PROJECT_DIR);

const services = options.services ?? [];
const { services = [], saveParsedSpecFile = false } = options;
validateOptions(options);
for (const serviceSpec of services) {
console.log(chalk.green(`Processing ${serviceSpec.path}`));

Expand All @@ -48,7 +74,6 @@ export default async (_: any, options: Props) => {
continue;
}

const openAPIFile = await readFile(serviceSpec.path, 'utf-8');
const document = await SwaggerParser.parse(serviceSpec.path);
const version = document.info.version;

Expand Down Expand Up @@ -138,7 +163,7 @@ export default async (_: any, options: Props) => {
// add any previous spec files to the list
...serviceSpecificationsFiles,
{
content: openAPIFile,
content: saveParsedSpecFile ? getParsedSpecFile(serviceSpec, document) : await getRawSpecFile(serviceSpec),
fileName: service.schemaPath,
},
];
Expand Down Expand Up @@ -235,3 +260,10 @@ const processMessagesForOpenAPISpec = async (pathToSpec: string, document: OpenA
}
return { receives, sends: [] };
};

const getParsedSpecFile = (service: Service, document: OpenAPI.Document) => {
const isSpecFileJSON = service.path.endsWith('.json');
return isSpecFileJSON ? JSON.stringify(document, null, 4) : yaml.dump(document, { noRefs: true });
};

const getRawSpecFile = async (service: Service) => await readFile(service.path, 'utf8');
113 changes: 113 additions & 0 deletions src/test/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,48 @@ describe('OpenAPI EventCatalog Plugin', () => {
expect(schema).toBeDefined();
});

it('the original openapi file is added to the service by default instead of parsed version', async () => {
const { getService } = utils(catalogDir);
await plugin(config, { services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }] });

const service = await getService('swagger-petstore', '1.0.0');

expect(service.schemaPath).toEqual('petstore.yml');

const schema = await fs.readFile(join(catalogDir, 'services', 'swagger-petstore', 'petstore.yml'), 'utf8');
expect(schema).toBeDefined();
});

it('the original openapi file is added to the service instead of parsed version', async () => {
const { getService } = utils(catalogDir);
await plugin(config, {
services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }],
saveParsedSpecFile: false,
});

const service = await getService('swagger-petstore', '1.0.0');

expect(service.schemaPath).toEqual('petstore.yml');

const schema = await fs.readFile(join(catalogDir, 'services', 'swagger-petstore', 'petstore.yml'), 'utf8');
expect(schema).toBeDefined();
});

it('the original openapi file is not added but the parsed version', async () => {
const { getService } = utils(catalogDir);
await plugin(config, {
services: [{ path: join(openAPIExamples, 'petstore.yml'), id: 'swagger-petstore' }],
saveParsedSpecFile: true,
});

const service = await getService('swagger-petstore', '1.0.0');

expect(service.schemaPath).toEqual('petstore.yml');

const schema = await fs.readFile(join(catalogDir, 'services', 'swagger-petstore', 'petstore.yml'), 'utf8');
expect(schema).toBeDefined();
});

it('the openapi file is added to the specifications list in eventcatalog', async () => {
const { getService, writeService } = utils(catalogDir);

Expand Down Expand Up @@ -510,6 +552,77 @@ describe('OpenAPI EventCatalog Plugin', () => {

expect(service).toBeDefined();
});

it('[id] if the `id` not provided in the service config options, The generator throw an explicit error', async () => {
await expect(
plugin(config, {
services: [
{
path: join(openAPIExamples, 'petstore.yml'),
} as any,
],
})
).rejects.toThrow('The service id is required');
});
it('[services] if the `services` not provided in options, The generator throw an explicit error', async () => {
await expect(plugin(config, {} as any)).rejects.toThrow('Please provide correct services configuration');
});
it('[services] if the `services` is undefiend in options, The generator throw an explicit error', async () => {
await expect(plugin(config, { services: undefined } as any)).rejects.toThrow(
'Please provide correct services configuration'
);
});
it('[services::path] if the `services::path` not provided in options, The generator throw an explicit error', async () => {
await expect(plugin(config, { services: [{ id: 'service_id' }] } as any)).rejects.toThrow(
'The service path is required. please provide the path to specification file'
);
});
it('[services::id] if the `services::id` not provided in options, The generator throw an explicit error', async () => {
await expect(plugin(config, { services: [{ path: 'path/to/spec' }] } as any)).rejects.toThrow(
'The service id is required. please provide the service id'
);
});
it('[path] if the `path` not provided in service config options, The generator throw an explicit error', async () => {
await expect(
plugin(config, {
services: [
{
name: 'Awesome account service',
id: 'awsome-service',
} as any,
],
})
).rejects.toThrow('The service path is required. please provide the path to specification file');
});
it('[services::saveParsedSpecFile] if the `services::saveParsedSpecFile` not a boolean in options, The generator throw an explicit error', async () => {
await expect(
plugin(config, { services: [{ path: 'path/to/spec', id: 'sevice_id' }], saveParsedSpecFile: 'true' } as any)
).rejects.toThrow('The saveParsedSpecFile is not a boolean in options');
});
it('[domain::id] if the `domain::id` not provided in options, The generator throw an explicit error', async () => {
await expect(
plugin(config, {
domain: { name: 'domain_name', version: '1.0.0' },
services: [{ path: 'path/to/spec', id: 'sevice_id' }],
} as any)
).rejects.toThrow('The domain id is required. please provide a domain id');
});
it('[domain::name] if the `domain::name` not provided in options, The generator throw an explicit error', async () => {
await expect(
plugin(config, {
domain: { id: 'domain_name', version: '1.0.0' },
services: [{ path: 'path/to/spec', id: 'sevice_id' }],
} as any)
).rejects.toThrow('The domain name is required. please provide a domain name');
});
it('[domain::version] if the `domain::version` not provided in options, The generator throw an explicit error', async () => {
await expect(
plugin(config, {
domain: { id: 'domain_name', name: 'domain_name' },
services: [{ path: 'path/to/spec', id: 'sevice_id' }],
} as any)
).rejects.toThrow('The domain version is required. please provide a domain version');
});
});
});
});
Expand Down