Skip to content

deps/update hey api 0.42.1 #21

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

Closed
wants to merge 11 commits into from
Closed
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
30 changes: 30 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior:

**OpenAPI spec file**
If possible, please upload the OpenAPI spec file.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Screenshots**
If applicable, add screenshots or logs to help explain your problem.

- OS: [e.g. macOS]
- Version [e.g. v1.0.0]


**Additional context**
Add any other context about the problem here.
20 changes: 20 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@v3
with:
version: 8
version: 9

- name: Install Node.js
uses: actions/setup-node@v4
9 changes: 8 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8
version: 9
- name: Install Node.js
uses: actions/setup-node@v4
with:
@@ -44,3 +44,10 @@ jobs:

- name: Run tsc
run: pnpm --filter @7nohe/react-app test:generated

- name: Run test
run: pnpm test

- name: Report coverage
if: always() && matrix.os == 'ubuntu-latest'
uses: davelosert/vitest-coverage-report-action@v2
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -25,3 +25,4 @@ dist-ssr

openapi
*.tsbuildinfo
coverage
29 changes: 11 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -45,13 +45,16 @@ Options:
-o, --output <value> Output directory (default: "openapi")
-c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
--request <value> Path to custom request file
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
--enums Generate JavaScript objects from enum definitions?
--base <value> Manually set base in OpenAPI config instead of inferring from server value
--serviceResponse <value> Define shape of returned value from service calls ['body', 'generics', 'response']
--format <value> Process output folder with formatter? ['biome', 'prettier']
--lint <value> Process output folder with linter? ['eslint', 'biome']
--operationId Use operation ID to generate operation names?
--lint Process output folder with linter?
--format Process output folder with formatter?
--serviceResponse <value> Define shape of returned value from service calls ['body', 'generics', 'response']
--base <value> Manually set base in OpenAPI config instead of inferring from server value
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
--debug Enable debug mode
--noSchemas Disable generating schemas for request and response objects
--schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
-h, --help display help for command
```

@@ -86,11 +89,7 @@ function App() {
return (
<div className="App">
<h1>Pet List</h1>
<ul>
{data?.map((pet) => (
<li key={pet.id}>{pet.name}</li>
))}
</ul>
<ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
</div>
);
}
@@ -129,13 +128,7 @@ import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
function ChildComponent() {
const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });

return (
<ul>
{data?.map((pet, index) => (
<li key={pet.id}>{pet.name}</li>
))}
</ul>
);
return <ul>{data?.map((pet, index) => <li key={pet.id}>{pet.name}</li>)}</ul>;
}

function ParentComponent() {
3 changes: 2 additions & 1 deletion examples/react-app/package.json
Original file line number Diff line number Diff line change
@@ -9,13 +9,14 @@
"dev:mock": "prism mock ./petstore.yaml --dynamic",
"build": "tsc && vite build",
"preview": "vite preview",
"generate:api": "node ../../dist/cli.mjs -i ./petstore.yaml -c axios --request ./request.ts",
"generate:api": "rimraf ./openapi && node ../../dist/cli.mjs -i ./petstore.yaml -c axios --request ./request.ts",
"test:generated": "tsc -p ./tsconfig.openapi.json --noEmit"
},
"dependencies": {
"@tanstack/react-query": "^5.18.1",
"axios": "^1.6.7",
"form-data": "~4.0.0",
"prettier": "^3.2.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@7nohe/openapi-react-query-codegen",
"version": "1.0.6",
"version": "1.1.0",
"description": "OpenAPI React Query Codegen",
"bin": {
"openapi-rq": "dist/cli.mjs"
@@ -13,7 +13,8 @@
"build": "rimraf dist && tsc -p tsconfig.json",
"preview": "npm run build && npm -C examples/react-app run generate:api",
"prepublishOnly": "npm run build",
"release": "npx git-ensure -a && npx bumpp --commit --tag --push"
"release": "npx git-ensure -a && npx bumpp --commit --tag --push",
"test": "vitest --coverage.enabled true"
},
"repository": {
"type": "git",
@@ -37,20 +38,22 @@
"author": "Daiki Urata (@7nohe)",
"license": "MIT",
"devDependencies": {
"@hey-api/openapi-ts": "0.36.0",
"@hey-api/openapi-ts": "0.42.1",
"@types/node": "^20.10.6",
"@vitest/coverage-v8": "^1.5.0",
"commander": "^12.0.0",
"glob": "^10.3.10",
"rimraf": "^5.0.5",
"ts-morph": "^22.0.0",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^1.5.0"
},
"peerDependencies": {
"@hey-api/openapi-ts": "0.36.0",
"commander": ">= 11 < 13",
"glob": ">= 10",
"ts-morph": ">= 22 < 23",
"typescript": ">= 4.8.3"
"@hey-api/openapi-ts": "0.42.1",
"commander": "12.x",
"glob": "10.x",
"ts-morph": "22.x",
"typescript": "5.x"
},
"engines": {
"node": ">=14"
5,055 changes: 2,999 additions & 2,056 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

51 changes: 44 additions & 7 deletions src/cli.mts
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
#!/usr/bin/env node
import { generate } from "./generate.mjs";
import { Command, Option } from "commander";
import { UserConfig } from "@hey-api/openapi-ts";
import { readFile } from "fs/promises";
import { dirname, join } from "path";
import { fileURLToPath } from "node:url";

const program = new Command();

export type LimitedUserConfig = Omit<UserConfig, "useOptions">;
export type LimitedUserConfig = {
input: string;
output: string;
client?: "angular" | "axios" | "fetch" | "node" | "xhr";
request?: string;
format?: "biome" | "prettier";
lint?: "biome" | "eslint";
operationId?: boolean;
serviceResponse?: "body" | "response";
base?: string;
enums?: "javascript" | "typescript";
useDateType?: boolean;
debug?: boolean;
noSchemas?: boolean;
schemaType?: "form" | "json";
};

async function setupProgram() {
const __filename = fileURLToPath(import.meta.url);
@@ -32,29 +46,52 @@ async function setupProgram() {
.default("fetch")
)
.option("--request <value>", "Path to custom request file")
.option("--format", "Process output folder with formatter?")
.option("--lint", "Process output folder with linter?")
.addOption(
new Option(
"--format <value>",
"Process output folder with formatter?"
).choices(["biome", "prettier"])
)
.addOption(
new Option(
"--lint <value>",
"Process output folder with linter?"
).choices(["biome", "eslint"])
)
.option("--operationId", "Use operation ID to generate operation names?")
.addOption(
new Option(
"--serviceResponse <value>",
"Define shape of returned value from service calls"
).choices(["body", "generics", "response"])
).choices(["body", "response"])
)
.option(
"--base <value>",
"Manually set base in OpenAPI config instead of inferring from server value"
)
.option("--enums", "Generate JavaScript objects from enum definitions?")
.addOption(
new Option(
"--enums <value>",
"Generate JavaScript objects from enum definitions?"
).choices(["javascript", "typescript"])
)
.option(
"--useDateType",
"Use Date type instead of string for date types for models, this will not convert the data to a Date object"
)
.option("--debug", "Run in debug mode?")
.option("--noSchemas", "Disable generating JSON schemas")
.addOption(
new Option(
"--schemaType <value>",
"Type of JSON schema [Default: 'json']"
).choices(["form", "json"])
)
.parse();

const options = program.opts<LimitedUserConfig>();

generate(options, version);
await generate(options, version);
}

setupProgram();
9 changes: 9 additions & 0 deletions src/common.mts
Original file line number Diff line number Diff line change
@@ -89,3 +89,12 @@ export function extractPropertiesFromObjectParam(param: ParameterDeclaration) {
}));
return paramNodes;
}

/**
* Replace the import("...") surrounding the type if there is one.
* This can happen when the type is imported from another file, but
* we are already importing all the types from that file.
*/
export function getShortType(type: string) {
return type.replaceAll(/import\("[a-zA-Z\/\.-]*"\)\./g, "");
}
3 changes: 3 additions & 0 deletions src/constants.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export const defaultOutputPath = "openapi";
export const queriesOutputPath = "queries";
export const requestsOutputPath = "requests";

export const serviceFileName = "services.gen";
export const modalsFileName = "types.gen";
33 changes: 29 additions & 4 deletions src/createExports.mts
Original file line number Diff line number Diff line change
@@ -6,11 +6,36 @@ export const createExports = (service: Service) => {
const { klasses } = service;
const methods = klasses.map((k) => k.methods).flat();

const allGet = methods.filter((m) => m.httpMethodName === "'GET'");
const allPost = methods.filter((m) => m.httpMethodName === "'POST'");
const allGet = methods.filter((m) =>
m.httpMethodName.toUpperCase().includes("GET")
);
const allPost = methods.filter((m) =>
m.httpMethodName.toUpperCase().includes("POST")
);
const allPut = methods.filter((m) =>
m.httpMethodName.toUpperCase().includes("PUT")
);
const allPatch = methods.filter((m) =>
m.httpMethodName.toUpperCase().includes("PATCH")
);
const allDelete = methods.filter((m) =>
m.httpMethodName.toUpperCase().includes("DELETE")
);

const allQueries = allGet.map((m) => createUseQuery(m));
const allMutations = allPost.map((m) => createUseMutation(m));
const allGetQueries = allGet.map((m) => createUseQuery(m));

const allPostMutations = allPost.map((m) => createUseMutation(m));
const allPutMutations = allPut.map((m) => createUseMutation(m));
const allPatchMutations = allPatch.map((m) => createUseMutation(m));
const allDeleteMutations = allDelete.map((m) => createUseMutation(m));

const allQueries = [...allGetQueries];
const allMutations = [
...allPostMutations,
...allPutMutations,
...allPatchMutations,
...allDeleteMutations,
];

const commonInQueries = allQueries
.map(({ apiResponse, returnType, key }) => [apiResponse, returnType, key])
58 changes: 34 additions & 24 deletions src/createImports.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ts from "typescript";
import { posix } from "path";
import { Project } from "ts-morph";
import { modalsFileName, serviceFileName } from "./constants.mjs";

const { join } = posix;

@@ -13,21 +14,25 @@ export const createImports = ({
}) => {
const modelsFile = project
.getSourceFiles()
.find((sourceFile) => sourceFile.getFilePath().includes("models.ts"));
.find((sourceFile) => sourceFile.getFilePath().includes(modalsFileName));

const serviceFile = project
.getSourceFiles()
.find((sourceFile) => sourceFile.getFilePath().includes("services.ts"));
.find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName));

if (!modelsFile) {
throw new Error("No models file found");
console.warn(`
⚠️ WARNING: No models file found.
This may be an error if \`.components.schemas\` or \`.components.parameters\` is defined in your OpenAPI input.`);
}

if (!serviceFile) {
throw new Error("No service file found");
}

const modalNames = Array.from(modelsFile.getExportedDeclarations().keys());
const modelNames = modelsFile
? Array.from(modelsFile.getExportedDeclarations().keys())
: [];

const serviceExports = Array.from(
serviceFile.getExportedDeclarations().keys()
@@ -41,7 +46,7 @@ export const createImports = ({
name.endsWith("Data")
);

return [
const imports = [
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
@@ -112,27 +117,32 @@ export const createImports = ({
),
])
),
ts.factory.createStringLiteral(join("../requests")),
ts.factory.createStringLiteral(join("../requests", serviceFileName)),
undefined
),
];
if (modelsFile) {
// import all the models by name
ts.factory.createImportDeclaration(
undefined,
ts.factory.createImportClause(
false,
imports.push(
ts.factory.createImportDeclaration(
undefined,
ts.factory.createNamedImports([
...modalNames.map((modelName) =>
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(modelName)
)
),
])
),
ts.factory.createStringLiteral(join("../requests/models")),
undefined
),
];
ts.factory.createImportClause(
false,
undefined,
ts.factory.createNamedImports([
...modelNames.map((modelName) =>
ts.factory.createImportSpecifier(
false,
undefined,
ts.factory.createIdentifier(modelName)
)
),
])
),
ts.factory.createStringLiteral(join("../requests/", modalsFileName)),
undefined
)
);
}
return imports;
};
14 changes: 2 additions & 12 deletions src/createUseMutation.mts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import {
capitalizeFirstLetter,
extractPropertiesFromObjectParam,
getNameFromMethod,
getShortType,
} from "./common.mjs";
import { addJSDocToNode } from "./util.mjs";

@@ -83,24 +84,13 @@ export const createUseMutation = ({
refParam.optional
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
: undefined,
// refParam.questionToken ?? refParam.initializer
// ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
// : refParam.questionToken,
ts.factory.createTypeReferenceNode(
refParam.type.getText(param)
getShortType(refParam.type.getText(param))
)
)
);
})
.flat()
// return ts.factory.createPropertySignature(
// undefined,
// ts.factory.createIdentifier(param.getName()),
// param.compilerNode.questionToken ?? param.compilerNode.initializer
// ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
// : param.compilerNode.questionToken,
// param.compilerNode.type
// );
)
: ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);

19 changes: 8 additions & 11 deletions src/createUseQuery.mts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import {
capitalizeFirstLetter,
extractPropertiesFromObjectParam,
getNameFromMethod,
getShortType,
queryKeyConstraint,
queryKeyGenericType,
TData,
@@ -77,21 +78,19 @@ export function getRequestParamFromMethod(method: MethodDeclaration) {
return null;
}

// we need to get the properties of the object

const params = method
.getParameters()
.map((param) => {
const paramNodes = extractPropertiesFromObjectParam(param);
return paramNodes.map((refParam) => ({
name: refParam.name,
typeName: refParam.type.getText(),
typeName: getShortType(refParam.type.getText()),
optional: refParam.optional,
}));
})
.flat();

const areAllOptional = params.every((param) => param.optional);
const areAllPropertiesOptional = params.every((param) => param.optional);

return ts.factory.createParameterDeclaration(
undefined,
@@ -115,14 +114,15 @@ export function getRequestParamFromMethod(method: MethodDeclaration) {
refParam.optional
? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
: undefined,
// param.hasQuestionToken() ?? param.getInitializer()?.compilerNode
// ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
// : param.getQuestionTokenNode()?.compilerNode,
ts.factory.createTypeReferenceNode(refParam.typeName)
);
})
),
areAllOptional ? ts.factory.createObjectLiteralExpression() : undefined
// if all params are optional, we create an empty object literal
// so the hook can be called without any parameters
areAllPropertiesOptional
? ts.factory.createObjectLiteralExpression()
: undefined
);
}

@@ -305,9 +305,6 @@ function createQueryHook({
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral("queryFn")
),
ts.factory.createLiteralTypeNode(
ts.factory.createStringLiteral("initialData")
),
]),
]
)
44 changes: 32 additions & 12 deletions src/generate.mts
Original file line number Diff line number Diff line change
@@ -4,8 +4,9 @@ import path from "path";
import { createSource } from "./createSource.mjs";
import { defaultOutputPath, requestsOutputPath } from "./constants.mjs";
import { safeParseNumber } from "./common.mjs";
import { LimitedUserConfig } from "./cli.mjs";

export async function generate(options: UserConfig, version: string) {
export async function generate(options: LimitedUserConfig, version: string) {
const openApiOutputPath = path.join(
options.output ?? defaultOutputPath,
requestsOutputPath
@@ -16,28 +17,47 @@ export async function generate(options: UserConfig, version: string) {
// if the property is a string of boolean then convert it to a boolean
const formattedOptions = Object.entries(options).reduce(
(acc, [key, value]) => {
const typedKey = key as keyof UserConfig;
const typedValue = value as (typeof options)[keyof UserConfig];
const typedKey = key as keyof LimitedUserConfig;
const typedValue = value as (typeof options)[keyof LimitedUserConfig];
const parsedNumber = safeParseNumber(typedValue);
if (!isNaN(parsedNumber)) {
(acc as any)[typedKey] = parsedNumber;
} else if (value === "true") {
if (value === "true" || value === true) {
(acc as any)[typedKey] = true;
} else if (value === "false") {
} else if (value === "false" || value === false) {
(acc as any)[typedKey] = false;
} else if (!isNaN(parsedNumber)) {
(acc as any)[typedKey] = parsedNumber;
} else {
(acc as any)[typedKey] = typedValue;
}
return acc;
},
options
);
const config: UserConfig = {
...formattedOptions,
base: formattedOptions.base,
client: formattedOptions.client,
debug: formattedOptions.debug,
dryRun: false,
enums: formattedOptions.enums,
exportCore: true,
format: formattedOptions.format,
input: formattedOptions.input,
lint: formattedOptions.lint,
output: openApiOutputPath,
request: formattedOptions.request,
schemas: {
export: !formattedOptions.noSchemas,
type: formattedOptions.schemaType,
},
services: {
export: true,
response: formattedOptions.serviceResponse,
},
types: {
dates: formattedOptions.useDateType,
export: true,
},
useOptions: true,
exportCore: true,
exportModels: true,
exportServices: true,
write: true,
};
await createClient(config);
const source = await createSource({
3 changes: 2 additions & 1 deletion src/service.mts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ts from "typescript";
import { ClassDeclaration, Project, SourceFile } from "ts-morph";
import { MethodDescription } from "./common.mjs";
import { serviceFileName } from "./constants.mjs";

export type Service = {
node: SourceFile;
@@ -14,7 +15,7 @@ export type Service = {
export async function getServices(project: Project): Promise<Service> {
const node = project
.getSourceFiles()
.find((sourceFile) => sourceFile.getFilePath().includes("services.ts"));
.find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName));

if (!node) {
throw new Error("No service node found");
82 changes: 82 additions & 0 deletions tests/__snapshots__/createSource.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`createSource > createSource 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
export * from "./common";
export * from "./queries";
"
`;

exports[`createSource > createSource 2`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { DefaultService } from "../requests/services.gen";
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
export type DefaultServiceFindPetsDefaultResponse = Awaited<ReturnType<typeof DefaultService.findPets>>;
export type DefaultServiceFindPetsQueryResult<TData = DefaultServiceFindPetsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
export const useDefaultServiceFindPetsKey = "DefaultServiceFindPets";
export type DefaultServiceGetNotDefinedDefaultResponse = Awaited<ReturnType<typeof DefaultService.getNotDefined>>;
export type DefaultServiceGetNotDefinedQueryResult<TData = DefaultServiceGetNotDefinedDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
export const useDefaultServiceGetNotDefinedKey = "DefaultServiceGetNotDefined";
export type DefaultServiceFindPetByIdDefaultResponse = Awaited<ReturnType<typeof DefaultService.findPetById>>;
export type DefaultServiceFindPetByIdQueryResult<TData = DefaultServiceFindPetByIdDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
export const useDefaultServiceFindPetByIdKey = "DefaultServiceFindPetById";
export type DefaultServiceAddPetMutationResult = Awaited<ReturnType<typeof DefaultService.addPet>>;
export type DefaultServicePostNotDefinedMutationResult = Awaited<ReturnType<typeof DefaultService.postNotDefined>>;
export type DefaultServiceDeletePetMutationResult = Awaited<ReturnType<typeof DefaultService.deletePet>>;
"
`;
exports[`createSource > createSource 3`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
import * as Common from "./common";
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { DefaultService } from "../requests/services.gen";
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
export const useDefaultServiceFindPets = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
limit?: number;
tags?: string[];
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
/**
* @deprecated
*/
export const useDefaultServiceGetNotDefined = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
export const useDefaultServiceFindPetById = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
id: number;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });
export const useDefaultServiceAddPet = <TData = Common.DefaultServiceAddPetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
requestBody: NewPet;
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
requestBody: NewPet;
}, TContext>({ mutationFn: ({ requestBody }) => DefaultService.addPet({ requestBody }) as unknown as Promise<TData>, ...options });
/**
* @deprecated
*/
export const useDefaultServicePostNotDefined = <TData = Common.DefaultServicePostNotDefinedMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, void, TContext>, "mutationFn">) => useMutation<TData, TError, void, TContext>({ mutationFn: () => DefaultService.postNotDefined() as unknown as Promise<TData>, ...options });
export const useDefaultServiceDeletePet = <TData = Common.DefaultServiceDeletePetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
id: number;
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
id: number;
}, TContext>({ mutationFn: ({ id }) => DefaultService.deletePet({ id }) as unknown as Promise<TData>, ...options });
"
`;
exports[`createSource > createSource 4`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
import * as Common from "./common";
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { DefaultService } from "../requests/services.gen";
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
export const useDefaultServiceFindPetsSuspense = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
limit?: number;
tags?: string[];
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
/**
* @deprecated
*/
export const useDefaultServiceGetNotDefinedSuspense = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
export const useDefaultServiceFindPetByIdSuspense = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
id: number;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });
"
`;
82 changes: 82 additions & 0 deletions tests/__snapshots__/generate.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`generate > common.ts 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { DefaultService } from "../requests/services.gen";
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
export type DefaultServiceFindPetsDefaultResponse = Awaited<ReturnType<typeof DefaultService.findPets>>;
export type DefaultServiceFindPetsQueryResult<TData = DefaultServiceFindPetsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
export const useDefaultServiceFindPetsKey = "DefaultServiceFindPets";
export type DefaultServiceGetNotDefinedDefaultResponse = Awaited<ReturnType<typeof DefaultService.getNotDefined>>;
export type DefaultServiceGetNotDefinedQueryResult<TData = DefaultServiceGetNotDefinedDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
export const useDefaultServiceGetNotDefinedKey = "DefaultServiceGetNotDefined";
export type DefaultServiceFindPetByIdDefaultResponse = Awaited<ReturnType<typeof DefaultService.findPetById>>;
export type DefaultServiceFindPetByIdQueryResult<TData = DefaultServiceFindPetByIdDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>;
export const useDefaultServiceFindPetByIdKey = "DefaultServiceFindPetById";
export type DefaultServiceAddPetMutationResult = Awaited<ReturnType<typeof DefaultService.addPet>>;
export type DefaultServicePostNotDefinedMutationResult = Awaited<ReturnType<typeof DefaultService.postNotDefined>>;
export type DefaultServiceDeletePetMutationResult = Awaited<ReturnType<typeof DefaultService.deletePet>>;
"
`;
exports[`generate > index.ts 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
export * from "./common";
export * from "./queries";
"
`;
exports[`generate > queries.ts 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
import * as Common from "./common";
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { DefaultService } from "../requests/services.gen";
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
export const useDefaultServiceFindPets = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
limit?: number;
tags?: string[];
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
/**
* @deprecated
*/
export const useDefaultServiceGetNotDefined = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
export const useDefaultServiceFindPetById = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
id: number;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });
export const useDefaultServiceAddPet = <TData = Common.DefaultServiceAddPetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
requestBody: NewPet;
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
requestBody: NewPet;
}, TContext>({ mutationFn: ({ requestBody }) => DefaultService.addPet({ requestBody }) as unknown as Promise<TData>, ...options });
/**
* @deprecated
*/
export const useDefaultServicePostNotDefined = <TData = Common.DefaultServicePostNotDefinedMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, void, TContext>, "mutationFn">) => useMutation<TData, TError, void, TContext>({ mutationFn: () => DefaultService.postNotDefined() as unknown as Promise<TData>, ...options });
export const useDefaultServiceDeletePet = <TData = Common.DefaultServiceDeletePetMutationResult, TError = unknown, TContext = unknown>(options?: Omit<UseMutationOptions<TData, TError, {
id: number;
}, TContext>, "mutationFn">) => useMutation<TData, TError, {
id: number;
}, TContext>({ mutationFn: ({ id }) => DefaultService.deletePet({ id }) as unknown as Promise<TData>, ...options });
"
`;
exports[`generate > suspense.ts 1`] = `
"// generated with @7nohe/openapi-react-query-codegen@1.0.0
import * as Common from "./common";
import { useQuery, useSuspenseQuery, useMutation, UseQueryResult, UseQueryOptions, UseMutationOptions, UseMutationResult } from "@tanstack/react-query";
import { DefaultService } from "../requests/services.gen";
import { Pet, NewPet, Error, $OpenApiTs } from "../requests/types.gen";
export const useDefaultServiceFindPetsSuspense = <TData = Common.DefaultServiceFindPetsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ limit, tags }: {
limit?: number;
tags?: string[];
} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetsKey, ...(queryKey ?? [{ limit, tags }])], queryFn: () => DefaultService.findPets({ limit, tags }) as TData, ...options });
/**
* @deprecated
*/
export const useDefaultServiceGetNotDefinedSuspense = <TData = Common.DefaultServiceGetNotDefinedDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>(queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceGetNotDefinedKey, ...(queryKey ?? [])], queryFn: () => DefaultService.getNotDefined() as TData, ...options });
export const useDefaultServiceFindPetByIdSuspense = <TData = Common.DefaultServiceFindPetByIdDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ id }: {
id: number;
}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: [Common.useDefaultServiceFindPetByIdKey, ...(queryKey ?? [{ id }])], queryFn: () => DefaultService.findPetById({ id }) as TData, ...options });
"
`;
34 changes: 34 additions & 0 deletions tests/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from "vitest";
import { BuildCommonTypeName, capitalizeFirstLetter, lowercaseFirstLetter, safeParseNumber } from "../src/common.mts";

describe("common", () => {
test("safeParseNumber", () => {
const parsed = safeParseNumber("123");
expect(parsed).toBe(123);

const nan = safeParseNumber("abc");
expect(nan).toBeNaN();

const trueVal = safeParseNumber(true);
expect(trueVal).toBe(1);

const falseVal = safeParseNumber(false);
expect(falseVal).toBe(0);
});

test("capitalizeFirstLetter", () => {
const capitalized = capitalizeFirstLetter("testTestTest");
expect(capitalized).toBe("TestTestTest");
});

test("lowercaseFirstLetter", () => {
const lowercased = lowercaseFirstLetter("TestTestTest");
expect(lowercased).toBe("testTestTest");
});

test('buildCommonTypeName', () => {
const name = 'Name';
const result = BuildCommonTypeName(name);
expect(result.escapedText).toBe('Common.Name');
});
});
71 changes: 71 additions & 0 deletions tests/createExports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { cleanOutputs, generateTSClients, outputPath } from "./utils";
import { Project, SyntaxKind } from "ts-morph";
import { createExports } from "../src/createExports.mts";
import { getServices } from "../src/service.mts";
import path from "path";

const fileName = "createExports"

describe(fileName, () => {
beforeAll(async () => await generateTSClients(fileName));
afterAll(async () => await cleanOutputs(fileName));

test("createExports", async () => {
const project = new Project({
skipAddingFilesFromTsConfig: true,
});
project.addSourceFilesAtPaths(path.join(outputPath(fileName), '**', '*'));
const service = await getServices(project);
const exports = createExports(service);

const commonTypes = exports.allCommon
.filter((c) => c.kind === SyntaxKind.TypeAliasDeclaration)
// @ts-ignore
.map((e) => e.name.escapedText);
expect(commonTypes).toStrictEqual([
"DefaultServiceFindPetsDefaultResponse",
"DefaultServiceFindPetsQueryResult",
"DefaultServiceGetNotDefinedDefaultResponse",
"DefaultServiceGetNotDefinedQueryResult",
"DefaultServiceFindPetByIdDefaultResponse",
"DefaultServiceFindPetByIdQueryResult",
"DefaultServiceAddPetMutationResult",
"DefaultServicePostNotDefinedMutationResult",
"DefaultServiceDeletePetMutationResult",
]);

const constants = exports.allCommon
.filter((c) => c.kind === SyntaxKind.VariableStatement)
// @ts-ignore
.map((c) => c.declarationList.declarations[0].name.escapedText);
expect(constants).toStrictEqual([
"useDefaultServiceFindPetsKey",
"useDefaultServiceGetNotDefinedKey",
"useDefaultServiceFindPetByIdKey",
]);

const mainExports = exports.mainExports.map(
// @ts-ignore
(e) => e.declarationList.declarations[0].name.escapedText
);
expect(mainExports).toStrictEqual([
"useDefaultServiceFindPets",
"useDefaultServiceGetNotDefined",
"useDefaultServiceFindPetById",
"useDefaultServiceAddPet",
"useDefaultServicePostNotDefined",
"useDefaultServiceDeletePet",
]);

const suspenseExports = exports.suspenseExports.map(
// @ts-ignore
(e) => e.declarationList.declarations[0].name.escapedText
);
expect(suspenseExports).toStrictEqual([
"useDefaultServiceFindPetsSuspense",
"useDefaultServiceGetNotDefinedSuspense",
"useDefaultServiceFindPetByIdSuspense",
]);
});
});
52 changes: 52 additions & 0 deletions tests/createImports.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, test } from "vitest";
import { cleanOutputs, generateTSClients, outputPath } from "./utils";
import { Project } from "ts-morph";
import { createImports } from "../src/createImports.mts";
import path from "path";

const fileName = "createImports";

describe(fileName, () => {
test("createImports", async () => {
await generateTSClients(fileName);
const project = new Project({
skipAddingFilesFromTsConfig: true,
});
project.addSourceFilesAtPaths(path.join(outputPath(fileName), "**", "*"));
const imports = createImports({
serviceEndName: "Service",
project,
});

// @ts-ignore
const moduleNames = imports.map((i) => i.moduleSpecifier.text);
expect(moduleNames).toStrictEqual([
"@tanstack/react-query",
"../requests/services.gen",
"../requests/types.gen",
]);
await cleanOutputs(fileName);
});

test("createImports (No models)", async () => {
const fileName = "createImportsNoModels";
await generateTSClients(fileName, "no-models.yaml");
const project = new Project({
skipAddingFilesFromTsConfig: true,
});
project.addSourceFilesAtPaths(path.join(outputPath(fileName), "**", "*"));
const imports = createImports({
serviceEndName: "Service",
project,
});

// @ts-ignore
const moduleNames = imports.map((i) => i.moduleSpecifier.text);
expect(moduleNames).toStrictEqual([
"@tanstack/react-query",
"../requests/services.gen",
"../requests/types.gen",
]);
await cleanOutputs(fileName);
});
});
29 changes: 29 additions & 0 deletions tests/createSource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { createSource } from '../src/createSource.mjs'
import { cleanOutputs, generateTSClients, outputPath } from "./utils";
const fileName = "createSource";
describe(fileName, () => {
beforeAll(async () => await generateTSClients(fileName));
afterAll(async () => await cleanOutputs(fileName));


test("createSource", async () => {
const source = await createSource({
outputPath: outputPath(fileName),
version: "1.0.0",
serviceEndName: "Service",
});

const indexTs = source.find((s) => s.name === "index.ts");
expect(indexTs?.content).toMatchSnapshot();

const commonTs = source.find((s) => s.name === "common.ts");
expect(commonTs?.content).toMatchSnapshot();

const queriesTs = source.find((s) => s.name === "queries.ts");
expect(queriesTs?.content).toMatchSnapshot();

const suspenseTs = source.find((s) => s.name === "suspense.ts");
expect(suspenseTs?.content).toMatchSnapshot();
});
});
46 changes: 46 additions & 0 deletions tests/generate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { generate } from "../src/generate.mjs";
import { rmdir } from "node:fs/promises";
import { LimitedUserConfig } from "../src/cli.mts";

const readOutput = (fileName: string) => {
return readFileSync(
path.join(__dirname, "outputs", "queries", fileName),
"utf-8"
);
};

describe("generate", () => {
beforeAll(async () => {
const options: LimitedUserConfig = {
input: path.join(__dirname, "inputs", "petstore.yaml"),
output: path.join("tests", "outputs"),
lint: "eslint",
};
await generate(options, "1.0.0");
});

afterAll(async () => {
if (existsSync(path.join(__dirname, "outputs"))) {
await rmdir(path.join(__dirname, "outputs"), { recursive: true });
}
});

test("common.ts", () => {
expect(readOutput("common.ts")).toMatchSnapshot();
});

test("queries.ts", () => {
expect(readOutput("queries.ts")).toMatchSnapshot();
});

test("index.ts", () => {
expect(readOutput("index.ts")).toMatchSnapshot();
});

test("suspense.ts", () => {
expect(readOutput("suspense.ts")).toMatchSnapshot();
});
});
105 changes: 105 additions & 0 deletions tests/inputs/no-models.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
termsOfService: http://swagger.io/terms/
contact:
name: Swagger API Team
email: apiteam@swagger.io
url: http://swagger.io
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://petstore.swagger.io/api
paths:
/pets:
get:
description: |
Returns all pets from the system that the user has access to
Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
operationId: findPets
parameters:
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
default:
description: unexpected error
post:
description: Creates a new pet in the store. Duplicates are allowed
operationId: addPet
requestBody:
description: Pet to add to the store
required: true
content:
application/json:
responses:
'200':
description: pet response
default:
description: unexpected error
/not-defined:
get:
deprecated: true
description: This path is not fully defined.
responses:
default:
description: unexpected error
post:
deprecated: true
description: This path is not defined at all.
responses:
default:
description: unexpected error
/pets/{id}:
get:
description: Returns a user based on a single ID, if the user does not have access to the pet
operationId: find pet by id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
responses:
'200':
description: pet response
default:
description: unexpected error
delete:
description: deletes a single pet based on the ID supplied
operationId: deletePet
parameters:
- name: id
in: path
description: ID of pet to delete
required: true
schema:
type: integer
format: int64
responses:
'204':
description: pet deleted
default:
description: unexpected error
171 changes: 171 additions & 0 deletions tests/inputs/petstore.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Swagger Petstore
description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification
termsOfService: http://swagger.io/terms/
contact:
name: Swagger API Team
email: apiteam@swagger.io
url: http://swagger.io
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0.html
servers:
- url: http://petstore.swagger.io/api
paths:
/pets:
get:
description: |
Returns all pets from the system that the user has access to
Nam sed condimentum est. Maecenas tempor sagittis sapien, nec rhoncus sem sagittis sit amet. Aenean at gravida augue, ac iaculis sem. Curabitur odio lorem, ornare eget elementum nec, cursus id lectus. Duis mi turpis, pulvinar ac eros ac, tincidunt varius justo. In hac habitasse platea dictumst. Integer at adipiscing ante, a sagittis ligula. Aenean pharetra tempor ante molestie imperdiet. Vivamus id aliquam diam. Cras quis velit non tortor eleifend sagittis. Praesent at enim pharetra urna volutpat venenatis eget eget mauris. In eleifend fermentum facilisis. Praesent enim enim, gravida ac sodales sed, placerat id erat. Suspendisse lacus dolor, consectetur non augue vel, vehicula interdum libero. Morbi euismod sagittis libero sed lacinia.
Sed tempus felis lobortis leo pulvinar rutrum. Nam mattis velit nisl, eu condimentum ligula luctus nec. Phasellus semper velit eget aliquet faucibus. In a mattis elit. Phasellus vel urna viverra, condimentum lorem id, rhoncus nibh. Ut pellentesque posuere elementum. Sed a varius odio. Morbi rhoncus ligula libero, vel eleifend nunc tristique vitae. Fusce et sem dui. Aenean nec scelerisque tortor. Fusce malesuada accumsan magna vel tempus. Quisque mollis felis eu dolor tristique, sit amet auctor felis gravida. Sed libero lorem, molestie sed nisl in, accumsan tempor nisi. Fusce sollicitudin massa ut lacinia mattis. Sed vel eleifend lorem. Pellentesque vitae felis pretium, pulvinar elit eu, euismod sapien.
operationId: findPets
parameters:
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
description: Creates a new pet in the store. Duplicates are allowed
operationId: addPet
requestBody:
description: Pet to add to the store
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewPet'
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/not-defined:
get:
deprecated: true
description: This path is not fully defined.
responses:
default:
description: unexpected error
post:
deprecated: true
description: This path is not defined at all.
responses:
default:
description: unexpected error
/pets/{id}:
get:
description: Returns a user based on a single ID, if the user does not have access to the pet
operationId: find pet by id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
responses:
'200':
description: pet response
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
description: deletes a single pet based on the ID supplied
operationId: deletePet
parameters:
- name: id
in: path
description: ID of pet to delete
required: true
schema:
type: integer
format: int64
responses:
'204':
description: pet deleted
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Pet:
allOf:
- $ref: '#/components/schemas/NewPet'
- type: object
required:
- id
properties:
id:
type: integer
format: int64

NewPet:
type: object
required:
- name
properties:
name:
type: string
tag:
type: string

Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
30 changes: 30 additions & 0 deletions tests/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { getServices } from "../src/service.mjs";
import { Project } from "ts-morph";
import { cleanOutputs, generateTSClients } from "./utils";
import path from "path";
const fileName = "service"
describe(fileName, () => {
beforeAll(async () => await generateTSClients(fileName));
afterAll(async () => await cleanOutputs(fileName));

test("getServices", async () => {
const project = new Project({
skipAddingFilesFromTsConfig: true,
});
project.addSourceFilesAtPaths(path.join('tests', `${fileName}-outputs`, '**', '*'));
const service = await getServices(project);
const klass = service.klasses[0];
expect(klass.className).toBe('DefaultService');
const methodNames = klass.methods.map((m) => m.method.getName());
expect(methodNames).toEqual(['findPets', 'addPet', 'getNotDefined', 'postNotDefined', 'findPetById', 'deletePet']);
});

test("getServices (No service node found)", async () => {
const project = new Project({
skipAddingFilesFromTsConfig: true,
});
project.addSourceFilesAtPaths(`no/services/**/*`);
await expect(() => getServices(project)).rejects.toThrowError("No service node found");
});
});
21 changes: 21 additions & 0 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { UserConfig, createClient } from "@hey-api/openapi-ts";
import { existsSync } from "node:fs";
import { rm } from "node:fs/promises";
import path from "node:path";
export const outputPath = (prefix: string) =>
path.join("tests", `${prefix}-outputs`);

export const generateTSClients = async (prefix: string, inputFile?: string) => {
const options: UserConfig = {
input: path.join(__dirname, "inputs", inputFile ?? "petstore.yaml"),
output: outputPath(prefix),
};
await createClient(options);
};

export const cleanOutputs = async (prefix: string) => {
const output = `${prefix}-outputs`;
if (existsSync(path.join(__dirname, output))) {
await rm(path.join(__dirname, output), { recursive: true });
}
};
17 changes: 17 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { defineConfig } from "vitest/config";

export default defineConfig({
test: {
coverage: {
reporter: ['text', 'json-summary', 'json', 'html'],
exclude: ['src/cli.mts', 'examples/**'],
reportOnFailure: true,
thresholds: {
lines: 95,
functions: 95,
statements: 95,
branches: 85,
}
}
},
});