@shipworthy/openapi-transformer 0.0.2 → 1.0.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.
Files changed (32) hide show
  1. package/README.md +19 -3
  2. package/dist/case.d.ts +15 -0
  3. package/dist/case.js +153 -0
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/transformers/3.1/callbacks.d.ts +463 -465
  7. package/dist/transformers/3.1/callbacks.js +4 -4
  8. package/dist/transformers/3.1/content.js +4 -4
  9. package/dist/transformers/3.1/examples.d.ts +181 -183
  10. package/dist/transformers/3.1/examples.js +4 -4
  11. package/dist/transformers/3.1/extensions.d.ts +1 -1
  12. package/dist/transformers/3.1/extensions.js +2 -1
  13. package/dist/transformers/3.1/headers.d.ts +37 -39
  14. package/dist/transformers/3.1/headers.js +3 -3
  15. package/dist/transformers/3.1/index.d.ts +3700 -3712
  16. package/dist/transformers/3.1/index.js +2 -2
  17. package/dist/transformers/3.1/operation.d.ts +12 -8
  18. package/dist/transformers/3.1/operation.js +1 -2
  19. package/dist/transformers/3.1/parameters.d.ts +212 -214
  20. package/dist/transformers/3.1/parameters.js +2 -2
  21. package/dist/transformers/3.1/paths.d.ts +979 -977
  22. package/dist/transformers/3.1/paths.js +5 -4
  23. package/dist/transformers/3.1/pointer.js +5 -5
  24. package/dist/transformers/3.1/request.d.ts +176 -178
  25. package/dist/transformers/3.1/request.js +3 -3
  26. package/dist/transformers/3.1/responses.d.ts +134 -138
  27. package/dist/transformers/3.1/responses.js +5 -5
  28. package/dist/transformers/3.1/securities.d.ts +108 -110
  29. package/dist/transformers/3.1/securities.js +4 -4
  30. package/dist/transformers/3.1/servers.d.ts +14 -18
  31. package/dist/transformers/3.1/servers.js +2 -2
  32. package/package.json +6 -6
package/README.md CHANGED
@@ -14,9 +14,9 @@ bun add @shipworthy/openapi-transformer @shipworthy/openapi-parser effect @effec
14
14
 
15
15
  Requirements:
16
16
 
17
- - **`effect` `^4.0.0`.** It is a dependency. `decode` returns an `Effect` and the model types are Effect `Schema`s, so add a compatible version to your app so both resolve to one copy of `effect` to run and compose them.
17
+ - **`effect` `4.1.4`.** It is a dependency. `decode` returns an `Effect` and the model types are Effect `Schema`s, so add a compatible version to your app so both resolve to one copy of `effect` to run and compose them.
18
18
  - **`@shipworthy/openapi-parser`.** It is a dependency. You install it yourself to produce the input document, and its `bundleDocument` / `ParsedOpenApiFromString` are the usual entry points.
19
- - **`@effect/platform-node` `^4.0.0`** (or `@effect/platform-bun`), only because the quick start imports `NodeServices` to provide services to the parser. This package does not depend on it.
19
+ - **`@effect/platform-node` `4.1.4`** (or `@effect/platform-bun`), only because the quick start imports `NodeServices` to provide services to the parser. This package does not depend on it.
20
20
  - **ESM only** (`"type": "module"`). There is no CommonJS build. The package ships compiled JavaScript with `.d.ts` declarations, so no TypeScript loader or bundler is needed.
21
21
  - **Node.js `>=22.19.0` or Bun.**
22
22
 
@@ -90,6 +90,8 @@ The package has a single entry point, `@shipworthy/openapi-transformer`. It expo
90
90
  | `ParsedService` and the other `Parsed*` names | `Schema` + type | The model. See [Model](#model). |
91
91
  | `HttpMethodName`, `ParamStyle`, `ParameterLocation`, `SecurityDeclaration` | `Schema` + literal type | Enumerations used in the model. |
92
92
  | `httpMethods`, `parameterLocations` | `readonly` tuple | The lowercase method keys and the parameter locations, in model order. |
93
+ | `propertyName`, `collidingProperties`, `schemaPropertyCollision` | functions | Shared field naming and collision checks for generators. |
94
+ | `camelCaseSchema` | function | Rename snake_case property names throughout a schema tree. |
93
95
 
94
96
  Each `Parsed*` name is exported both as a value (an Effect `Schema`) and as a type (`typeof X.Type`). Use `import type { ParsedOperation }` when you only need the type.
95
97
 
@@ -180,6 +182,20 @@ const check = Schema.toStandardSchemaV1(ParsedService);
180
182
  check["~standard"].validate(model); // { value } or { issues }
181
183
  ```
182
184
 
185
+ ### `camelCaseSchema(schema)`
186
+
187
+ ```ts
188
+ const camelCaseSchema: (schema: ParsedSchema) => ParsedSchema;
189
+ ```
190
+
191
+ Returns a copy of a schema tree in which lower snake_case property names become camelCase, using `propertyName(name, true)`. Names that are not lower snake_case, such as `already_Camel`, `_private`, `UPPER_CASE`, or `a`, are unchanged. The input is not modified, and booleans are returned as they are.
192
+
193
+ - Renamed: keys of `properties` and `dependentSchemas`, string entries of `required`, keys and value entries of `dependentRequired`, and `discriminator.propertyName`. `discriminator.mapping` is unchanged.
194
+ - Recursed into: `properties`, `patternProperties`, `dependentSchemas`, `$defs`, `additionalProperties`, `unevaluatedProperties`, `items`, `unevaluatedItems`, `contains`, `propertyNames`, `not`, `if`, `then`, `else`, `allOf`, `anyOf`, `oneOf`, and `prefixItems`. A value that is not an object or a boolean is left as it is.
195
+ - Unchanged: `patternProperties` keys, `$ref`, `enum`, `const`, `default`, `example`, `examples`, `x-*` extensions, and every other keyword.
196
+
197
+ It does not resolve `$ref`s and does not detect collisions. Call `schemaPropertyCollision(schema, resolve)` first and fail on its result.
198
+
183
199
  ## Model
184
200
 
185
201
  These conventions apply across the whole model:
@@ -394,7 +410,7 @@ function requiresAuth(operation: ParsedOperation) {
394
410
 
395
411
  ## Key facts (for AI agents)
396
412
 
397
- - Import from `@shipworthy/openapi-transformer`. It has one entry point, is ESM-only, ships compiled JavaScript, depends on `effect` `^4.0.0`, and runs on Node.js `>=22.19.0` or Bun.
413
+ - Import from `@shipworthy/openapi-transformer`. It has one entry point, is ESM-only, ships compiled JavaScript, depends on `effect` `4.1.4`, and runs on Node.js `>=22.19.0` or Bun.
398
414
  - Pipeline: `bundleDocument(source)` (from `@shipworthy/openapi-parser`, needs FileSystem/Path/HttpClient layers) → `decode(document)` → `ParsedService`.
399
415
  - `decode` is `Schema.decodeEffect(ParsedServiceFromParsedOpenApi)`. It returns `Effect<ParsedService, Schema.SchemaError>` and needs no services. Run it with `Effect.runSync` or `Effect.runPromise`.
400
416
  - Generators and plugins must type their input as `ParsedService` and walk it through the exported `Parsed*` types. Schema nodes are `ParsedSchema` (`JsonSchema | boolean`). Do not use a local `{ [key: string]: any }` stand-in.
package/dist/case.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { ParsedSchema } from "./model/schema.ts";
2
+ export declare function propertyName(name: string, camelCase: boolean): string;
3
+ export declare function collidingProperties(names: readonly string[], camelCase: boolean): readonly [string, string] | undefined;
4
+ /** Find a name collision at any object depth reached through composition and references. */
5
+ export declare function schemaPropertyCollision(schema: ParsedSchema, resolve: (ref: string) => ParsedSchema | undefined, extraNames?: readonly string[]): readonly [string, string] | undefined;
6
+ /**
7
+ * Rename snake_case property names to camelCase throughout a schema tree.
8
+ *
9
+ * Renames `properties` keys, `required` entries, `dependentRequired` keys and values,
10
+ * `dependentSchemas` keys, and `discriminator.propertyName` with `propertyName(name, true)`, and
11
+ * recurses into every subschema. Values such as `enum`, `const`, `default`, and `examples`,
12
+ * `patternProperties` keys, and `$ref`s keep their spelling. References are not resolved and
13
+ * collisions are not detected; use `schemaPropertyCollision` first. The input is not modified.
14
+ */
15
+ export declare function camelCaseSchema(schema: ParsedSchema): ParsedSchema;
package/dist/case.js ADDED
@@ -0,0 +1,153 @@
1
+ import { Array, Predicate, Record, Schema, String } from "effect";
2
+ import { ParsedSchema } from "./model/schema.js";
3
+ const isParsedSchema = Schema.is(ParsedSchema);
4
+ const snakeCase = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
5
+ export function propertyName(name, camelCase) {
6
+ return camelCase && snakeCase.test(name) ? String.snakeToCamel(name) : name;
7
+ }
8
+ export function collidingProperties(names, camelCase) {
9
+ if (!camelCase)
10
+ return undefined;
11
+ const seen = new Map();
12
+ return Array.reduce(names, undefined, (collision, name) => {
13
+ if (collision)
14
+ return collision;
15
+ const transformed = propertyName(name, true);
16
+ const previous = seen.get(transformed);
17
+ seen.set(transformed, name);
18
+ return previous !== undefined && previous !== name ? [previous, name] : undefined;
19
+ });
20
+ }
21
+ function schemaEntries(value) {
22
+ return Predicate.isObject(value)
23
+ ? Array.filter(Record.toEntries(value), (entry) => isParsedSchema(entry[1]))
24
+ : [];
25
+ }
26
+ function schemaList(value) {
27
+ const candidates = Array.isArray(value) ? value : [];
28
+ return Array.filter(candidates, (candidate) => isParsedSchema(candidate));
29
+ }
30
+ /** Find a name collision at any object depth reached through composition and references. */
31
+ export function schemaPropertyCollision(schema, resolve, extraNames = []) {
32
+ const schemaIds = new WeakMap();
33
+ const inspected = new Set();
34
+ let nextId = 0;
35
+ const expand = (current, visitedRefs) => {
36
+ if (!Predicate.isObject(current))
37
+ return [];
38
+ const ref = Predicate.isString(current.$ref) ? current.$ref : undefined;
39
+ const referenced = ref && !visitedRefs.has(ref) ? resolve(ref) : undefined;
40
+ const nextRefs = ref ? new Set(visitedRefs).add(ref) : visitedRefs;
41
+ const variants = Array.flatMap(["allOf", "anyOf", "oneOf"], (keyword) => schemaList(current[keyword]));
42
+ return [
43
+ current,
44
+ ...Array.flatMap(referenced === undefined ? variants : [referenced, ...variants], (member) => expand(member, nextRefs)),
45
+ ];
46
+ };
47
+ const inspect = (schemas, additionalNames = []) => {
48
+ const expanded = Array.flatMap(schemas, (member) => expand(member, new Set()));
49
+ if (expanded.length === 0)
50
+ return undefined;
51
+ const signature = Array.join(Array.map(expanded, (member) => {
52
+ let id = schemaIds.get(member);
53
+ if (id === undefined) {
54
+ id = ++nextId;
55
+ schemaIds.set(member, id);
56
+ }
57
+ return `${id}`;
58
+ }), ",");
59
+ if (inspected.has(signature))
60
+ return undefined;
61
+ inspected.add(signature);
62
+ const fields = Array.flatMap(expanded, (member) => schemaEntries(member.properties));
63
+ const collision = collidingProperties([...Array.map(fields, ([name]) => name), ...additionalNames], true);
64
+ if (collision)
65
+ return collision;
66
+ const grouped = new Map();
67
+ Array.forEach(fields, ([name, nested]) => grouped.set(name, Array.append(grouped.get(name) ?? [], nested)));
68
+ const nestedCollision = Array.reduce(grouped.values(), undefined, (found, group) => found ?? (group.length > 1 ? inspect(group) : undefined));
69
+ if (nestedCollision)
70
+ return nestedCollision;
71
+ const items = Array.flatMap(expanded, (member) => schemaList(member.items));
72
+ const itemCollision = items.length > 1 ? inspect(items) : undefined;
73
+ if (itemCollision)
74
+ return itemCollision;
75
+ const additional = Array.flatMap(expanded, (member) => schemaList(member.additionalProperties));
76
+ return additional.length > 1 ? inspect(additional) : undefined;
77
+ };
78
+ return inspect([schema], extraNames);
79
+ }
80
+ const schemaMapKeywords = new Set(["properties", "patternProperties", "dependentSchemas", "$defs"]);
81
+ const schemaKeywords = new Set([
82
+ "additionalProperties",
83
+ "unevaluatedProperties",
84
+ "items",
85
+ "unevaluatedItems",
86
+ "contains",
87
+ "propertyNames",
88
+ "not",
89
+ "if",
90
+ "then",
91
+ "else",
92
+ ]);
93
+ const schemaListKeywords = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
94
+ /** A subschema position holds an object or a boolean; anything else is left alone. */
95
+ function camelCaseSubschema(value) {
96
+ return Predicate.isBoolean(value) || Predicate.isObject(value) ? camelCaseSchema(value) : value;
97
+ }
98
+ function camelCaseName(value) {
99
+ return Predicate.isString(value) ? propertyName(value, true) : value;
100
+ }
101
+ function camelCaseNames(value) {
102
+ return Array.isArray(value) ? Array.map(value, camelCaseName) : value;
103
+ }
104
+ function mapEntries(value, f) {
105
+ return Predicate.isObject(value)
106
+ ? Record.fromEntries(Array.map(Record.toEntries(value), f))
107
+ : value;
108
+ }
109
+ function camelCaseKeyword(keyword, value) {
110
+ switch (keyword) {
111
+ case "properties":
112
+ case "dependentSchemas":
113
+ return mapEntries(value, ([name, part]) => [
114
+ propertyName(name, true),
115
+ camelCaseSubschema(part),
116
+ ]);
117
+ case "required":
118
+ return camelCaseNames(value);
119
+ case "dependentRequired":
120
+ return mapEntries(value, ([name, names]) => [
121
+ propertyName(name, true),
122
+ camelCaseNames(names),
123
+ ]);
124
+ case "discriminator":
125
+ return Predicate.isObject(value) && Predicate.isString(value.propertyName)
126
+ ? { ...value, propertyName: propertyName(value.propertyName, true) }
127
+ : value;
128
+ }
129
+ if (schemaMapKeywords.has(keyword))
130
+ return mapEntries(value, ([name, part]) => [name, camelCaseSubschema(part)]);
131
+ if (schemaKeywords.has(keyword))
132
+ return camelCaseSubschema(value);
133
+ if (schemaListKeywords.has(keyword))
134
+ return Array.isArray(value) ? Array.map(value, camelCaseSubschema) : value;
135
+ return value;
136
+ }
137
+ /**
138
+ * Rename snake_case property names to camelCase throughout a schema tree.
139
+ *
140
+ * Renames `properties` keys, `required` entries, `dependentRequired` keys and values,
141
+ * `dependentSchemas` keys, and `discriminator.propertyName` with `propertyName(name, true)`, and
142
+ * recurses into every subschema. Values such as `enum`, `const`, `default`, and `examples`,
143
+ * `patternProperties` keys, and `$ref`s keep their spelling. References are not resolved and
144
+ * collisions are not detected; use `schemaPropertyCollision` first. The input is not modified.
145
+ */
146
+ export function camelCaseSchema(schema) {
147
+ if (Predicate.isBoolean(schema))
148
+ return schema;
149
+ return Record.fromEntries(Array.map(Record.toEntries(schema), ([keyword, value]) => [
150
+ keyword,
151
+ camelCaseKeyword(keyword, value),
152
+ ]));
153
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { decode, ParsedServiceFromParsedOpenApi } from "./decode.ts";
2
+ export { camelCaseSchema, collidingProperties, propertyName, schemaPropertyCollision, } from "./case.ts";
2
3
  export * from "./model/index.ts";
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export { decode, ParsedServiceFromParsedOpenApi } from "./decode.js";
2
+ export { camelCaseSchema, collidingProperties, propertyName, schemaPropertyCollision, } from "./case.js";
2
3
  export * from "./model/index.js";