Skip to content

Commit

Permalink
WIP: openapi-codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
Minhir committed May 19, 2024
1 parent e9b1967 commit 47d51a6
Show file tree
Hide file tree
Showing 13 changed files with 1,238 additions and 34 deletions.
11 changes: 11 additions & 0 deletions packages/openapi-codegen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# openapi-codegen

This library was generated with [Nx](https://nx.dev).

## Building

Run `nx build openapi-codegen` to build the library.

## Running unit tests

Run `nx test openapi-codegen` to execute the unit tests via [Jest](https://jestjs.io).
49 changes: 49 additions & 0 deletions packages/openapi-codegen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@farfetched/openapi-codegen",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"license": "MIT",
"repository": "https://github.com/igorkamyshev/farfetched",
"dependencies": {
"handlebars": "^4.7.8",
"oas": "^24.1.0",
"oas-normalize": "^11.0.1"
},
"devDependencies": {
"@farfetched/core": "workspace:*",
"@farfetched/json-schema": "workspace:*",
"tsx": "^4.6.2"
},
"peerDependencies": {
"@farfetched/core": "workspace:*",
"@farfetched/json-schema": "workspace:*",
"effector": "^23.0.0"
},
"scripts": {
"start": "tsx ./src/index.ts",
"test:run": "vitest run --typecheck",
"publint": "node ../../tools/scripts/publint.mjs",
"typelint": "attw --pack"
},
"type": "module",
"files": [
"dist"
],
"main": "./dist/openapi-codegen.cjs",
"module": "./dist/openapi-codegen.js",
"types": "./dist/openapi-codegen.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/openapi-codegen.d.ts",
"default": "./dist/openapi-codegen.js"
},
"require": {
"types": "./dist/openapi-codegen.d.cts",
"default": "./dist/openapi-codegen.cjs"
}
}
}
}
14 changes: 14 additions & 0 deletions packages/openapi-codegen/src/__tests__/petstore/petstore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { it, expect } from 'vitest';

import { generateEndpoints } from '../../index';
import path, { resolve } from 'path';

it('generates working code', async () => {
const result = await generateEndpoints({
schemaFile: resolve(__dirname, './petstore.yaml'),
apiFile: './test.ts',
outputFile: './src/test.ts',
});

expect(result).toMatchFileSnapshot('./petstore.ts');
});
65 changes: 65 additions & 0 deletions packages/openapi-codegen/src/__tests__/petstore/petstore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { createJsonQuery, createQuery, declareParams } from "@farfetched/core";
import { jsonSchemaContract } from "@farfetched/json-schema";

export const listPetsQuery = createJsonQuery({
params: declareParams<{ id: number }>(),
request: {
method: "GET",
url: () => "/pets",
},
response: {
contract: jsonSchemaContract({
type: "array",
maxItems: 100,
items: {
type: "object",
required: ["id", "name"],
properties: {
id: {
type: "integer",
format: "int64",
minimum: -9223372036854776000,
maximum: 9223372036854776000,
},
name: { type: "string" },
tag: { type: "string" },
},
},
$schema: "http://json-schema.org/draft-04/schema#",
}),
},
});

export const createPetsQuery = createJsonQuery({
params: declareParams<{ id: number }>(),
request: {
method: "POST",
url: () => "/pets",
},
response: {},
});

export const showPetByIdQuery = createJsonQuery({
params: declareParams<{ id: number }>(),
request: {
method: "GET",
url: () => "/pets/{petId}",
},
response: {
contract: jsonSchemaContract({
type: "object",
required: ["id", "name"],
properties: {
id: {
type: "integer",
format: "int64",
minimum: -9223372036854776000,
maximum: 9223372036854776000,
},
name: { type: "string" },
tag: { type: "string" },
},
$schema: "http://json-schema.org/draft-04/schema#",
}),
},
});
113 changes: 113 additions & 0 deletions packages/openapi-codegen/src/__tests__/petstore/petstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
openapi: '3.0.0'
info:
version: 1.0.0
title: Swagger Petstore
license:
name: MIT
servers:
- url: http://petstore.swagger.io/v1
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags:
- pets
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
maximum: 100
format: int32
responses:
'200':
description: A paged array of pets
headers:
x-next:
description: A link to the next page of responses
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/Pets'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Create a pet
operationId: createPets
tags:
- pets
responses:
'201':
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags:
- pets
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
'200':
description: Expected response to a valid request
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
maxItems: 100
items:
$ref: '#/components/schemas/Pet'
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
34 changes: 34 additions & 0 deletions packages/openapi-codegen/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import OASNormalize from 'oas-normalize';
import * as prettier from 'prettier';
import Oas from 'oas';
import { generateQueries } from './lib/generator';

type GenerationOptions = {
schemaFile: string;
apiFile: string;
outputFile: string;
};

export async function generateEndpoints(
options: GenerationOptions
): Promise<string | void> {
const oasNormalize = new OASNormalize(options.schemaFile, {
enablePaths: true,
});

const oasContent = await oasNormalize.validate();

const oas = new Oas(oasContent);

await oas.dereference();

const sourceCode = generateQueries(oas);

if (!sourceCode) {
return;
}

const outputFile = options.outputFile;

return prettier.format(sourceCode, { parser: 'typescript' });
}
87 changes: 87 additions & 0 deletions packages/openapi-codegen/src/lib/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Oas from 'oas';

import { compile } from 'handlebars';

const apiGenerator = compile(`
import { createJsonQuery, createQuery, declareParams } from '@farfetched/core';
import { jsonSchemaContract } from "@farfetched/json-schema";
{{#each queries}}
{{{this}}}
{{/each}}
`);

const queryGenerator = compile(`
export const {{name}}Query = createJsonQuery({
params: declareParams<{ id: number }>(),
request: {
method: '{{method}}',
url: () => '{{{url}}}',
},
response: {
{{{contract}}}
},
});
`);

const contractGenerator = compile(
` contract: jsonSchemaContract( {{{jsonSchema}}} ),`
);

function getAllOasOperations(oas: Oas) {
return Object.values(oas.getPaths()).flatMap((v) => Object.values(v));
}

export function generateQueries(oas: Oas) {
if (!oas.api.paths) {
return;
}

const queries: string[] = [];

const operations = getAllOasOperations(oas);

for (const operation of operations) {
const { method, path } = operation;

if (!operation.isJson()) {
console.warn(`Skipping ${method} ${path} because it is not JSON.`);

continue;
}

const params = operation.getParametersAsJSONSchema();

const response = operation.getResponseAsJSONSchema(200) ?? [];

const responseBody = response.find((v) => v.label === 'Response body');

if (responseBody) {
deepOmit(responseBody, 'x-readme-ref-name');
}

const ffQuery = queryGenerator({
name: operation.getOperationId(),
url: path,
method: method.toUpperCase(),
contract: responseBody
? contractGenerator({ jsonSchema: JSON.stringify(responseBody.schema) })
: '',
});

queries.push(ffQuery);
}

return apiGenerator({ queries });
}

function deepOmit(obj: Object, key: string) {
// @ts-expect-error
delete obj[key];

for (const val of Object.values(obj)) {
if (typeof val === 'object') {
deepOmit(val, key);
}
}
}
Loading

0 comments on commit 47d51a6

Please sign in to comment.