zopia 0.3.0 โ 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +48 -16
- package/docs/07-api-docs.md +77 -2
- package/docs/09-configuration.md +2 -2
- package/docs/10-usage.md +26 -3
- package/package.json +10 -2
- package/src/conversions/api-docs-generate.ts +6 -12
- package/src/conversions/api-docs-names.ts +54 -0
- package/src/conversions/manifest-writer.ts +1 -1
- package/src/conversions/openapi-to-api-docs.ts +5 -10
- package/src/runtime/create-api-docs.ts +321 -0
- package/src/runtime.ts +10 -0
- package/docs/01-overview.md +0 -94
- package/docs/02-targets.md +0 -55
- package/docs/03-roadmap.md +0 -205
- package/docs/04-architecture.md +0 -345
- package/docs/05-concepts.md +0 -239
- package/docs/06-conversions.md +0 -493
- package/docs/08-components.md +0 -223
- package/docs/11-testing.md +0 -267
- package/docs/12-standards.md +0 -242
- package/docs/README.md +0 -42
- package/docs/publish-workflow.yml.example +0 -48
package/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-29
|
|
11
|
+
|
|
12
|
+
### ๐ Changed
|
|
13
|
+
- ๐ฆ **Slimmer npm package โ practical docs only.** The published archive now
|
|
14
|
+
ships just the user-facing guides (`docs/07-api-docs.md`,
|
|
15
|
+
`docs/09-configuration.md`, `docs/10-usage.md`) next to `README.md` /
|
|
16
|
+
`CHANGELOG.md`; the development documentation (overview, targets, roadmap,
|
|
17
|
+
architecture, concepts, conversions, components, testing, standards, the
|
|
18
|
+
docs map, and the publish-workflow example) stays in the GitHub repository
|
|
19
|
+
and is linked from the README โ npm users installing the package get
|
|
20
|
+
usage/installation docs, not project management artifacts. The tarball
|
|
21
|
+
shrinks **239 KB โ 182 KB packed (915 KB โ 750 KB unpacked, 50 โ 39
|
|
22
|
+
files)**; `npm` force-includes `README*` from any directory, so the docs map
|
|
23
|
+
is explicitly negated (`!docs/README.md`) in `files`. `package:check` and
|
|
24
|
+
the release contract now enforce the slim archive: a missing practical doc
|
|
25
|
+
fails, and any development doc leaking into the pack fails. The release
|
|
26
|
+
standard (R-192) and the docs map describe the split.
|
|
27
|
+
|
|
10
28
|
### โจ Added
|
|
29
|
+
- ๐ณ **Runtime tree consumption โ `zopia/runtime` (S-94).** New opt-in subpath
|
|
30
|
+
export (`import { createApiDocs, flattenApiDocs } from 'zopia/runtime'`) that
|
|
31
|
+
turns a generated api-docs directory into the objects an application
|
|
32
|
+
consumes, with **zero changes to generation output**. `createApiDocs(dir)` โ
|
|
33
|
+
the entire consumer DX โ discovers the root `.zopia-manifest.json` plus every
|
|
34
|
+
one-level-deep preset bucket manifest (`multi-tag`/`multi-server`), merges
|
|
35
|
+
them (deduplicating by `${path}#${method}`, root manifest first), sorts
|
|
36
|
+
deterministically (path segments, then the canonical method order), and
|
|
37
|
+
returns one nested object keyed by the exact URL path segments with the
|
|
38
|
+
lowercase method as leaf key holding the endpoint module's `export default`
|
|
39
|
+
(loaded via `pathToFileURL`, Windows-safe). `flattenApiDocs(tree)` is the
|
|
40
|
+
flat freebie: a `Record<string, config>` keyed by `operationId`, deriving
|
|
41
|
+
missing names and collision suffixes through the **same shared rules the
|
|
42
|
+
generator uses for its export identifiers** (extracted to
|
|
43
|
+
`src/conversions/api-docs-names.ts`: camelize, reserved-word guard,
|
|
44
|
+
leading-numeric guard, `2`/`3`โฆ suffixing โ `await` โ `awaitEndpoint`).
|
|
45
|
+
Missing/garbled manifests, unsafe manifest paths, import failures, and
|
|
46
|
+
modules without default exports fail typed (`ZOPIA_DOCS_MISSING_MANIFEST`,
|
|
47
|
+
`ZOPIA_MANIFEST_INVALID`, `ZOPIA_DOCS_IMPORT_FAILED`); paths that cannot
|
|
48
|
+
share one nested tree (below a method leaf, trailing-slash twins) fail typed
|
|
49
|
+
`ZOPIA_SPEC_INVALID`. Documented in
|
|
50
|
+
[docs/07-api-docs.md โ Runtime tree consumption](docs/07-api-docs.md).
|
|
11
51
|
- ๐ฆ **npm publish pipeline** โ `.github/workflows/publish.yml` publishes on GitHub Release creation (or manually) with the pinned Bun toolchain, verifies the release tag matches `package.json`'s version, and runs `npm publish --provenance --access public`. Its only credential is the repository-secret `NODE_AUTH_TOKEN` (npm `NPM_TOKEN`) โ never handled in chat or commits; npm trusted-publishing (OIDC) is supported by the declared `id-token` permission.
|
|
12
52
|
|
|
13
53
|
### ๐ Fixed
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
[](https://bun.sh/)
|
|
14
14
|
[](https://vitest.dev/)
|
|
15
15
|
|
|
16
|
-
โ
**Status โ Phase 3 complete ยท v0.
|
|
16
|
+
โ
**Status โ Phase 3 complete ยท v0.4.0 released**
|
|
17
17
|
|
|
18
18
|
</div>
|
|
19
19
|
|
|
@@ -91,25 +91,57 @@ await openApiToApiDocs('swagger.json', {
|
|
|
91
91
|
const { openapi } = await apiDocsToOpenApi('api_docs', { version: '3.1' });
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### ๐ณ Consuming the tree at runtime
|
|
95
|
+
|
|
96
|
+
When wiring endpoints dynamically beats importing generated files one by one,
|
|
97
|
+
import the opt-in runtime subpath and point it at any directory zopia ever
|
|
98
|
+
generated into (`directory`, `flat`, and `multi-tag` / `multi-server` splits
|
|
99
|
+
all resolve through their manifests):
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { createApiDocs, flattenApiDocs } from 'zopia/runtime';
|
|
103
|
+
|
|
104
|
+
// Nested: URL path segments โ lowercase method โ the makeApiConfig object
|
|
105
|
+
const apiDocs = await createApiDocs('api_docs');
|
|
106
|
+
const getUser = apiDocs.users['{userId}'].get; // default export of that index.ts
|
|
107
|
+
|
|
108
|
+
// Flat freebie: keyed by operationId, same leaf objects, deterministic order
|
|
109
|
+
const endpoints = flattenApiDocs(apiDocs);
|
|
110
|
+
endpoints.getUser === getUser; // โ true
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Generation output is untouched โ the resolver only reads manifests and imports
|
|
114
|
+
modules. See [docs/07-api-docs.md โ Runtime tree consumption](docs/07-api-docs.md#-runtime-tree-consumption--createapidocs).
|
|
115
|
+
|
|
94
116
|
## ๐ Documentation
|
|
95
117
|
|
|
96
|
-
|
|
97
|
-
|
|
118
|
+
The **practical guides ship inside the npm package** โ usage, configuration,
|
|
119
|
+
and the generated output format. The development documentation (project
|
|
120
|
+
targets, roadmap, architecture, testing strategy, engineering standards)
|
|
121
|
+
stays in the
|
|
122
|
+
[GitHub repository](https://github.com/komeilm76/zopia/tree/main/docs).
|
|
123
|
+
|
|
124
|
+
**๐ฆ Packed with the npm package** (relative links, in `docs/`):
|
|
98
125
|
|
|
99
126
|
| ๐ Document | Contents |
|
|
100
127
|
| --- | --- |
|
|
101
|
-
| ๐งญ [Overview](docs/01-overview.md) | What zopia is, the problem it solves, principles, non-goals |
|
|
102
|
-
| ๐ฏ [Targets](docs/02-targets.md) | The explicit, testable targets of this project |
|
|
103
|
-
| ๐บ๏ธ [Roadmap](docs/03-roadmap.md) | Phases, milestones, definition of done |
|
|
104
|
-
| ๐๏ธ [Architecture](docs/04-architecture.md) | Modules, pipeline, internal model, error model, safety |
|
|
105
|
-
| ๐งฉ [Concepts](docs/05-concepts.md) | Glossary โ Swagger 2.0, OpenAPI 3.x, JSON Schema, `$ref`, Zod v4, km-api |
|
|
106
|
-
| ๐ [Conversions](docs/06-conversions.md) | The four engines: algorithms, mapping tables, edge cases |
|
|
107
|
-
| ๐ [API docs format](docs/07-api-docs.md) | `directory` & `flat` layouts, `index.ts` contract, manifest |
|
|
108
|
-
| ๐งฑ [Components](docs/08-components.md) | `insertComponents` / `useComponentAsReference`, `$ref` graphs |
|
|
109
|
-
| โ๏ธ [Configuration](docs/09-configuration.md) | Full option reference, defaults, validation rules |
|
|
110
128
|
| ๐ [Usage](docs/10-usage.md) | Installation, programmatic API, CLI, end-to-end example |
|
|
111
|
-
|
|
|
112
|
-
|
|
|
129
|
+
| โ๏ธ [Configuration](docs/09-configuration.md) | Full option reference, defaults, validation rules |
|
|
130
|
+
| ๐ [API docs format](docs/07-api-docs.md) | `directory` & `flat` layouts, `index.ts` contract, manifest, runtime consumption |
|
|
131
|
+
|
|
132
|
+
**๐ GitHub-only** (development docs, not packed):
|
|
133
|
+
|
|
134
|
+
| ๐ Document | Contents |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| ๐งญ [Overview](https://github.com/komeilm76/zopia/blob/main/docs/01-overview.md) | What zopia is, the problem it solves, principles, non-goals |
|
|
137
|
+
| ๐ฏ [Targets](https://github.com/komeilm76/zopia/blob/main/docs/02-targets.md) | The explicit, testable targets of this project |
|
|
138
|
+
| ๐บ๏ธ [Roadmap](https://github.com/komeilm76/zopia/blob/main/docs/03-roadmap.md) | Phases, milestones, definition of done |
|
|
139
|
+
| ๐๏ธ [Architecture](https://github.com/komeilm76/zopia/blob/main/docs/04-architecture.md) | Modules, pipeline, internal model, error model, safety |
|
|
140
|
+
| ๐งฉ [Concepts](https://github.com/komeilm76/zopia/blob/main/docs/05-concepts.md) | Glossary โ Swagger 2.0, OpenAPI 3.x, JSON Schema, `$ref`, Zod v4, km-api |
|
|
141
|
+
| ๐ [Conversions](https://github.com/komeilm76/zopia/blob/main/docs/06-conversions.md) | The four engines: algorithms, mapping tables, edge cases |
|
|
142
|
+
| ๐งฑ [Components](https://github.com/komeilm76/zopia/blob/main/docs/08-components.md) | `insertComponents` / `useComponentAsReference`, `$ref` graphs |
|
|
143
|
+
| ๐งช [Testing](https://github.com/komeilm76/zopia/blob/main/docs/11-testing.md) | Vitest strategy, scenario matrix, fixtures, coverage gates |
|
|
144
|
+
| ๐ [Standards](https://github.com/komeilm76/zopia/blob/main/docs/12-standards.md) | Code, JSDoc, commits, changelog, releases, key decisions |
|
|
113
145
|
|
|
114
146
|
## ๐๏ธ Project structure
|
|
115
147
|
|
|
@@ -139,7 +171,7 @@ zopia/
|
|
|
139
171
|
|
|
140
172
|
## ๐งช Development
|
|
141
173
|
|
|
142
|
-
zopia is a **Bun-first** project (see [docs/12-standards.md](docs/12-standards.md)):
|
|
174
|
+
zopia is a **Bun-first** project (see [docs/12-standards.md](https://github.com/komeilm76/zopia/blob/main/docs/12-standards.md)):
|
|
143
175
|
|
|
144
176
|
```bash
|
|
145
177
|
bun install --frozen-lockfile # ๐ฆ reproducible install from bun.lock
|
|
@@ -157,7 +189,7 @@ bun run release:check # ๐ข complete pinned-Bun prepublish gate
|
|
|
157
189
|
|
|
158
190
|
## ๐ค Contributing
|
|
159
191
|
|
|
160
|
-
Read [docs/12-standards.md](docs/12-standards.md) first โ it defines code style,
|
|
192
|
+
Read [docs/12-standards.md](https://github.com/komeilm76/zopia/blob/main/docs/12-standards.md) first โ it defines code style,
|
|
161
193
|
JSDoc rules, the commit convention, and the changelog rule. Every feature ships
|
|
162
194
|
**with** its documentation in the same commit.
|
|
163
195
|
|
package/docs/07-api-docs.md
CHANGED
|
@@ -106,6 +106,81 @@ templates, duplicate parameters, unsupported methods, and unsafe roots. Engine
|
|
|
106
106
|
does not emit a facade module in v0.1.0: direct imports are the generated
|
|
107
107
|
file-level API, and the manifest remains authoritative (D-06).
|
|
108
108
|
|
|
109
|
+
## ๐ณ Runtime tree consumption โ `createApiDocs`
|
|
110
|
+
|
|
111
|
+
> ๐ฏ **S-94** โ *an opt-in runtime API, shipped in the `zopia` package itself,
|
|
112
|
+
> converts a generated tree directory into the nested / flat objects an
|
|
113
|
+
> application consumes. No generation output changes: the feature only reads
|
|
114
|
+
> and imports.*
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { createApiDocs, flattenApiDocs } from 'zopia/runtime';
|
|
118
|
+
|
|
119
|
+
const apiDocs = await createApiDocs('api_docs'); // โ the entire consumer DX
|
|
120
|
+
const endpoint = apiDocs.applicant['{applicantId}'].exame['{examId}'].get;
|
|
121
|
+
// โ the endpoint module's makeApiConfig object (its default export)
|
|
122
|
+
|
|
123
|
+
const endpoints = flattenApiDocs(apiDocs); // the flat freebie
|
|
124
|
+
endpoints.getExam; // same leaf object
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Manifest discovery & merge (B-conditions).** The resolver reads the root
|
|
128
|
+
`.zopia-manifest.json` **plus** every one-level-deep preset bucket manifest
|
|
129
|
+
(`multi-tag` / `multi-server` splits write one manifest per bucket directory),
|
|
130
|
+
merging all of them and deduplicating by `${path}#${method}` โ the root
|
|
131
|
+
manifest wins duplicates, buckets follow in sorted directory order. Any
|
|
132
|
+
directory that was ever a zopia output root is accepted, including a single
|
|
133
|
+
preset bucket root. Entries are then sorted deterministically: URL path
|
|
134
|
+
segments first, then the canonical method order
|
|
135
|
+
(`get, post, put, delete, head, options, patch, trace`).
|
|
136
|
+
|
|
137
|
+
**Nesting.** Branch keys are the URL path segments **exactly and in order**
|
|
138
|
+
(including literal `{param}` segments), the leaf key is the lowercase method,
|
|
139
|
+
and the leaf value is the endpoint module's **default export**, loaded with
|
|
140
|
+
dynamic `import()` through `pathToFileURL` (Windows-safe). Because which keys
|
|
141
|
+
exist depends on the source spec, the `ApiDocsTree` type types every node
|
|
142
|
+
permissively (branch โช config). A tree `/` path nests its methods at the root
|
|
143
|
+
(`apiDocs.get`). Paths that cannot coexist in one nested tree โ a path
|
|
144
|
+
continuing below another path's method leaf, or two paths differing only by a
|
|
145
|
+
trailing slash โ fail with a typed `ZOPIA_SPEC_INVALID` instead of silently
|
|
146
|
+
dropping an endpoint. Key insertion follows the deterministic sort, so the
|
|
147
|
+
same tree always enumerates keys in the same order (P-1).
|
|
148
|
+
|
|
149
|
+
**Errors** reuse the manifest conventions: a missing manifest โ
|
|
150
|
+
`ZOPIA_DOCS_MISSING_MANIFEST`; garbled JSON or unusable `apis[]` records โ
|
|
151
|
+
`ZOPIA_MANIFEST_INVALID`; a module that cannot be imported or has no default
|
|
152
|
+
export โ `ZOPIA_DOCS_IMPORT_FAILED` naming the file. Reading is pure โ nothing
|
|
153
|
+
on disk is written.
|
|
154
|
+
|
|
155
|
+
**The flatten freebie.** `flattenApiDocs(apiDocs)` deep-walks the tree in its
|
|
156
|
+
deterministic leaf order and returns a flat `Record<string, config>` keyed by
|
|
157
|
+
each config's `operationId`. When a leaf has no usable `operationId`, or two
|
|
158
|
+
keys collide, the name is derived by the **same rules the generator uses for
|
|
159
|
+
its `export const` identifiers** (R-732: camelize, reserved-word guard,
|
|
160
|
+
leading-numeric guard, `2`/`3`โฆ uniqueness suffix โ `await` โ `awaitEndpoint`,
|
|
161
|
+
`get-a` + `getA` โ `getA` + `getA2`), so the flat keys always match the
|
|
162
|
+
modules' named exports. The shared rules live in
|
|
163
|
+
`src/conversions/api-docs-names.ts` and both call-sites (generation and
|
|
164
|
+
runtime) use them.
|
|
165
|
+
|
|
166
|
+
**A small km-api hand-off** โ every leaf *is* a `makeApiConfig()` object, so
|
|
167
|
+
the km-api surface is available directly:
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
import { createApiDocs } from 'zopia/runtime';
|
|
171
|
+
|
|
172
|
+
const apiDocs = await createApiDocs('api_docs');
|
|
173
|
+
const getUser = apiDocs.users['{userId}'].get;
|
|
174
|
+
|
|
175
|
+
getUser.method; // "GET"
|
|
176
|
+
getUser.pathShape; // "/users/{userId}"
|
|
177
|
+
getUser.request.params.parse({ userId: '22ccbc6a-โฆ' }); // โ
Zod-validated path params
|
|
178
|
+
getUser.response[200].parse({ id: 'u1', email: 'a@b.c' }); // โ
Zod-validated response
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
> ๐ก Generated webhook modules are not URL-path endpoints and stay outside the
|
|
182
|
+
> nested tree โ import them directly or reverse-convert the tree (engine โฃ).
|
|
183
|
+
|
|
109
184
|
## ๐งฌ Operation contract extraction
|
|
110
185
|
|
|
111
186
|
Before rendering an endpoint, zopia normalizes each operation into an
|
|
@@ -177,7 +252,7 @@ With the default options (`insertComponents: false`), every `index.ts` imports
|
|
|
177
252
|
**only** `zod` and `km-api` (R-502): each component use is inlined into its
|
|
178
253
|
request/parameter/response expression (R-403). Cross-file imports appear
|
|
179
254
|
**only** when `useComponentAsReference` is `true` โ see
|
|
180
|
-
[Components](08-components.md).
|
|
255
|
+
[Components](https://github.com/komeilm76/zopia/blob/main/docs/08-components.md).
|
|
181
256
|
|
|
182
257
|
## ๐ฆ The manifest โ `.zopia-manifest.json`
|
|
183
258
|
|
|
@@ -333,5 +408,5 @@ Security requirements remain manifest-owned because km-api stores only `auth: 'Y
|
|
|
333
408
|
|
|
334
409
|
## ๐ Next
|
|
335
410
|
|
|
336
|
-
- ๐งฑ What changes when components are emitted โ [Components](08-components.md)
|
|
411
|
+
- ๐งฑ What changes when components are emitted โ [Components](https://github.com/komeilm76/zopia/blob/main/docs/08-components.md)
|
|
337
412
|
- โ๏ธ Every option that shapes this output โ [Configuration](09-configuration.md)
|
package/docs/09-configuration.md
CHANGED
|
@@ -107,7 +107,7 @@ interface ZopiaReverseOptions {
|
|
|
107
107
|
|
|
108
108
|
## ๐ `ZodToJsonSchemaOptions` โ engine โ
|
|
109
109
|
|
|
110
|
-
See [Conversions โ Engine โ ](06-conversions.md)
|
|
110
|
+
See [Conversions โ Engine โ ](https://github.com/komeilm76/zopia/blob/main/docs/06-conversions.md)
|
|
111
111
|
(`target`, `$schema`, `io`).
|
|
112
112
|
|
|
113
113
|
## ๐ `JsonSchemaToZodOptions` โ engine โก
|
|
@@ -164,4 +164,4 @@ zopia diff <old-spec> <new-spec> (no options yet; `--config path` accep
|
|
|
164
164
|
## ๐ Next
|
|
165
165
|
|
|
166
166
|
- ๐ How options are used end-to-end โ [Usage](10-usage.md)
|
|
167
|
-
- ๐ What each option changes in the tree โ [API docs format](07-api-docs.md) ยท [Components](08-components.md)
|
|
167
|
+
- ๐ What each option changes in the tree โ [API docs format](07-api-docs.md) ยท [Components](https://github.com/komeilm76/zopia/blob/main/docs/08-components.md)
|
package/docs/10-usage.md
CHANGED
|
@@ -178,8 +178,31 @@ stderr, hint included) ยท `2` internal error (should never happen โ report it)
|
|
|
178
178
|
| R-101 | ๐ **Import, don't re-type** โ your app imports the generated `index.ts` files; their Zod schemas *are* the validation | โ |
|
|
179
179
|
| R-102 | ๐ **Spec or generation options changed?** re-run `zopia generate` โ output is idempotent (P-1); manifest staleness warns on source/config/incomplete-tree drift and safely prunes only obsolete manifest-owned files (`ZOPIA_WARN_STALE_TREE`) | [07 โ Regeneration](07-api-docs.md#-regeneration--manual-edits-phase-1-policy) |
|
|
180
180
|
| R-103 | โ๏ธ **Hand edits** โ Phase 1 overwrites them on regeneration (see the file banner); the merge-safe custom layer arrives in Phase 3 | [07 โ Regeneration](07-api-docs.md#-regeneration--manual-edits-phase-1-policy) |
|
|
181
|
-
| R-104 | ๐งช **km-api helpers** โ `makeFullPath`, `makeParams`, `convertResponseType`, โฆ are available on every generated config for free | [Concepts โ km-api](05-concepts.md#-km-api) |
|
|
181
|
+
| R-104 | ๐งช **km-api helpers** โ `makeFullPath`, `makeParams`, `convertResponseType`, โฆ are available on every generated config for free | [Concepts โ km-api](https://github.com/komeilm76/zopia/blob/main/docs/05-concepts.md#-km-api) |
|
|
182
182
|
| R-105 | ๐ซ **No zopia import in app code** โ generated files depend only on `zod` + `km-api` (R-502) | โ |
|
|
183
|
+
| R-106 | ๐ณ **Runtime tree loading** โ when wiring endpoints dynamically beats importing files one by one, `createApiDocs()` from the opt-in `zopia/runtime` subpath turns the whole directory into one nested object (plus the `flattenApiDocs` flat record); generation output is untouched | [07 โ Runtime tree consumption](07-api-docs.md#-runtime-tree-consumption--createapidocs) |
|
|
184
|
+
|
|
185
|
+
### ๐ณ Consuming the tree at runtime
|
|
186
|
+
|
|
187
|
+
Import the dedicated subpath (the package root stays free of
|
|
188
|
+
filesystem-importing APIs) and point it at any directory zopia ever generated
|
|
189
|
+
into โ `directory`, `flat`, and split `multi-tag` / `multi-server` trees all
|
|
190
|
+
resolve through their manifests:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { createApiDocs, flattenApiDocs } from 'zopia/runtime';
|
|
194
|
+
|
|
195
|
+
// Nested: exact URL path segments, lowercase method leaf, default-export config
|
|
196
|
+
const apiDocs = await createApiDocs('api_docs');
|
|
197
|
+
const getUser = apiDocs.users['{userId}'].get; // the makeApiConfig object
|
|
198
|
+
|
|
199
|
+
// Flat: keyed by operationId (derived with the generator's own naming rules)
|
|
200
|
+
const endpoints = flattenApiDocs(apiDocs);
|
|
201
|
+
endpoints.getUser === getUser; // โ true
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Failures are typed `ZopiaError`s with `at`/`hint` โ see
|
|
205
|
+
[API docs โ Runtime tree consumption](07-api-docs.md#-runtime-tree-consumption--createapidocs).
|
|
183
206
|
|
|
184
207
|
## ๐งฏ Error handling
|
|
185
208
|
|
|
@@ -200,9 +223,9 @@ try {
|
|
|
200
223
|
```
|
|
201
224
|
|
|
202
225
|
The full error-code table lives in
|
|
203
|
-
[Architecture โ Error model](04-architecture.md#-error-model).
|
|
226
|
+
[Architecture โ Error model](https://github.com/komeilm76/zopia/blob/main/docs/04-architecture.md#-error-model).
|
|
204
227
|
|
|
205
228
|
## ๐ Next
|
|
206
229
|
|
|
207
230
|
- โ๏ธ Every option โ [Configuration](09-configuration.md)
|
|
208
|
-
- ๐งช How all of this is tested โ [Testing](11-testing.md)
|
|
231
|
+
- ๐งช How all of this is tested โ [Testing](https://github.com/komeilm76/zopia/blob/main/docs/11-testing.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zopia",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Type-safe OpenAPI, JSON Schema, and Zod conversion toolkit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-docs",
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"bin",
|
|
29
29
|
"src",
|
|
30
|
-
"docs",
|
|
30
|
+
"docs/07-api-docs.md",
|
|
31
|
+
"docs/09-configuration.md",
|
|
32
|
+
"docs/10-usage.md",
|
|
33
|
+
"!docs/README.md",
|
|
31
34
|
"CHANGELOG.md",
|
|
32
35
|
"LICENSE",
|
|
33
36
|
"README.md"
|
|
@@ -72,6 +75,11 @@
|
|
|
72
75
|
"types": "./src/index.ts",
|
|
73
76
|
"import": "./src/index.ts",
|
|
74
77
|
"default": "./src/index.ts"
|
|
78
|
+
},
|
|
79
|
+
"./runtime": {
|
|
80
|
+
"types": "./src/runtime.ts",
|
|
81
|
+
"import": "./src/runtime.ts",
|
|
82
|
+
"default": "./src/runtime.ts"
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstat, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
3
|
import { asZopiaError, ZopiaError } from '../errors';
|
|
4
|
+
import { endpointExportName } from './api-docs-names';
|
|
4
5
|
import { buildOpenApiOperationIR } from './openapi-ir';
|
|
5
6
|
import { deriveReusableParameterSchema, deriveReusableResponseSchema, extractOperationContracts, reusableDeclarations } from './openapi-contracts';
|
|
6
7
|
import { jsonSchemaToZod } from './json-schema-to-zod';
|
|
@@ -61,7 +62,7 @@ function collectComponentRefs(value: unknown, names = new Set<string>(), mapEntr
|
|
|
61
62
|
return names;
|
|
62
63
|
}
|
|
63
64
|
function schemaCode(schema: unknown, name: string): string {
|
|
64
|
-
const safeName =
|
|
65
|
+
const safeName = endpointExportName(name);
|
|
65
66
|
const converted = jsonSchemaToZod(schema === undefined ? true : schema as any, { rootName: safeName });
|
|
66
67
|
const source = converted.code.trimEnd();
|
|
67
68
|
const direct = source.match(new RegExp(`^const ${safeName.replace(/[$]/g, '\\$&')} = ([\\s\\S]*);$`));
|
|
@@ -222,23 +223,16 @@ function resolveObject(value: unknown, source: OpenApiDocument): any {
|
|
|
222
223
|
}
|
|
223
224
|
return current;
|
|
224
225
|
}
|
|
225
|
-
function exportName(operationId: string): string {
|
|
226
|
-
const parts = operationId.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
|
|
227
|
-
let name = parts.map((part, index) => index === 0 ? part : part[0].toUpperCase() + part.slice(1)).join('') || 'endpoint';
|
|
228
|
-
if (!/^[A-Za-z_$]/.test(name)) name = `endpoint${name}`;
|
|
229
|
-
if (['arguments', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield'].includes(name)) name = `${name}Endpoint`;
|
|
230
|
-
return name;
|
|
231
|
-
}
|
|
232
226
|
function componentExportName(componentName: string): string {
|
|
233
|
-
const name =
|
|
227
|
+
const name = endpointExportName(componentName);
|
|
234
228
|
return name.endsWith('Schema') ? name : `${name}Schema`;
|
|
235
229
|
}
|
|
236
230
|
function componentParameterExportName(componentName: string): string {
|
|
237
|
-
const name =
|
|
231
|
+
const name = endpointExportName(componentName);
|
|
238
232
|
return name.endsWith('Parameter') ? name : `${name}Parameter`;
|
|
239
233
|
}
|
|
240
234
|
function componentResponseExportName(componentName: string): string {
|
|
241
|
-
const name =
|
|
235
|
+
const name = endpointExportName(componentName);
|
|
242
236
|
return name.endsWith('Response') ? name : `${name}Response`;
|
|
243
237
|
}
|
|
244
238
|
function componentReaches(source: OpenApiDocument, from: string, target: string, seen = new Set<string>()): boolean {
|
|
@@ -352,7 +346,7 @@ function renderEndpoint(operation: any, source: OpenApiDocument, mode: ApiDocsMo
|
|
|
352
346
|
const examplesValue = { ...(Object.keys(requestExamples).length ? { request: requestExamples } : {}), ...(Object.keys(responseExamples).length ? { response: responseExamples } : {}) };
|
|
353
347
|
const examples = Object.keys(examplesValue).length ? `examples: JSON.parse(${JSON.stringify(stableDataJson(examplesValue))}),` : '';
|
|
354
348
|
const opId = operation.operationId;
|
|
355
|
-
const exportId =
|
|
349
|
+
const exportId = endpointExportName(opId);
|
|
356
350
|
const tags = ir.tags;
|
|
357
351
|
const auth = ir.security !== undefined && ir.security.length > 0 && ir.security.every((requirement) => Object.keys(requirement as Record<string, unknown>).length > 0) ? 'YES' : 'NO';
|
|
358
352
|
const sourceName = JSON.stringify(`${source.info.title} v${source.info.version}`).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ๐ Shared endpoint identifier rules (R-732).
|
|
3
|
+
*
|
|
4
|
+
* The single source of truth for deriving safe export identifiers from
|
|
5
|
+
* operation IDs โ and for the numeric uniqueness suffix applied when two
|
|
6
|
+
* derived identifiers collide. Generation (Engine โข's `export const` names
|
|
7
|
+
* and the collectors' deterministic operation IDs) and the runtime tree
|
|
8
|
+
* resolver (`flattenApiDocs` record keys) call these helpers so both surfaces
|
|
9
|
+
* keep producing identical names by construction.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** ECMAScript keywords and literals never emitted as generated identifiers. */
|
|
13
|
+
const RESERVED_IDENTIFIER_NAMES: ReadonlySet<string> = new Set([
|
|
14
|
+
'arguments', 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Derive the endpoint export identifier from one operation ID.
|
|
19
|
+
*
|
|
20
|
+
* Applies the fixed naming contract of R-732: split on non-identifier
|
|
21
|
+
* characters, camel-case the parts (camelize), fall back to `endpoint` when
|
|
22
|
+
* nothing remains, prefix `endpoint` when the result starts with a digit or
|
|
23
|
+
* other non-identifier character, and append `Endpoint` when the result is a
|
|
24
|
+
* reserved ECMAScript keyword or literal (e.g. `await` โ `awaitEndpoint`).
|
|
25
|
+
*
|
|
26
|
+
* @param operationId Explicit or deterministically derived operation identifier.
|
|
27
|
+
* @returns Safe camel-case TypeScript identifier for the module export.
|
|
28
|
+
*/
|
|
29
|
+
export function endpointExportName(operationId: string): string {
|
|
30
|
+
const parts = operationId.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
|
|
31
|
+
let name = parts.map((part, index) => index === 0 ? part : part[0].toUpperCase() + part.slice(1)).join('') || 'endpoint';
|
|
32
|
+
if (!/^[A-Za-z_$]/.test(name)) name = `endpoint${name}`;
|
|
33
|
+
if (RESERVED_IDENTIFIER_NAMES.has(name)) name = `${name}Endpoint`;
|
|
34
|
+
return name;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return the first free variant of a derived identifier, suffixing `2`, `3`, โฆ
|
|
39
|
+
*
|
|
40
|
+
* This is the exact uniqueness rule the operation collectors apply when a
|
|
41
|
+
* derived operation ID collides (`getA` โ `getA2` โ `getA3`): the first
|
|
42
|
+
* candidate is the base itself, and every subsequent candidate appends the
|
|
43
|
+
* next integer directly to the base.
|
|
44
|
+
*
|
|
45
|
+
* @param base Identifier before any uniqueness suffix.
|
|
46
|
+
* @param used Already-claimed identifiers this derivation must not clobber.
|
|
47
|
+
* @returns `base` itself when free, otherwise the first suffixed variant.
|
|
48
|
+
*/
|
|
49
|
+
export function uniqueEndpointName(base: string, used: ReadonlySet<string>): string {
|
|
50
|
+
let name = base;
|
|
51
|
+
let suffix = 1;
|
|
52
|
+
while (used.has(name)) name = `${base}${++suffix}`;
|
|
53
|
+
return name;
|
|
54
|
+
}
|
|
@@ -16,7 +16,7 @@ export const ZOPIA_MANIFEST_SCHEMA = 'zopia:manifest@1' as const;
|
|
|
16
16
|
export const ZOPIA_MANIFEST_FILE = '.zopia-manifest.json' as const;
|
|
17
17
|
|
|
18
18
|
/** Package version recorded by the current manifest writer. */
|
|
19
|
-
export const ZOPIA_VERSION = '0.
|
|
19
|
+
export const ZOPIA_VERSION = '0.4.0' as const;
|
|
20
20
|
|
|
21
21
|
/** Supported source dialect labels stored in a manifest. */
|
|
22
22
|
export type ZopiaManifestSourceKind = 'swagger-2.0' | 'openapi-3.0' | 'openapi-3.1';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ZopiaError } from '../errors';
|
|
2
2
|
import { normalizeOpenApiDocument, type OpenApiDocument } from './openapi';
|
|
3
|
+
import { uniqueEndpointName } from './api-docs-names';
|
|
3
4
|
import { resolveOpenApiLocalRef } from './openapi-ref';
|
|
4
5
|
|
|
5
6
|
/** Canonical km-api/OpenAPI operation method order. */
|
|
@@ -107,15 +108,12 @@ export function collectOpenApiOperations(input: OpenApiDocument | string): OpenA
|
|
|
107
108
|
const previous = owners.get(operationId);
|
|
108
109
|
if (previous) {
|
|
109
110
|
if (previous.operation.operationId !== undefined) throw new ZopiaError('ZOPIA_SPEC_INVALID', `Duplicate operationId: ${operationId}`);
|
|
110
|
-
|
|
111
|
-
while (ids.has(replacement)) replacement = `${operationId}${++suffix}`;
|
|
111
|
+
const replacement = uniqueEndpointName(operationId, ids);
|
|
112
112
|
ids.delete(previous.operationId); owners.delete(previous.operationId);
|
|
113
113
|
previous.operationId = replacement; ids.add(replacement); owners.set(replacement, previous);
|
|
114
114
|
}
|
|
115
115
|
} else {
|
|
116
|
-
|
|
117
|
-
operationId = base; let suffix = 1;
|
|
118
|
-
while (ids.has(operationId)) operationId = `${base}${++suffix}`;
|
|
116
|
+
operationId = uniqueEndpointName(deriveOperationId(path, method), ids);
|
|
119
117
|
}
|
|
120
118
|
const collected = { path, method, operation, operationId, parameters: mergedParameters };
|
|
121
119
|
ids.add(operationId); owners.set(operationId, collected); operations.push(collected);
|
|
@@ -185,15 +183,12 @@ export function collectOpenApiWebhookOperations(document: OpenApiDocument): Open
|
|
|
185
183
|
const previous = owners.get(operationId);
|
|
186
184
|
if (previous) {
|
|
187
185
|
if (previous.operation.operationId !== undefined) throw new ZopiaError('ZOPIA_SPEC_INVALID', `Duplicate operationId: ${operationId}`);
|
|
188
|
-
|
|
189
|
-
while (ids.has(replacement)) replacement = `${operationId}${++suffix}`;
|
|
186
|
+
const replacement = uniqueEndpointName(operationId, ids);
|
|
190
187
|
ids.delete(previous.operationId); owners.delete(previous.operationId);
|
|
191
188
|
previous.operationId = replacement; ids.add(replacement); owners.set(replacement, previous);
|
|
192
189
|
}
|
|
193
190
|
} else {
|
|
194
|
-
|
|
195
|
-
operationId = base; let suffix = 1;
|
|
196
|
-
while (ids.has(operationId)) operationId = `${base}${++suffix}`;
|
|
191
|
+
operationId = uniqueEndpointName(`${method}${pascalPath(name)}`, ids);
|
|
197
192
|
}
|
|
198
193
|
const collected = { path: name, method, operation, operationId, parameters: mergedParameters };
|
|
199
194
|
ids.add(operationId); owners.set(operationId, collected); operations.push(collected);
|