diff --git a/.env_example b/.env_example new file mode 100644 index 0000000..5d6da56 --- /dev/null +++ b/.env_example @@ -0,0 +1,5 @@ +SPEC_URL= +SPEC_OUT= +SPEC_STRATEGY= +SPEC_USERNAME= +SPEC_PASSWORD= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1e332f7..a0da989 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ count.txt src/gen # Bun .bun-cache -.bun-version \ No newline at end of file +.bun-version +spec/ \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 13fe28e..86e7bd5 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/eslint.config.js b/eslint.config.js index 4ff07a3..d17eb0b 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,5 +34,6 @@ export default antfu({ "style/indent-binary-ops": "off", "style/indent": "off", "style/comma-dangle": "off", + "style/quote-props": "off", }, }); diff --git a/package.json b/package.json index 688de3d..f4e15c8 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "postgenerate:kubb": "find src/gen/types -name '*.ts' -exec sed -i '' 's/\\[key: string\\]: string\\[\\] | null;/[key: string]: string[] | undefined;/g' {} \\;", "generate": "bun run generate:kubb && bun run generate:routes", "check:routes": "bun run generate:routes && git diff --exit-code src/routes/routes.gen.ts", - "download-spec": "bun scripts/dow-spec.mjs --url https://petstore3.swagger.io/api/v3/openapi.json --out ./spec/openapi.yaml" + "download-spec": "bun scripts/dow-spec.mjs --out ./spec/openapi.yaml" }, "dependencies": { "@hookform/resolvers": "^5.2.1", @@ -59,6 +59,7 @@ "@vitejs/plugin-react": "^4.3.4", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", + "cheerio": "^1.1.2", "eslint": "^9.34.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", diff --git a/scripts/create-spec.mjs b/scripts/create-spec.mjs new file mode 100644 index 0000000..b60a502 --- /dev/null +++ b/scripts/create-spec.mjs @@ -0,0 +1,49 @@ +import { Buffer } from "node:buffer"; +import fs from "node:fs/promises"; +import yaml from "js-yaml"; + +export class CreateSpec { + _url = ""; + _strategy = null; + _outPath = ""; + _outDir = ""; + + constructor({ url, strategy, outDir, outPath }) { + this._url = url; + this._outDir = outDir; + this._outPath = outPath; + this._strategy = strategy; + } + + async generate() { + const text = await this._strategy.download(this._url); + + await this._createFile(text); + } + + async _createFile(text) { + let yamlText; + const trimmed = text.trim(); + const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("["); + + if (looksLikeJson) { + try { + const obj = JSON.parse(text); + yamlText = yaml.dump(obj, { noRefs: true }); + console.log("✅ JSON -> converted to YAML"); + } catch { + yamlText = text; + console.log("⚠️ JSON parse failed -> saving raw"); + } + } else { + yamlText = text; + console.log("✅ YAML -> saving as-is"); + } + + await fs.mkdir(this._outDir, { recursive: true }); + await fs.writeFile(this._outPath, yamlText, "utf8"); + console.log( + `💾 saved ${Buffer.byteLength(yamlText, "utf8")} bytes to ${this._outPath} (overwritten if existed)`, + ); + } +} diff --git a/scripts/dow-spec.mjs b/scripts/dow-spec.mjs index bf27cf2..892798f 100644 --- a/scripts/dow-spec.mjs +++ b/scripts/dow-spec.mjs @@ -7,12 +7,17 @@ * bun scripts/download-spec.mjs --url https://example.com/openapi.json --out ./spec/my.yaml */ -import { Buffer } from "node:buffer"; -import fs from "node:fs/promises"; import path from "node:path"; import process from "node:process"; -import yaml from "js-yaml"; import minimist from "minimist"; +import { CreateSpec } from "./create-spec.mjs"; +import { DjangoStrategy } from "./strategies/djangoStrategy.mjs"; +import { PublicStrategy } from "./strategies/publicStrategy.mjs"; + +const strategies = { + Public: PublicStrategy, + Django: DjangoStrategy, +}; const args = minimist(process.argv.slice(2), { string: ["url", "out"], @@ -20,6 +25,7 @@ const args = minimist(process.argv.slice(2), { }); const url = args.url || process.env.SPEC_URL; + if (!url) { console.error( "❌ SPEC URL not provided. Use --url, SPEC_URL env, or package.json specUrl", @@ -34,33 +40,29 @@ const outDir = path.dirname(outPath); console.log(`[spec] URL: ${url}`); console.log(`[spec] output: ${outPath}`); -// 1️⃣ Скачиваем spec -const res = await fetch(url); -if (!res.ok) - throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`); -const text = await res.text(); +const strategyArg = args.strategy || process.env.SPEC_STRATEGY; + +if (!strategyArg) { + console.error( + "❌ SPEC STRATEGY not provided. Use --strategy, or SPEC_STRATEGY env", + ); + process.exit(1); +} + +const strategy = new strategies[strategyArg](); -// 2️⃣ Конвертируем JSON -> YAML если нужно -let yamlText; -const trimmed = text.trim(); -const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("["); -if (looksLikeJson) { - try { - const obj = JSON.parse(text); - yamlText = yaml.dump(obj, { noRefs: true }); - console.log("✅ JSON -> converted to YAML"); - } catch { - yamlText = text; - console.log("⚠️ JSON parse failed -> saving raw"); - } -} else { - yamlText = text; - console.log("✅ YAML -> saving as-is"); +if (!strategy) { + console.error("❌ SPEC STRATEGY is incorrect."); + process.exit(1); } -// 3️⃣ Создаём директорию (если нет) и перезаписываем файл -await fs.mkdir(outDir, { recursive: true }); -await fs.writeFile(outPath, yamlText, "utf8"); -console.log( - `💾 saved ${Buffer.byteLength(yamlText, "utf8")} bytes to ${outPath} (overwritten if existed)`, -); +console.log(`[spec] using strategy: ${strategyArg}`); + +const constructorSpec = new CreateSpec({ + url, + outDir, + outPath, + strategy, +}); + +await constructorSpec.generate(); diff --git a/scripts/strategies/baseStrategy.mjs b/scripts/strategies/baseStrategy.mjs new file mode 100644 index 0000000..2dd7e7e --- /dev/null +++ b/scripts/strategies/baseStrategy.mjs @@ -0,0 +1,4 @@ +export class BaseStrategy { + // url + async download(_) {} +} diff --git a/scripts/strategies/djangoStrategy.mjs b/scripts/strategies/djangoStrategy.mjs new file mode 100644 index 0000000..4ec8b16 --- /dev/null +++ b/scripts/strategies/djangoStrategy.mjs @@ -0,0 +1,88 @@ +import process from "node:process"; +import * as cheerio from "cheerio"; +import { BaseStrategy } from "./baseStrategy.mjs"; + +export class DjangoStrategy extends BaseStrategy { + _getSetCookie(response, names) { + const setCookie = response.headers.get("set-cookie"); + const cookies = []; + + for (let i = 0; i < names.length; i++) { + const name = names[i]; + let cookie = null; + + if (setCookie) { + const regex = new RegExp(`${name}=([^;]+)`); + const match = setCookie.match(regex); + if (match) cookie = match[1]; + } + + if (!cookie) { + console.error(`❌ Не удалось достать ${name} из Set-Cookie`); + process.exit(1); + } + + cookies.push(cookie); + } + + return cookies; + } + + async download(url) { + await super.download(url); + + const response = await fetch(url); + + if (!response.ok) { + throw new Error( + `Failed to fetch: ${response.status} ${response.statusText}`, + ); + } + + const html = await response.text(); + const $ = cheerio.load(html); + + const csrfmiddlewaretoken = $("input[name=csrfmiddlewaretoken]").val(); + const next = $("input[name=next]").val(); + const submit = $("input[name=submit]").val(); + + const csrftoken = this._getSetCookie(response, ["csrftoken"])[0]; + + const urlObj = new URL(response.url); + urlObj.search = ""; + + const urlWithoutSearch = urlObj.toString(); + const payload = new URLSearchParams({ + username: process.env.SPEC_USERNAME, + password: process.env.SPEC_PASSWORD, + next, + submit, + csrfmiddlewaretoken, + }); + + const loginResponse = await fetch(urlWithoutSearch, { + body: payload, + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Cookie: `csrftoken=${csrftoken}`, + "X-CSRFToken": csrftoken, + Referer: response.url, + Origin: urlObj.origin, + }, + method: "POST", + redirect: "manual", + }); + + const sessionid = this._getSetCookie(loginResponse, ["sessionid"]); + const redirectURL = loginResponse.headers.get("location"); + const nextUrl = new URL(redirectURL, urlObj.origin); + + const docResponse = await fetch(nextUrl, { + headers: { + Cookie: `csrftoken=${csrftoken}; sessionid=${sessionid}`, + }, + }); + + return await docResponse.text(); + } +} diff --git a/scripts/strategies/publicStrategy.mjs b/scripts/strategies/publicStrategy.mjs new file mode 100644 index 0000000..2dbf2b6 --- /dev/null +++ b/scripts/strategies/publicStrategy.mjs @@ -0,0 +1,14 @@ +import { BaseStrategy } from "./baseStrategy.mjs"; + +export class PublicStrategy extends BaseStrategy { + async download(url) { + await super.download(url); + + const res = await fetch(url); + + if (!res.ok) + throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`); + + return await res.text(); + } +} diff --git a/spec/openapi.yaml b/spec/openapi.yaml deleted file mode 100644 index afe5401..0000000 --- a/spec/openapi.yaml +++ /dev/null @@ -1,845 +0,0 @@ -openapi: 3.0.4 -info: - title: Swagger Petstore - OpenAPI 3.0 - description: >- - This is a sample Pet Store Server based on the OpenAPI 3.0 specification. - You can find out more about - - Swagger at [https://swagger.io](https://swagger.io). In the third iteration - of the pet store, we've switched to the design first approach! - - You can now help us improve the API whether it's by making changes to the - definition itself or to the code. - - That way, with time, we can improve the API in general, and expose some of - the new features in OAS3. - - - Some useful links: - - - [The Pet Store - repository](https://github.com/swagger-api/swagger-petstore) - - - [The source API definition for the Pet - Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) - termsOfService: https://swagger.io/terms/ - contact: - email: apiteam@swagger.io - license: - name: Apache 2.0 - url: https://www.apache.org/licenses/LICENSE-2.0.html - version: 1.0.27 -externalDocs: - description: Find out more about Swagger - url: https://swagger.io -servers: - - url: /api/v3 -tags: - - name: pet - description: Everything about your Pets - externalDocs: - description: Find out more - url: https://swagger.io - - name: store - description: Access to Petstore orders - externalDocs: - description: Find out more about our store - url: https://swagger.io - - name: user - description: Operations about user -paths: - /pet: - put: - tags: - - pet - summary: Update an existing pet. - description: Update an existing pet by Id. - operationId: updatePet - requestBody: - description: Update an existent pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Pet' - required: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid ID supplied - '404': - description: Pet not found - '422': - description: Validation exception - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - post: - tags: - - pet - summary: Add a new pet to the store. - description: Add a new pet to the store. - operationId: addPet - requestBody: - description: Create a new pet in the store - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Pet' - required: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid input - '422': - description: Validation exception - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - /pet/findByStatus: - get: - tags: - - pet - summary: Finds Pets by status. - description: Multiple status values can be provided with comma separated strings. - operationId: findPetsByStatus - parameters: - - name: status - in: query - description: Status values that need to be considered for filter - required: true - explode: true - schema: - type: string - default: available - enum: - - available - - pending - - sold - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid status value - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - /pet/findByTags: - get: - tags: - - pet - summary: Finds Pets by tags. - description: >- - Multiple tags can be provided with comma separated strings. Use tag1, - tag2, tag3 for testing. - operationId: findPetsByTags - parameters: - - name: tags - in: query - description: Tags to filter by - required: true - explode: true - schema: - type: array - items: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - type: array - items: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid tag value - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - /pet/{petId}: - get: - tags: - - pet - summary: Find pet by ID. - description: Returns a single pet. - operationId: getPetById - parameters: - - name: petId - in: path - description: ID of pet to return - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid ID supplied - '404': - description: Pet not found - default: - description: Unexpected error - security: - - api_key: [] - - petstore_auth: - - write:pets - - read:pets - post: - tags: - - pet - summary: Updates a pet in the store with form data. - description: Updates a pet resource based on the form data. - operationId: updatePetWithForm - parameters: - - name: petId - in: path - description: ID of pet that needs to be updated - required: true - schema: - type: integer - format: int64 - - name: name - in: query - description: Name of pet that needs to be updated - schema: - type: string - - name: status - in: query - description: Status of pet that needs to be updated - schema: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - '400': - description: Invalid input - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - delete: - tags: - - pet - summary: Deletes a pet. - description: Delete a pet. - operationId: deletePet - parameters: - - name: api_key - in: header - description: '' - required: false - schema: - type: string - - name: petId - in: path - description: Pet id to delete - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: Pet deleted - '400': - description: Invalid pet value - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - /pet/{petId}/uploadImage: - post: - tags: - - pet - summary: Uploads an image. - description: Upload image of the pet. - operationId: uploadFile - parameters: - - name: petId - in: path - description: ID of pet to update - required: true - schema: - type: integer - format: int64 - - name: additionalMetadata - in: query - description: Additional Metadata - required: false - schema: - type: string - requestBody: - content: - application/octet-stream: - schema: - type: string - format: binary - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/ApiResponse' - '400': - description: No file uploaded - '404': - description: Pet not found - default: - description: Unexpected error - security: - - petstore_auth: - - write:pets - - read:pets - /store/inventory: - get: - tags: - - store - summary: Returns pet inventories by status. - description: Returns a map of status codes to quantities. - operationId: getInventory - responses: - '200': - description: successful operation - content: - application/json: - schema: - type: object - additionalProperties: - type: integer - format: int32 - default: - description: Unexpected error - security: - - api_key: [] - /store/order: - post: - tags: - - store - summary: Place an order for a pet. - description: Place a new order in the store. - operationId: placeOrder - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - application/xml: - schema: - $ref: '#/components/schemas/Order' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Order' - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - '400': - description: Invalid input - '422': - description: Validation exception - default: - description: Unexpected error - /store/order/{orderId}: - get: - tags: - - store - summary: Find purchase order by ID. - description: >- - For valid response try integer IDs with value <= 5 or > 10. Other values - will generate exceptions. - operationId: getOrderById - parameters: - - name: orderId - in: path - description: ID of order that needs to be fetched - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Order' - application/xml: - schema: - $ref: '#/components/schemas/Order' - '400': - description: Invalid ID supplied - '404': - description: Order not found - default: - description: Unexpected error - delete: - tags: - - store - summary: Delete purchase order by identifier. - description: >- - For valid response try integer IDs with value < 1000. Anything above - 1000 or non-integers will generate API errors. - operationId: deleteOrder - parameters: - - name: orderId - in: path - description: ID of the order that needs to be deleted - required: true - schema: - type: integer - format: int64 - responses: - '200': - description: order deleted - '400': - description: Invalid ID supplied - '404': - description: Order not found - default: - description: Unexpected error - /user: - post: - tags: - - user - summary: Create user. - description: This can only be done by the logged in user. - operationId: createUser - requestBody: - description: Created user object - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/xml: - schema: - $ref: '#/components/schemas/User' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/User' - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/xml: - schema: - $ref: '#/components/schemas/User' - default: - description: Unexpected error - /user/createWithList: - post: - tags: - - user - summary: Creates list of users with given input array. - description: Creates list of users with given input array. - operationId: createUsersWithListInput - requestBody: - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/User' - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/xml: - schema: - $ref: '#/components/schemas/User' - default: - description: Unexpected error - /user/login: - get: - tags: - - user - summary: Logs user into the system. - description: Log into the system. - operationId: loginUser - parameters: - - name: username - in: query - description: The user name for login - required: false - schema: - type: string - - name: password - in: query - description: The password for login in clear text - required: false - schema: - type: string - responses: - '200': - description: successful operation - headers: - X-Rate-Limit: - description: calls per hour allowed by the user - schema: - type: integer - format: int32 - X-Expires-After: - description: date in UTC when token expires - schema: - type: string - format: date-time - content: - application/xml: - schema: - type: string - application/json: - schema: - type: string - '400': - description: Invalid username/password supplied - default: - description: Unexpected error - /user/logout: - get: - tags: - - user - summary: Logs out current logged in user session. - description: Log user out of the system. - operationId: logoutUser - parameters: [] - responses: - '200': - description: successful operation - default: - description: Unexpected error - /user/{username}: - get: - tags: - - user - summary: Get user by user name. - description: Get user detail based on username. - operationId: getUserByName - parameters: - - name: username - in: path - description: The name that needs to be fetched. Use user1 for testing - required: true - schema: - type: string - responses: - '200': - description: successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/xml: - schema: - $ref: '#/components/schemas/User' - '400': - description: Invalid username supplied - '404': - description: User not found - default: - description: Unexpected error - put: - tags: - - user - summary: Update user resource. - description: This can only be done by the logged in user. - operationId: updateUser - parameters: - - name: username - in: path - description: name that need to be deleted - required: true - schema: - type: string - requestBody: - description: Update an existent user in the store - content: - application/json: - schema: - $ref: '#/components/schemas/User' - application/xml: - schema: - $ref: '#/components/schemas/User' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/User' - responses: - '200': - description: successful operation - '400': - description: bad request - '404': - description: user not found - default: - description: Unexpected error - delete: - tags: - - user - summary: Delete user resource. - description: This can only be done by the logged in user. - operationId: deleteUser - parameters: - - name: username - in: path - description: The name that needs to be deleted - required: true - schema: - type: string - responses: - '200': - description: User deleted - '400': - description: Invalid username supplied - '404': - description: User not found - default: - description: Unexpected error -components: - schemas: - Order: - type: object - properties: - id: - type: integer - format: int64 - example: 10 - petId: - type: integer - format: int64 - example: 198772 - quantity: - type: integer - format: int32 - example: 7 - shipDate: - type: string - format: date-time - status: - type: string - description: Order Status - example: approved - enum: - - placed - - approved - - delivered - complete: - type: boolean - xml: - name: order - Category: - type: object - properties: - id: - type: integer - format: int64 - example: 1 - name: - type: string - example: Dogs - xml: - name: category - User: - type: object - properties: - id: - type: integer - format: int64 - example: 10 - username: - type: string - example: theUser - firstName: - type: string - example: John - lastName: - type: string - example: James - email: - type: string - example: john@email.com - password: - type: string - example: '12345' - phone: - type: string - example: '12345' - userStatus: - type: integer - description: User Status - format: int32 - example: 1 - xml: - name: user - Tag: - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - xml: - name: tag - Pet: - required: - - name - - photoUrls - type: object - properties: - id: - type: integer - format: int64 - example: 10 - name: - type: string - example: doggie - category: - $ref: '#/components/schemas/Category' - photoUrls: - type: array - xml: - wrapped: true - items: - type: string - xml: - name: photoUrl - tags: - type: array - xml: - wrapped: true - items: - $ref: '#/components/schemas/Tag' - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - xml: - name: pet - ApiResponse: - type: object - properties: - code: - type: integer - format: int32 - type: - type: string - message: - type: string - xml: - name: '##default' - requestBodies: - Pet: - description: Pet object that needs to be added to the store - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - UserArray: - description: List of user object - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/User' - securitySchemes: - petstore_auth: - type: oauth2 - flows: - implicit: - authorizationUrl: https://petstore3.swagger.io/oauth/authorize - scopes: - write:pets: modify pets in your account - read:pets: read your pets - api_key: - type: apiKey - name: api_key - in: header