diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 59be383..a3b8a6b 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # @grafikart/o2ts -A simple tool to convert OpenAPI 3.0/3.1 specs into a TypeScript file with useful types. +Un outil simple et puissant pour convertir des spécifications OpenAPI 3.0/3.1 en fichiers TypeScript avec des types utilisables. -## Install +## Installation ```bash npm i -D @grafikart/o2ts @@ -10,9 +10,9 @@ yarn add -D @grafikart/o2ts pnpm add -D @grafikart/o2ts ``` -## Usage +## Utilisation basique -Add the following script to your `package.json`: +Ajoutez le script suivant à votre `package.json` : ```json { @@ -22,26 +22,213 @@ Add the following script to your `package.json`: } ``` -Then: +Puis exécutez : ```bash npm run openapi ``` -## Example +Par défaut, `o2ts` générera un fichier TypeScript avec le même nom que votre fichier OpenAPI (en remplaçant l'extension `.yml` ou `.yaml` par `.ts`). -Here is an example of using the definitions generated with the tool: +## Utilisation avancée -```ts +### Spécifier un nom de fichier de sortie + +```bash +o2ts ./openapi.yml ./types/api.ts +``` + +### Utiliser avec différentes sources OpenAPI + +Le package peut traiter les spécifications OpenAPI depuis : + +- Un fichier local : `o2ts ./path/to/openapi.yml` +- Un URL (bientôt disponible) : `o2ts https://example.com/api/openapi.json` + +## Configuration + +@grafikart/o2ts est configurable pour s'adapter à vos besoins. Vous pouvez configurer l'outil de deux façons : + +### 1. Fichier `.o2tsrc.json` + +Créez un fichier `.o2tsrc.json` à la racine de votre projet : + +```json +{ + "format": { + "semi": true, + "tabWidth": 4, + "singleQuote": false, + "trailingComma": false + }, + "generator": { + "typePrefix": "MyAPI", + "includeExamples": true + } +} +``` + +### 2. Section dans `package.json` + +Ou ajoutez une section `o2ts` dans votre `package.json` : + +```json +{ + "name": "votre-projet", + "version": "1.0.0", + "o2ts": { + "format": { + "semi": true, + "singleQuote": false + }, + "generator": { + "typePrefix": "MyAPI" + } + } +} +``` + +### Options de configuration disponibles + +#### Format du code généré + +| Option | Type | Défaut | Description | +|--------|------|--------|-------------| +| `format.semi` | `boolean` | `false` | Ajouter des points-virgules à la fin des lignes | +| `format.tabWidth` | `number` | `2` | Largeur d'indentation | +| `format.singleQuote` | `boolean` | `true` | Utiliser des guillemets simples au lieu de doubles | +| `format.trailingComma` | `boolean` | `true` | Ajouter des virgules finales | + +#### Options du générateur + +| Option | Type | Défaut | Description | +|--------|------|--------|-------------| +| `generator.typePrefix` | `string` | `"API"` | Préfixe pour les types générés | +| `generator.includeExamples` | `boolean` | `false` | Inclure les exemples de requête dans les commentaires | +| `generator.includeRequestBodies` | `boolean` | `true` | Générer des types pour les corps de requête | +| `generator.includeResponses` | `boolean` | `true` | Générer des types pour les réponses | + +## Exemples d'utilisation + +### Exemple basique + +Voici un exemple simple d'utilisation des types générés : + +```typescript import type { APIPaths, APIRequests, APIResponse } from './openapi' export async function fetchAPI< Path extends APIPaths, Options extends APIRequests -> (path: Path, options: Options): Promise> { - // Your code here +>(path: Path, options: Options): Promise> { + // Votre code ici } ``` -You can find more implementations in the [examples directory](./examples). +### Exemple avec Axios + +```typescript +import axios from 'axios' +import type { APIPaths, APIRequest, APIResponse } from './openapi' + +export async function apiCall< + Path extends APIPaths, + Method extends string | undefined = undefined +>( + path: Path, + options?: APIRequest +): Promise> { + const method = (options?.method ?? 'get').toLowerCase() + const config = { + params: options?.query, + data: options?.body + } + + const response = await axios.request({ + url: path, + method, + ...config + }) + + return response.data +} + +// Utilisation +const users = await apiCall('/users') +const user = await apiCall('/users/{id}', { + method: 'get', + urlParams: { id: 123 } +}) +const newUser = await apiCall('/users', { + method: 'post', + body: { name: 'John' } +}) +``` + +### Exemple avec React Query + +```typescript +import { useQuery } from 'react-query' +import type { APIPaths, APIRequest, APIResponse } from './openapi' + +// Fonction API de base +const apiCall = async< + Path extends APIPaths, + Method extends string | undefined = undefined +>( + path: Path, + options?: APIRequest +): Promise> => { + // Implémentation de l'appel API +} + +// Hook personnalisé pour React Query +export function useApiQuery< + Path extends APIPaths, + Method extends 'get' | undefined = 'get' +>( + path: Path, + options?: APIRequest, + queryOptions?: any +) { + return useQuery( + [path, options], + () => apiCall(path, { method: 'get', ...options } as any), + queryOptions + ) +} + +// Utilisation +function UserComponent({ userId }: { userId: number }) { + const { data, isLoading } = useApiQuery('/users/{id}', { + urlParams: { id: userId } + }) + + if (isLoading) return
Loading...
+ + return
{data.name}
+} +``` + +## Types générés + +L'outil génère les types suivants : + +- `APIPaths`: Union des chemins d'API disponibles +- `APIRequests`: Type pour les options de requête pour un chemin donné +- `APIMethods`: Union des méthodes HTTP disponibles pour un chemin donné +- `APIRequest`: Type pour une requête complète avec une méthode spécifique +- `APIResponse`: Type pour la réponse d'une requête avec une méthode spécifique + +D'autres types générés incluent : +- `APISchemas`: Types pour tous les schémas définis dans les `components.schemas` +- `APIParameters`: Types pour tous les paramètres définis dans les `components.parameters` +- `APIResponses`: Types pour toutes les réponses définies dans les `components.responses` + +## Contribuer + +Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request sur [le dépôt GitHub](https://github.com/Grafikart/OpenApiToTS). + +## Licence +ISC diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..15d6713 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,345 @@ +{ + "name": "@grafikart/o2ts", + "version": "0.1.15", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@grafikart/o2ts", + "version": "0.1.15", + "license": "ISC", + "dependencies": { + "@readme/openapi-parser": "^2.5.0", + "prettier": "^3.2.5" + }, + "bin": { + "o2ts": "dist/generator.js" + }, + "devDependencies": { + "@types/bun": "^1.0.12", + "@types/node": "^20.12.3", + "openapi-types": "^12.1.3", + "typescript": "^5.4.3" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@humanwhocodes/momoa": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", + "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@readme/better-ajv-errors": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@readme/better-ajv-errors/-/better-ajv-errors-2.3.2.tgz", + "integrity": "sha512-T4GGnRAlY3C339NhoUpgJJFsMYko9vIgFAlhgV+/vEGFw66qEY4a4TRJIAZBcX/qT1pq5DvXSme+SQODHOoBrw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/runtime": "^7.22.5", + "@humanwhocodes/momoa": "^2.0.3", + "jsonpointer": "^5.0.0", + "leven": "^3.1.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ajv": "4.11.8 - 8" + } + }, + "node_modules/@readme/json-schema-ref-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@readme/json-schema-ref-parser/-/json-schema-ref-parser-1.2.1.tgz", + "integrity": "sha512-FKCnFnpKklBPu8atyXqmSRBPSYlZLdcdbIilX19y0vVFiVthqKV9SQp4GZ8L4rOqSVmjn14uZ4Ono5tZKMr1SQ==", + "deprecated": "This package is no longer maintained. Please use `@apidevtools/json-schema-ref-parser` instead.", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.12", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@readme/openapi-parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@readme/openapi-parser/-/openapi-parser-2.7.0.tgz", + "integrity": "sha512-P8WSr8WTOxilnT89tcCRKWYsG/II4sAwt1a/DIWub8xTtkrG9cCBBy/IUcvc5X8oGWN82MwcTA3uEkDrXZd/7A==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "@readme/better-ajv-errors": "^2.0.0", + "@readme/json-schema-ref-parser": "^1.2.0", + "@readme/openapi-schemas": "^3.1.0", + "ajv": "^8.12.0", + "ajv-draft-04": "^1.0.0", + "call-me-maybe": "^1.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@readme/openapi-schemas": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz", + "integrity": "sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/bun": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.14.tgz", + "integrity": "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.14" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.17.50", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", + "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "license": "MIT", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/bun-types": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.14.tgz", + "integrity": "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/src/SchemaParser.ts b/src/SchemaParser.ts index a59e6d4..850f11f 100644 --- a/src/SchemaParser.ts +++ b/src/SchemaParser.ts @@ -21,9 +21,43 @@ type Response = OpenAPIV3_1.ResponseObject type Schema = IJsonSchema | OpenAPIV3_1.BaseSchemaObject | IJsonSchema[] | OpenAPIV3.BaseSchemaObject type RequestBody = OpenAPIV3_1.RequestBodyObject +export interface SchemaParserOptions { + /** + * Préfixe pour les types générés + * @default "API" + */ + typePrefix?: string + /** + * Inclure les exemples de requête dans les commentaires + * @default false + */ + includeExamples?: boolean + /** + * Générer des types pour les corps de requête + * @default true + */ + includeRequestBodies?: boolean + /** + * Générer des types pour les réponses + * @default true + */ + includeResponses?: boolean +} + export class SchemaParser { + // Options par défaut qui seront utilisées si aucune n'est fournie + private options: SchemaParserOptions = { + typePrefix: 'API', + includeExamples: false, + includeRequestBodies: true, + includeResponses: true + } - constructor (private document: Document) { + constructor (private document: Document, options?: SchemaParserOptions) { + // Si des options sont fournies, les fusionner avec les options par défaut + if (options) { + this.options = { ...this.options, ...options } + } } convertToCode (): string { @@ -35,7 +69,7 @@ export class SchemaParser { if (group === 'securitySchemes') { continue } - const typeName = 'API' + capitalize(group) + const typeName = this.options.typePrefix + capitalize(group) const components = new ObjectType() for (const [schemaName, componentSchema] of Object.entries(schemas)) { components.addProperty(schemaName, this.itemToNode(componentSchema?.['content']?.['application/json']?.['schema'] ?? componentSchema)) @@ -60,13 +94,13 @@ export class SchemaParser { apiEndpoint.addProperty('requests', requests) apiEndpoints.addProperty(endpoint, apiEndpoint) } - types.push(['APIEndpoints', apiEndpoints]) + types.push([this.options.typePrefix + 'Endpoints', apiEndpoints]) return types.map(([name, type]) => `export type ${name} = ${type.toString()}`).join('\n\n') + ` -export type APIPaths = keyof APIEndpoints +export type APIPaths = keyof ${this.options.typePrefix}Endpoints -export type APIRequests = APIEndpoints[T]["requests"] +export type APIRequests = ${this.options.typePrefix}Endpoints[T]["requests"] export type APIMethods = NonNullable["method"]> @@ -84,8 +118,8 @@ type DefaultToGet = T extends string export type APIResponse< T extends APIPaths, M extends string | undefined -> = DefaultToGet extends keyof APIEndpoints[T]["responses"] - ? APIEndpoints[T]["responses"][DefaultToGet] +> = DefaultToGet extends keyof ${this.options.typePrefix}Endpoints[T]["responses"] + ? ${this.options.typePrefix}Endpoints[T]["responses"][DefaultToGet] : never` } @@ -236,7 +270,7 @@ export type APIResponse< if ('$ref' in item && item.$ref) { const [_, __, group, name] = item.$ref.split('/') - return new SimpleType(`API${capitalize(group)}['${name}']`).with(infos) + return new SimpleType(`${this.options.typePrefix}${capitalize(group)}['${name}']`).with(infos) } if ('anyOf' in item && item.anyOf) { diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..71d59d7 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,123 @@ +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' + +export interface O2TSConfig { + /** + * Format du code généré + */ + format?: { + /** + * Ajouter des points-virgules à la fin des lignes + * @default false + */ + semi?: boolean + /** + * Largeur d'indentation + * @default 2 + */ + tabWidth?: number + /** + * Utiliser des simples quotes au lieu de doubles + * @default true + */ + singleQuote?: boolean + /** + * Ajouter des virgules finales + * @default true + */ + trailingComma?: boolean + } + /** + * Options du générateur + */ + generator?: { + /** + * Préfixe pour les types générés + * @default "API" + */ + typePrefix?: string + /** + * Inclure les exemples de requête dans les commentaires + * @default false + */ + includeExamples?: boolean + /** + * Générer des types pour les corps de requête + * @default true + */ + includeRequestBodies?: boolean + /** + * Générer des types pour les réponses + * @default true + */ + includeResponses?: boolean + } +} + +export const DEFAULT_CONFIG: O2TSConfig = { + format: { + semi: false, + tabWidth: 2, + singleQuote: true, + trailingComma: true + }, + generator: { + typePrefix: 'API', + includeExamples: false, + includeRequestBodies: true, + includeResponses: true + } +} + +/** + * Récupère la configuration de o2ts + * + * Cherche dans l'ordre: + * 1. Fichier .o2tsrc.json à la racine du projet + * 2. Section "o2ts" dans package.json + * 3. Configuration par défaut + */ +export function getConfig(basePath: string = process.cwd()): O2TSConfig { + // Chercher un fichier de configuration .o2tsrc.json + const configPath = path.join(basePath, '.o2tsrc.json') + if (existsSync(configPath)) { + try { + const configContent = readFileSync(configPath, 'utf-8') + return mergeConfig(JSON.parse(configContent)) + } catch (error) { + console.warn(`Erreur lors de la lecture du fichier .o2tsrc.json: ${error}`) + } + } + + // Chercher une section o2ts dans package.json + const packagePath = path.join(basePath, 'package.json') + if (existsSync(packagePath)) { + try { + const packageContent = readFileSync(packagePath, 'utf-8') + const packageData = JSON.parse(packageContent) + if (packageData.o2ts && typeof packageData.o2ts === 'object') { + return mergeConfig(packageData.o2ts) + } + } catch (error) { + console.warn(`Erreur lors de la lecture de la configuration dans package.json: ${error}`) + } + } + + return DEFAULT_CONFIG +} + +/** + * Fusionne la configuration utilisateur avec la configuration par défaut + */ +function mergeConfig(userConfig: Partial): O2TSConfig { + return { + format: { + ...DEFAULT_CONFIG.format, + ...userConfig.format + }, + generator: { + ...DEFAULT_CONFIG.generator, + ...userConfig.generator + } + } +} diff --git a/src/generator.ts b/src/generator.ts index 6b71247..6f2f7de 100644 --- a/src/generator.ts +++ b/src/generator.ts @@ -5,15 +5,31 @@ import type { OpenAPIV3_1 } from 'openapi-types' import { format } from 'prettier' import { writeFileSync } from 'node:fs' import { SchemaParser } from './SchemaParser.js' +import { getConfig } from './config.js' try { const args = process.argv.slice(2) const yamlFile = args[0] ?? './openapi.yml' - const tsFile = args[1] ? args[1] : args[0].replace('.yml', '.ts').replace('.yaml', '.ts') + const tsFile = args[1] ? args[1] : yamlFile.replace(/\.ya?ml$/, '.ts') + + // Récupérer la configuration + const projectRoot = process.cwd() + const config = getConfig(projectRoot) + const apiSchema = await OpenAPI.parse(yamlFile) as OpenAPIV3_1.Document; - const options = new SchemaParser(apiSchema) + const options = new SchemaParser(apiSchema, config.generator) const code = options.convertToCode() - writeFileSync(tsFile, await format(code, { semi: false, parser: "typescript" })) + + // Utiliser les options de format de la configuration + const formatOptions = { + parser: "typescript", + semi: config.format?.semi ?? false, + tabWidth: config.format?.tabWidth ?? 2, + singleQuote: config.format?.singleQuote ?? true, + trailingComma: (config.format?.trailingComma === true ? "all" : "none") as "all" | "none" | "es5" + } + + writeFileSync(tsFile, await format(code, formatOptions)) process.stdout.write(`${tsFile} created with success`) process.exit(0) } diff --git a/tests/generator.test.ts b/tests/generator.test.ts index a800849..0448ad6 100644 --- a/tests/generator.test.ts +++ b/tests/generator.test.ts @@ -9,14 +9,19 @@ import {format} from 'prettier' const testDir = dirname(fileURLToPath(import.meta.url)) +// Fonction pour normaliser les sauts de ligne (convertir CRLF et LF en LF) +const normalizeLine = (text: string) => text.replace(/\r\n/g, "\n"); + const shouldMatchFiles = async (file: string) => { const apiSchema = await OpenAPI.parse(join(testDir, `${file}.yml`)) as OpenAPIV3_1.Document; + // Utiliser la nouvelle signature de SchemaParser en passant null pour les options const options = new SchemaParser(apiSchema) const code = options.convertToCode() const got = await format(code, {semi: false, parser: "typescript"}) const fixturePath = join(testDir, `${file}.ts`) const want = await Bun.file(fixturePath).text() - expect(got).toBe(want) + // Normaliser les sauts de ligne avant de comparer + expect(normalizeLine(got)).toBe(normalizeLine(want)) } test('it should work with OpenAPIV3.1', () => {