diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..20d1f18 --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,40 @@ +name: ✅ Run Checks + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: checks-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + checks: + name: Lint, Build & Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Run Lint + run: npm run lint + + - name: Run Build + run: npm run build + + - name: Run Tests + run: npm test diff --git a/.github/workflows/pr-validate.yaml b/.github/workflows/pr-validate.yaml index 7828c1b..c53c534 100644 --- a/.github/workflows/pr-validate.yaml +++ b/.github/workflows/pr-validate.yaml @@ -16,7 +16,7 @@ jobs: name: 🏷️ Validate PR Title runs-on: ubuntu-latest steps: - - uses: amannn/action-semantic-pull-request@v5 + - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -28,12 +28,5 @@ jobs: subjectPatternError: | The subject "{subject}" found in the pull request title "{title}" didn't match the configured pattern. - - - lowercase types (chore, feat, fix) - - # If the PR only contains a single commit, the action will validate that - # it matches the configured pattern. - validateSingleCommit: true - # Related to `validateSingleCommit` you can opt-in to validate that the PR - # title matches a single commit to avoid confusion. - validateSingleCommitMatchesPrTitle: true \ No newline at end of file + + - lowercase types (chore, feat, fix) \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b61b889..2e171f4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,4 @@ -name: 🕵🏻‍♂️ Release Check +name: 🚀 Release Please on: push: @@ -13,33 +13,29 @@ jobs: release-please: runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v3 + - uses: googleapis/release-please-action@v5 id: publish - with: - release-type: node - package-name: '@pablaber/openapi-typescript-types' - changelog-types: '[{"type":"feat","section":"Features","hidden":false},{"type":"fix","section":"Bug Fixes","hidden":false},{"type":"chore","section":"Miscellaneous","hidden":false}]' - - uses: actions/checkout@v3 + - uses: actions/checkout@v6 if: ${{ steps.publish.outputs.release_created }} - name: Setup Node - uses: actions/setup-node@v3 if: ${{ steps.publish.outputs.release_created }} + uses: actions/setup-node@v6 with: node-version-file: .nvmrc - name: Install Dependencies - run: npm install if: ${{ steps.publish.outputs.release_created }} - + run: npm ci + - name: Run Build - run: npm run build if: ${{ steps.publish.outputs.release_created }} + run: npm run build - name: Publish to NPM + if: ${{ steps.publish.outputs.release_created }} run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> .npmrc && npm publish env: - NPM_TOKEN: ${{secrets.NPM_TOKEN}} - if: ${{ steps.publish.outputs.release_created }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39b8fbb..9c3da29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules .local dist -out \ No newline at end of file +out +test/output \ No newline at end of file diff --git a/.nvmrc b/.nvmrc index 2edeafb..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +24 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..0477999 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.3.2" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c2508e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,264 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. + +## Project overview + +`openapi-typescript-types` (binary: `ott`) is a Node.js CLI that reads an +OpenAPI 3.x document and emits a single TypeScript file containing types for +the document's component schemas, request bodies, and 2xx response bodies. + +It is published to npm as `openapi-typescript-types` and built from +TypeScript sources in `lib/` and `main.ts` to `dist/` via `tsc`. + +## Repository layout + +- `main.ts` — CLI entrypoint (`#! /usr/bin/env node`). Wires the pipeline: + options → parse → build → write. +- `lib/options.ts` — Parses CLI flags (via `commander`) and YAML config files + (via `js-yaml`) into a `ProgramOptions` object. Either `--config ` or + the explicit `--input`/`--output` flags are required. +- `lib/openapi-parser.ts` — Wraps `@readme/openapi-parser` to validate and + dereference the OpenAPI document. +- `lib/builder/type-builder.ts` — Core type-emission logic. Walks the + dereferenced schema recursively and produces TypeScript source strings. + Handles `oneOf`/`anyOf`/`allOf`, enums, `nullable`, arrays, + `additionalProperties`, and nested objects. +- `lib/builder/path-builder-utils.ts` — Builds a synthetic `PropertiesMap` + for path operations: each entry is keyed by a generated name like + `GetPetByIdOkResponse` or `PostPetRequestBody`. Only 2xx responses with an + `application/json` schema are emitted. +- `lib/writer/file-writer.ts` — Writes the generated types to disk, prepends + a generated-file banner and the shared `Nullable` helper. +- `lib/types.ts` — Internal TypeScript types describing the OpenAPI subset + this tool understands plus `ProgramOptions` and `YamlConfig`. +- `lib/utils.ts` — Small helpers (`matchesAny` glob check, status-code text, + capitalize). +- `lib/logger.ts` — `winston` console logger; level is set to `debug` when + `--debug` is passed, otherwise `info`. +- `lib/error.ts` — `logErrorAndExit` helper. +- `demo/petstore.yaml` — Sample OpenAPI doc used for ad-hoc smoke tests. +- `test/fixtures/*.yaml` — Test-suite fixtures exercising supported + OpenAPI constructs. +- `test/run.js` — Test runner. Generates types from each fixture and + type-checks the output with `tsc --strict`. +- `.github/workflows/` — `checks.yaml` runs lint + build on every PR; + `pr-validate.yaml` enforces Conventional Commit PR titles (`chore`, + `feat`, `fix`); `release.yaml` runs release-please on `main` and + publishes to npm when a release PR is merged. +- `release-please-config.json` — release-please configuration (changelog + sections for `feat`, `fix`, `chore`). + +## Development commands + +Node version is pinned in `.nvmrc` (Node 24). Install with `npm install`. + +- `npm run build` — Compile TypeScript to `dist/` per `tsconfig.json`. +- `npm run lint` — Run ESLint over the repo. +- `npm run create-executable` — Build then `npm i -g`, exposing `ott` + globally for local end-to-end testing. +- `npm test` — Run the fixture-based test suite via `test/run.js`. + Requires `dist/main.js` to exist (run `npm run build` first). + +The test suite generates types from every YAML in `test/fixtures/` and +type-checks the output with `tsc --noEmit --strict`. To add coverage, +drop another `*.yaml` in `test/fixtures/` — the runner picks it up +automatically. Output lands in `test/output/` (gitignored). + +For a bug fix tied to a new OpenAPI construct, prefer adding a case to +`test/fixtures/test-api.yaml` (or a new fixture) over a one-off manual +check. + +## CLI surface + +Flags (see `lib/options.ts` for the source of truth): + +- `-c, --config ` — YAML config file (preferred). When provided, all + other input/output flags are ignored. +- `-i, --input ` — OpenAPI document. +- `-o, --output ` — Destination `.ts` file. +- `--exclude-paths` — Skip path/operation types. +- `--exclude-schemas` — Skip component-schema types. Cannot be combined with + `--exclude-paths`. +- `-d, --debug` — Verbose logging. +- `-v, --version` — Print version from `package.json`. + +The YAML config schema (`YamlConfig` in `lib/types.ts`) supports +`paths`/`schemas` as either booleans or `{ include?: string[]; exclude?: +string[] }`. `include` takes precedence over `exclude`. Patterns are matched +with `minimatch` (full glob syntax). + +## Code style and conventions + +- TypeScript strict mode is on (`tsconfig.json`). ESLint uses + `typescript-eslint`'s `strict` configs — keep the codebase free of + warnings. +- Prettier config: `semi: true`, `singleQuote: true` (`.prettierrc.json`). +- Imports use ES module syntax. The project compiles to CommonJS (per + `tsconfig.json`); avoid runtime-only ESM features. +- Logger usage: import the default logger from `lib/logger`. Use + `logger.debug` for trace info, `logger.info` for user-facing progress, + `logger.warn` for skipped/unsupported schema constructs, and the + `logErrorAndExit` helper in `lib/error.ts` for fatal parse failures. +- Errors thrown in `main.ts` are caught and logged with `process.exit(1)`; + prefer throwing `Error` with a clear message over silent failures. +- Follow the patterns already in `type-builder.ts` for recursion: pass a + `currentPath` array down so warning messages can pinpoint the schema + location. + +## Generated output contract + +`writer/file-writer.ts` produces files with this shape: + +``` +// This file was generated by the openapi-to-typescript tool. +// Do not modify it by hand. + +// START TYPES BASE +type Nullable = T | null; +// END TYPES BASE + +export type Pet = { ... }; +export type GetPetByIdOkResponse = { ... }; +``` + +Keep the banner and `START TYPES BASE`/`END TYPES BASE` markers stable — +downstream consumers may rely on them. + +## Naming for generated path types + +Path operation names are built in `path-builder-utils.ts`: + +- Method is upper-cased and prepended (`Get`, `Post`, …). +- Path segments are PascalCased; `{id}`-style variables become `ById`. +- Response types are suffixed with the status text (`Ok` for 200, + `Created` for 201, the raw code otherwise) plus `Response`. +- Request bodies are suffixed with `RequestBody`. + +Example: `GET /pet/{petId}` 200 → `GetPetByPetIdOkResponse`. + +## Release process + +- Commits to `main` follow Conventional Commits (`feat:`, `fix:`, `chore:`). + PR titles are validated by `pr-validate.yaml`. +- `release.yaml` uses `release-please` (configured via + `release-please-config.json`) to open release PRs; merging one triggers + a build and `npm publish`. Do not bump the version in `package.json` + manually. + +## Pull request conventions + +When opening a PR (whether by hand or as an agent), follow the rules below +so that `pr-validate.yaml` passes and release-please produces a sensible +changelog entry. + +### 1. Conventional Commit PR title + +The PR title is the source of truth for release-please. It must match: + +``` +[optional scope][!]: +``` + +Only these `` prefixes are accepted (enforced by +`amannn/action-semantic-pull-request` in `pr-validate.yaml`, and the only +ones that produce a release-please changelog entry per +`release-please-config.json`): + +| Prefix | Changelog section | Version bump | Use for | +| -------- | ----------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- | +| `feat:` | Features | minor | New user-visible capability — a new flag, support for a new OpenAPI construct, a new code-generation behavior. | +| `fix:` | Bug Fixes | patch | Correcting incorrect behavior — wrong generated output, crashes, regressions, malformed type emission. | +| `chore:` | Miscellaneous | none | Repo housekeeping with no runtime effect — CI tweaks, docs, dependency bumps, refactors, build-tooling changes, dev scripts. | + +Add `!` after the type (e.g. `feat!: drop Node 20 support`) **or** include +a `BREAKING CHANGE:` footer in the PR body to force a major version bump. +Use this for any change that breaks existing consumers — dropping a flag, +changing the generated-output contract, requiring a newer Node version, +etc. + +Examples: + +- `fix: wrap nullable arrays correctly in generated types` +- `feat: support discriminated unions via discriminator mapping` +- `chore: bump @types/node to ^24.0.0` +- `feat!: rename --exclude-paths to --no-paths` + +### 2. PR description: what changed + +Include a short summary of the change as bullet points. Keep it concrete +— callers reading the changelog should be able to tell from the bullets +whether the change affects them. + +```markdown +## Summary + +- Fix nullable arrays emitting `Nullable< name: type[]>` instead of + `name: Nullable` (file: `lib/builder/type-builder.ts`). +- No change to non-nullable arrays or arrays of objects. +``` + +### 3. Validation steps + +Document the exact commands you ran to verify the change. Because there +is no automated test suite, this is the only record of what was checked. +At minimum: + +```markdown +## Validation + +- `npm run lint` — clean. +- `npm run build` — clean. +- `npm test` — all fixtures pass. +- Added a nullable-array case to `test/fixtures/test-api.yaml` and + confirmed it appears in `test/output/test-api.ts` as + `Nullable`. +``` + +If you generated types against an external/private OpenAPI doc to +reproduce a reported issue, mention it but do not check the doc into the +repo. + +### 4. Manual validation for reviewers (if needed) + +If the change cannot be fully verified by lint+build alone (e.g. it +affects generated output for a specific OpenAPI shape), include a +"Manual validation" section telling the reviewer how to reproduce. +Provide a minimal YAML snippet inline and the expected generated TS +output, or a one-liner they can paste: + +```markdown +## Manual validation + +Save as `repro.yaml`: + +\`\`\`yaml +# minimal schema that triggered the bug +\`\`\` + +Then run: + +\`\`\`bash +npm run build +node dist/main.js --input repro.yaml --output /tmp/out.ts +\`\`\` + +Expect `types: Nullable;` in the output (previously emitted +`Nullable< types: string[]>` which fails to parse). +``` + +Skip this section if `npm run lint && npm run build` plus the petstore +smoke test are sufficient to demonstrate the change works. + +## Tips for agents + +- Prefer editing existing modules over adding new files; the codebase is + intentionally small. +- When adding support for a new OpenAPI construct, extend + `generateCodeForProperty` in `type-builder.ts` and update + `PropertyDefinition` in `types.ts` if the field isn't already modeled. +- After non-trivial changes, run `npm run lint && npm run build` and + regenerate `demo/petstore.yaml` types to spot regressions. +- There is no test runner. Treat `demo/petstore.yaml` plus a manual diff of + the generated output as the smoke test, and call out in the PR that + testing was manual. diff --git a/README.md b/README.md index 57b0e9a..ef48823 100644 --- a/README.md +++ b/README.md @@ -64,4 +64,80 @@ ott \ --exclude-schemas ``` +## Development + +### Prerequisites + +Node version is pinned in `.nvmrc` (Node 24). If you use `nvm`: + +```bash +nvm use +``` + +### Setup + +```bash +npm install +``` + +### Scripts + +- `npm run build` — Compile TypeScript sources to `dist/` via `tsc`. +- `npm run lint` — Run ESLint over the repo (uses `typescript-eslint`'s + strict configs). +- `npm run create-executable` — Build and `npm i -g` so `ott` is available + globally on your machine for end-to-end testing. +- `npm test` — Run the fixture-based test suite (see "Testing" below). + Requires `npm run build` first. + +### Testing + +The test suite (`test/run.js`) drives the built CLI against every +fixture in `test/fixtures/` and confirms the generated TypeScript compiles +under `tsc --strict`. The default fixture (`test/fixtures/test-api.yaml`) +covers the OpenAPI constructs ott supports: basic types, nullable types, +enums, arrays (including nullable arrays of objects), nested objects, +`additionalProperties`, `oneOf`/`anyOf`/`allOf`, path operations with +request bodies and 2xx responses, and path variables. + +```bash +npm run build +npm test +``` + +To extend coverage for a new construct or a regression, drop another +`*.yaml` file into `test/fixtures/` — the runner picks it up +automatically. Generated output is written to `test/output/` (gitignored). + +### CI workflows + +Located in `.github/workflows/`: + +- `checks.yaml` — Runs on every push to `main` and every pull request. + Sets up Node from `.nvmrc`, runs `npm ci`, `npm run lint`, and + `npm run build`. PRs must be green here before merge. +- `pr-validate.yaml` — Validates that the PR title follows + [Conventional Commits](https://www.conventionalcommits.org/) using + `amannn/action-semantic-pull-request`. Only `feat`, `fix`, and `chore` + prefixes are accepted. +- `release.yaml` — Runs on push to `main`. Uses + [release-please](https://github.com/googleapis/release-please) to open + release PRs that bump the version and update `CHANGELOG.md`. Merging a + release PR triggers a build and `npm publish`. + +### Release process + +This repo uses release-please. **Do not edit `package.json`'s `version` +field manually** — release-please derives the next version from the +Conventional Commit messages on `main`: + +- `feat:` commits trigger a minor version bump. +- `fix:` commits trigger a patch version bump. +- `feat!:` (or any commit with a `BREAKING CHANGE:` footer) triggers a + major version bump. +- `chore:` commits appear in the changelog under "Miscellaneous" and do + not bump the version. + +Sections in the generated changelog are configured in +`release-please-config.json`. diff --git a/eslint.config.mjs b/eslint.config.mjs index fc85061..b75c219 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,6 +3,7 @@ import pluginJs from '@eslint/js'; import tseslint from 'typescript-eslint'; export default [ + { ignores: ['dist/**', 'test/output/**'] }, { languageOptions: { globals: globals.node } }, pluginJs.configs.recommended, ...tseslint.configs.strict, diff --git a/lib/builder/type-builder.ts b/lib/builder/type-builder.ts index c63ec58..f3f1f34 100644 --- a/lib/builder/type-builder.ts +++ b/lib/builder/type-builder.ts @@ -182,14 +182,16 @@ function generateCodeForProperty( return null; } const nextPath = [...currentPath, 'items[]']; - const propTypeString = generateCodeForProperty({ + const itemTypeString = generateCodeForProperty({ currentPath: nextPath, - propertyName: propertyName, propertyDefinition: items, - isRequired, + isRequired: false, level, }); - if (propTypeString) return wrapNullable(`${propTypeString}[]`, nullable); + if (itemTypeString) { + const arrayType = wrapNullable(`${itemTypeString}[]`, nullable); + return indented(`${propertyPrefix}${arrayType}`, level, unnamed); + } } const currentPathString = `${currentPath.join('.')}.${propertyName}`; diff --git a/package-lock.json b/package-lock.json index 876a812..cc56c85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "@eslint/js": "^9.1.1", "@types/js-yaml": "^4.0.9", "@types/minimatch": "^5.1.2", - "@types/node": "^20.12.7", + "@types/node": "^24.0.0", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", @@ -471,12 +471,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~7.16.0" } }, "node_modules/@types/semver": { @@ -2198,10 +2199,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" }, "node_modules/uri-js": { "version": "4.4.1", diff --git a/package.json b/package.json index b1bbfec..3606d54 100644 --- a/package.json +++ b/package.json @@ -27,13 +27,13 @@ "build": "tsc", "create-executable": "npm run build && npm i -g", "lint": "eslint .", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node test/run.js" }, "devDependencies": { "@eslint/js": "^9.1.1", "@types/js-yaml": "^4.0.9", "@types/minimatch": "^5.1.2", - "@types/node": "^20.12.7", + "@types/node": "^24.0.0", "@typescript-eslint/eslint-plugin": "^7.7.1", "@typescript-eslint/parser": "^7.7.1", "eslint": "^8.57.0", diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..949bc54 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "node", + "packages": { + ".": { + "changelog-sections": [ + { "type": "feat", "section": "Features", "hidden": false }, + { "type": "fix", "section": "Bug Fixes", "hidden": false }, + { "type": "chore", "section": "Miscellaneous", "hidden": false } + ] + } + } +} diff --git a/test/fixtures/test-api.yaml b/test/fixtures/test-api.yaml new file mode 100644 index 0000000..f808957 --- /dev/null +++ b/test/fixtures/test-api.yaml @@ -0,0 +1,233 @@ +openapi: 3.0.0 +info: + title: ott Test API + version: '1.0.0' + description: >- + Fixture exercising every OpenAPI construct that ott is expected to + support. The generated TypeScript must compile under tsc --strict. + +paths: + /items: + get: + summary: List items + responses: + '200': + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + post: + summary: Create item + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + responses: + '201': + description: created + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + + /items/{itemId}: + get: + summary: Get item + parameters: + - name: itemId + in: path + required: true + schema: + type: string + responses: + '200': + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + '404': + description: not found + + /items/{itemId}/tags: + put: + summary: Replace tags + parameters: + - name: itemId + in: path + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + type: object + required: [tags] + properties: + tags: + type: array + items: + type: string + responses: + '200': + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/Arrays' + +components: + schemas: + BasicTypes: + type: object + required: [name, count, ratio, active] + properties: + name: + type: string + count: + type: integer + ratio: + type: number + active: + type: boolean + optional_field: + type: string + + NullableTypes: + type: object + required: [maybe_string, maybe_int, maybe_bool] + properties: + maybe_string: + type: string + nullable: true + maybe_int: + type: integer + nullable: true + maybe_bool: + type: boolean + nullable: true + + Status: + type: object + required: [state] + properties: + state: + type: string + enum: [active, inactive, pending] + + Arrays: + type: object + required: [tags, nullable_tags, objects] + properties: + tags: + type: array + items: + type: string + nullable_tags: + type: array + nullable: true + items: + type: string + objects: + type: array + items: + type: object + required: [id] + properties: + id: + type: string + nullable_objects: + type: array + nullable: true + items: + type: object + required: [name] + properties: + name: + type: string + nullable: true + + NestedObject: + type: object + required: [profile] + properties: + profile: + type: object + required: [name] + properties: + name: + type: string + address: + type: object + nullable: true + required: [city] + properties: + city: + type: string + zip: + type: string + nullable: true + + AdditionalPropertiesOnly: + type: object + additionalProperties: + type: string + + AdditionalPropertiesWithProps: + type: object + required: [name] + properties: + name: + type: string + additionalProperties: + type: integer + + EmptyObject: + type: object + + Unions: + type: object + required: [value] + properties: + value: + oneOf: + - type: string + - type: integer + any_value: + anyOf: + - type: string + - type: boolean + intersected: + allOf: + - type: object + required: [a] + properties: + a: + type: string + - type: object + required: [b] + properties: + b: + type: integer + + Item: + type: object + required: [id, name] + properties: + id: + type: string + name: + type: string + description: + type: string + nullable: true + tags: + type: array + nullable: true + items: + type: string + status: + $ref: '#/components/schemas/Status' diff --git a/test/run.js b/test/run.js new file mode 100644 index 0000000..088b1fc --- /dev/null +++ b/test/run.js @@ -0,0 +1,70 @@ +#!/usr/bin/env node +/* eslint-disable @typescript-eslint/no-var-requires */ +const { execFileSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..'); +const FIXTURES_DIR = path.join(__dirname, 'fixtures'); +const OUTPUT_DIR = path.join(__dirname, 'output'); +const CLI = path.join(ROOT, 'dist', 'main.js'); + +function run(label, file, args) { + process.stdout.write(` ${label} ... `); + try { + execFileSync(file, args, { cwd: ROOT, stdio: 'pipe' }); + console.log('ok'); + } catch (err) { + console.log('FAIL'); + if (err.stdout) process.stderr.write(err.stdout.toString()); + if (err.stderr) process.stderr.write(err.stderr.toString()); + process.exit(1); + } +} + +if (!fs.existsSync(CLI)) { + console.error(`error: ${CLI} not found. Run "npm run build" first.`); + process.exit(1); +} + +fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + +const fixtures = fs + .readdirSync(FIXTURES_DIR) + .filter((f) => f.endsWith('.yaml') || f.endsWith('.yml')); + +if (fixtures.length === 0) { + console.error(`error: no fixtures found in ${FIXTURES_DIR}`); + process.exit(1); +} + +let failed = 0; +for (const fixture of fixtures) { + console.log(`\n[${fixture}]`); + const input = path.join(FIXTURES_DIR, fixture); + const output = path.join( + OUTPUT_DIR, + fixture.replace(/\.ya?ml$/, '.ts'), + ); + + try { + run('generate', process.execPath, [CLI, '--input', input, '--output', output]); + run('typecheck', path.join(ROOT, 'node_modules', '.bin', 'tsc'), [ + '--noEmit', + '--strict', + '--target', + 'es2020', + '--module', + 'commonjs', + output, + ]); + } catch { + failed++; + } +} + +if (failed > 0) { + console.error(`\n${failed} fixture(s) failed`); + process.exit(1); +} +console.log(`\nall ${fixtures.length} fixture(s) passed`);