@appweaver/client 1.3.0 → 1.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.
Files changed (43) hide show
  1. package/cjs/clients/modules/resource-client.d.ts +12 -11
  2. package/cjs/clients/modules/resource-client.js +6 -6
  3. package/cjs/commands/generate-command.js +3 -1
  4. package/cjs/constants.d.ts +9 -0
  5. package/cjs/constants.js +28 -1
  6. package/cjs/generators/generate-types.d.ts +3 -1
  7. package/cjs/generators/generate-types.js +99 -9
  8. package/cjs/index.d.ts +1 -0
  9. package/cjs/index.js +1 -0
  10. package/cjs/types/generator.d.ts +6 -0
  11. package/cjs/types/generator.js +2 -0
  12. package/cjs/types/index.d.ts +2 -0
  13. package/cjs/types/index.js +2 -0
  14. package/cjs/types/resource.d.ts +2 -0
  15. package/cjs/types/resource.js +2 -0
  16. package/cjs/utils/enum-util.d.ts +17 -17
  17. package/cjs/utils/enum-util.js +50 -170
  18. package/cjs/utils/hoist-util.d.ts +29 -0
  19. package/cjs/utils/hoist-util.js +338 -0
  20. package/cjs/utils/index.d.ts +1 -0
  21. package/cjs/utils/index.js +1 -0
  22. package/esm/clients/modules/resource-client.d.ts +12 -11
  23. package/esm/clients/modules/resource-client.js +6 -6
  24. package/esm/commands/generate-command.js +3 -1
  25. package/esm/constants.d.ts +9 -0
  26. package/esm/constants.js +27 -0
  27. package/esm/generators/generate-types.d.ts +3 -1
  28. package/esm/generators/generate-types.js +100 -10
  29. package/esm/index.d.ts +1 -0
  30. package/esm/index.js +1 -0
  31. package/esm/types/generator.d.ts +6 -0
  32. package/esm/types/generator.js +1 -0
  33. package/esm/types/index.d.ts +2 -0
  34. package/esm/types/index.js +2 -0
  35. package/esm/types/resource.d.ts +2 -0
  36. package/esm/types/resource.js +1 -0
  37. package/esm/utils/enum-util.d.ts +17 -17
  38. package/esm/utils/enum-util.js +49 -169
  39. package/esm/utils/hoist-util.d.ts +29 -0
  40. package/esm/utils/hoist-util.js +335 -0
  41. package/esm/utils/index.d.ts +1 -0
  42. package/esm/utils/index.js +1 -0
  43. package/package.json +3 -2
@@ -1,179 +1,59 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hoistSharedEnums = hoistSharedEnums;
4
- const constants_1 = require("../constants");
3
+ exports.rewriteEnumsAsObjects = rewriteEnumsAsObjects;
5
4
  /**
6
- * Hoists the inline enums a schema repeats across its definitions into shared definitions
7
- * of their own, replacing every occurrence with a reference to the hoisted definition.
8
- *
9
- * An Appweaver schema declares the same enum inline over and over, once per property that
10
- * accepts it, and each of them would otherwise become an enum of its own in the generated
11
- * types (i.e. a separate `asc | desc` enum for every sortable field of every resource).
12
- *
13
- * Only enums that are byte for byte identical are hoisted, so no property loses its
14
- * description, example or nullability along the way. The hoisted definition is named after
15
- * {@link SHARED_ENUM_NAMES} when its values are well known, and otherwise after the part
16
- * the declaring definitions have in common followed by the property name (i.e. `PostCreate`
17
- * and `PostSingle` declaring `status` give `PostStatus`). Enums whose name cannot be
18
- * resolved, or whose name is already taken, are left inline.
19
- *
20
- * The given schema is mutated in place.
21
- *
22
- * @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared enums of.
23
- * @return {string[]} The names of the hoisted definitions, in the order they were created.
24
- */
25
- function hoistSharedEnums(schema) {
26
- const definitions = schema.components?.schemas;
27
- if (!definitions) {
28
- return [];
29
- }
30
- // Enums declared by identical schema content, keyed by that content
31
- const groups = new Map();
32
- for (const [key, definition] of Object.entries(definitions)) {
33
- if (definition && typeof definition === 'object') {
34
- const owner = definition['title'];
35
- collectEnums(definition, typeof owner === 'string' ? owner : key, '', groups);
36
- }
37
- }
38
- const takenNames = new Set(Object.keys(definitions));
39
- for (const definition of Object.values(definitions)) {
40
- const title = definition?.['title'];
41
- if (typeof title === 'string') {
42
- takenNames.add(title);
43
- }
44
- }
45
- const hoistedNames = [];
46
- for (const [content, occurrences] of groups) {
47
- if (occurrences.length < 2) {
48
- continue;
49
- }
50
- const name = resolveEnumName(JSON.parse(content), occurrences);
51
- if (!name || takenNames.has(name)) {
52
- continue;
53
- }
54
- takenNames.add(name);
55
- hoistedNames.push(name);
56
- definitions[name] = { title: name, ...JSON.parse(content) };
57
- for (const { container, key } of occurrences) {
58
- container[key] = {
59
- $ref: `#/components/schemas/${name}`
60
- };
61
- }
62
- }
63
- return hoistedNames;
64
- }
65
- /**
66
- * Collects every inline enum declared below the given schema node into the groups map,
67
- * keyed by the stable serialization of the enum schema, so identical enums group together.
68
- *
69
- * @param {SchemaContainer} container The schema node to collect the enums of.
70
- * @param {string} owner The name of the definition the node belongs to.
71
- * @param {string} property The name of the property the node is declared under, if any.
72
- * @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
73
- */
74
- function collectEnums(container, owner, property, groups) {
75
- const entries = Array.isArray(container)
76
- ? container.map((value, index) => [index, value])
77
- : Object.entries(container);
78
- for (const [key, value] of entries) {
79
- if (!value || typeof value !== 'object') {
80
- continue;
81
- }
82
- // The keys of a `properties` object name the schemas below them, everything
83
- // else keeps the property name of the node it was reached through
84
- if (key === 'properties' && !Array.isArray(value)) {
85
- const properties = value;
86
- for (const name of Object.keys(properties)) {
87
- visitSchema(properties, name, owner, name, groups);
88
- }
89
- continue;
90
- }
91
- visitSchema(container, key, owner, property, groups);
92
- }
93
- }
94
- /**
95
- * Records the schema held by the container under the given key as an enum occurrence, or
96
- * descends into it when it declares no enum of its own.
97
- *
98
- * @param {SchemaContainer} container The container holding the schema node.
99
- * @param {string | number} key The key the schema node is held under.
100
- * @param {string} owner The name of the definition the node belongs to.
101
- * @param {string} property The name of the property the node is declared under, if any.
102
- * @param {Map<string, EnumOccurrence[]>} groups The collected occurrences, grouped by content.
103
- */
104
- function visitSchema(container, key, owner, property, groups) {
105
- const node = container[key];
106
- if (Array.isArray(node['enum'])) {
107
- const content = stableStringify(node);
108
- const occurrences = groups.get(content) ?? [];
109
- occurrences.push({ container, key, owner, property });
110
- groups.set(content, occurrences);
111
- return;
112
- }
113
- collectEnums(node, owner, property, groups);
114
- }
115
- /**
116
- * Resolves the name of the definition an enum is hoisted into, preferring the name given to
117
- * its values by {@link SHARED_ENUM_NAMES} and falling back to the common part of the names of
118
- * the definitions declaring it, followed by the property name they all declare it under.
119
- *
120
- * @param {Record<string, unknown>} node The enum schema to resolve the name of.
121
- * @param {EnumOccurrence[]} occurrences The places the enum is declared in.
122
- * @return {string | undefined} The resolved name, or undefined when the enum has no name to
123
- * be hoisted under.
124
- */
125
- function resolveEnumName(node, occurrences) {
126
- const values = node['enum'];
127
- const knownName = constants_1.SHARED_ENUM_NAMES[values.join('|')];
128
- if (knownName) {
129
- return knownName;
130
- }
131
- const { property } = occurrences[0];
132
- if (!property || occurrences.some((o) => o.property !== property)) {
133
- return undefined;
134
- }
135
- const prefix = commonNamePrefix(occurrences.map((o) => o.owner));
136
- if (!prefix) {
137
- return undefined;
138
- }
139
- return prefix + property.charAt(0).toUpperCase() + property.slice(1);
140
- }
141
- /**
142
- * Resolves the longest prefix the given names share, cut at a word boundary so the result
143
- * stays a readable name (i.e. `HealthCheckResponse` and `HealthCheckResult` give
144
- * `HealthCheck` rather than `HealthCheckRes`).
145
- *
146
- * @param {string[]} names The names to find the common prefix of.
147
- * @return {string} The common prefix, empty when the names start with different words.
5
+ * Rewrites the TypeScript enums of the generated types into a constant object holding the
6
+ * members and a type alias of their values, so both forms are accepted wherever the enum is
7
+ * used (i.e. `SortDirection.asc` and the plain `'asc'` literal alike).
8
+ *
9
+ * A TypeScript `enum` declares a nominal type, which rejects the very literals it is built
10
+ * from, forcing users of the generated client to import the enum for a value the API
11
+ * documents as a string. The rewritten declaration keeps the member access working while
12
+ * typing the property as the union of its values:
13
+ *
14
+ * ```ts
15
+ * export const SortDirection = { asc: 'asc', desc: 'desc' } as const;
16
+ * export type SortDirection = (typeof SortDirection)[keyof typeof SortDirection];
17
+ * ```
18
+ *
19
+ * @param {string} content The generated TypeScript types to rewrite the enums of.
20
+ * @param {boolean} [declaration=false] Whether the types are emitted into a declaration
21
+ * (`.d.ts`) file, which holds no runtime values and so declares the constant instead of
22
+ * initializing it.
23
+ * @return {string} The types with every enum declaration rewritten.
148
24
  */
149
- function commonNamePrefix(names) {
150
- const wordsPerName = names.map((name) => name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? []);
151
- const words = [];
152
- for (let i = 0; i < wordsPerName[0].length; i++) {
153
- const word = wordsPerName[0][i];
154
- if (wordsPerName.some((other) => other[i] !== word)) {
155
- break;
156
- }
157
- words.push(word);
158
- }
159
- return words.join('');
25
+ function rewriteEnumsAsObjects(content, declaration = false) {
26
+ return content.replace(/export enum (\w+) \{([^}]*)}/g, (match, name, body) => {
27
+ const members = parseEnumMembers(body);
28
+ if (members.length === 0) {
29
+ return match;
30
+ }
31
+ const values = members
32
+ .map(([member, value]) => declaration
33
+ ? ` readonly ${member}: ${value};`
34
+ : ` ${member}: ${value},`)
35
+ .join('\n');
36
+ const constant = declaration
37
+ ? `export declare const ${name}: {\n${values}\n};`
38
+ : `export const ${name} = {\n${values}\n} as const;`;
39
+ return `${constant}\nexport type ${name} = (typeof ${name})[keyof typeof ${name}];`;
40
+ });
160
41
  }
161
42
  /**
162
- * Serializes a value with its object keys sorted, so two schemas holding the same content
163
- * in a different key order serialize alike.
43
+ * Parses the members of an enum declaration body into their name and value pairs, keeping
44
+ * the value verbatim so a quoted member name or a numeric value survives the rewrite.
164
45
  *
165
- * @param {unknown} value The value to serialize.
166
- * @return {string} The stable JSON serialization of the value.
46
+ * @param {string} body The body of the enum declaration, without the enclosing braces.
47
+ * @return {[string, string][]} The name and value of every member, in declaration order.
167
48
  */
168
- function stableStringify(value) {
169
- if (Array.isArray(value)) {
170
- return `[${value.map(stableStringify).join(',')}]`;
171
- }
172
- if (value && typeof value === 'object') {
173
- const entries = Object.keys(value)
174
- .sort()
175
- .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
176
- return `{${entries.join(',')}}`;
177
- }
178
- return JSON.stringify(value) ?? 'null';
49
+ function parseEnumMembers(body) {
50
+ const members = [];
51
+ // A member name, quoted or not, followed by its value, which is a quoted string
52
+ // (holding a comma of its own or not) or a number
53
+ const pattern = /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[\w$]+)\s*=\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|-?[\d.]+)/g;
54
+ let member;
55
+ while ((member = pattern.exec(body)) !== null) {
56
+ members.push([member[1], member[2]]);
57
+ }
58
+ return members;
179
59
  }
@@ -0,0 +1,29 @@
1
+ import { OpenAPI3 } from 'openapi-typescript';
2
+ /**
3
+ * Hoists the schemas an OpenAPI document repeats inline across its definitions into shared
4
+ * definitions of their own, replacing every occurrence with a reference to the hoisted
5
+ * definition.
6
+ *
7
+ * An Appweaver schema declares the same shapes over and over, once per property that accepts
8
+ * them, and each of them would otherwise be spelled out again in the generated types (i.e. the
9
+ * `asc | desc` enum of every sortable field, or the accepted value of every filterable field of
10
+ * every resource). Two kinds of schema are hoisted:
11
+ *
12
+ * - The well known shapes of {@link SHARED_SCHEMA_SHAPES}, matched in the order they are
13
+ * declared so an outer shape is hoisted before the shapes nested inside it.
14
+ * - The enums the definitions repeat, named after {@link SHARED_ENUM_NAMES} when their values
15
+ * are well known, and otherwise after the part the declaring definitions have in common
16
+ * followed by the property name (i.e. `PostCreate` and `PostSingle` declaring `status` give
17
+ * `PostStatus`). Enums whose name cannot be resolved are left inline.
18
+ *
19
+ * Only the structure of a schema decides whether it is hoisted, and every occurrence keeps the
20
+ * documentation it carries, so no property loses its description or example along the way.
21
+ *
22
+ * A schema declared only once, or whose name is already taken, is left inline as well.
23
+ *
24
+ * The given schema is mutated in place.
25
+ *
26
+ * @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared definitions of.
27
+ * @return {string[]} The names of the hoisted definitions, in the order they were created.
28
+ */
29
+ export declare function hoistSharedTypes(schema: OpenAPI3): string[];
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hoistSharedTypes = hoistSharedTypes;
4
+ const constants_1 = require("../constants");
5
+ /** Keys documenting a schema node rather than describing its structure. They are kept on the
6
+ * property the node is hoisted out of, so no property loses its description along the way. */
7
+ const ANNOTATION_KEYS = ['description', 'example', 'examples', 'deprecated'];
8
+ /**
9
+ * Hoists the schemas an OpenAPI document repeats inline across its definitions into shared
10
+ * definitions of their own, replacing every occurrence with a reference to the hoisted
11
+ * definition.
12
+ *
13
+ * An Appweaver schema declares the same shapes over and over, once per property that accepts
14
+ * them, and each of them would otherwise be spelled out again in the generated types (i.e. the
15
+ * `asc | desc` enum of every sortable field, or the accepted value of every filterable field of
16
+ * every resource). Two kinds of schema are hoisted:
17
+ *
18
+ * - The well known shapes of {@link SHARED_SCHEMA_SHAPES}, matched in the order they are
19
+ * declared so an outer shape is hoisted before the shapes nested inside it.
20
+ * - The enums the definitions repeat, named after {@link SHARED_ENUM_NAMES} when their values
21
+ * are well known, and otherwise after the part the declaring definitions have in common
22
+ * followed by the property name (i.e. `PostCreate` and `PostSingle` declaring `status` give
23
+ * `PostStatus`). Enums whose name cannot be resolved are left inline.
24
+ *
25
+ * Only the structure of a schema decides whether it is hoisted, and every occurrence keeps the
26
+ * documentation it carries, so no property loses its description or example along the way.
27
+ *
28
+ * A schema declared only once, or whose name is already taken, is left inline as well.
29
+ *
30
+ * The given schema is mutated in place.
31
+ *
32
+ * @param {OpenAPI3} schema The OpenAPI v3 schema to hoist the shared definitions of.
33
+ * @return {string[]} The names of the hoisted definitions, in the order they were created.
34
+ */
35
+ function hoistSharedTypes(schema) {
36
+ const definitions = schema.components?.schemas;
37
+ if (!definitions) {
38
+ return [];
39
+ }
40
+ const takenNames = new Set(Object.keys(definitions));
41
+ for (const definition of Object.values(definitions)) {
42
+ const title = definition?.['title'];
43
+ if (typeof title === 'string') {
44
+ takenNames.add(title);
45
+ }
46
+ }
47
+ const hoistedNames = [];
48
+ for (const { name, schema: shape } of constants_1.SHARED_SCHEMA_SHAPES) {
49
+ if (hoistShape(definitions, name, shape, takenNames)) {
50
+ hoistedNames.push(name);
51
+ }
52
+ }
53
+ hoistedNames.push(...hoistEnums(definitions, takenNames));
54
+ return hoistedNames;
55
+ }
56
+ /**
57
+ * Hoists every occurrence of a well known shape into a definition of the given name. The
58
+ * definition is built from the first occurrence, so it holds the references the shape declares
59
+ * by title as the references of the document.
60
+ *
61
+ * @param {Record<string, unknown>} definitions The definitions of the schema.
62
+ * @param {string} name The name of the definition the shape is hoisted into.
63
+ * @param {unknown} shape The shape to hoist, holding its references as definition titles.
64
+ * @param {Set<string>} takenNames The definition names already in use.
65
+ * @return {boolean} Whether the shape was hoisted.
66
+ */
67
+ function hoistShape(definitions, name, shape, takenNames) {
68
+ const titles = definitionTitles(definitions);
69
+ const signature = schemaSignature(shape, titles);
70
+ const groups = collectOccurrences(definitions, (node) => schemaSignature(structure(node), titles) === signature ? name : undefined);
71
+ const occurrences = groups.get(name);
72
+ if (!occurrences || occurrences.length < 2 || takenNames.has(name)) {
73
+ return false;
74
+ }
75
+ takenNames.add(name);
76
+ const { container, key } = occurrences[0];
77
+ definitions[name] = {
78
+ title: name,
79
+ ...structure(container[key])
80
+ };
81
+ for (const occurrence of occurrences) {
82
+ replaceWithReference(occurrence, name);
83
+ }
84
+ return true;
85
+ }
86
+ /**
87
+ * Hoists the enums the definitions repeat into shared definitions named after their values.
88
+ *
89
+ * @param {Record<string, unknown>} definitions The definitions of the schema.
90
+ * @param {Set<string>} takenNames The definition names already in use.
91
+ * @return {string[]} The names of the hoisted definitions, in the order they were created.
92
+ */
93
+ function hoistEnums(definitions, takenNames) {
94
+ // Enums declared by identical schema content, keyed by that content. The
95
+ // documentation of a node is left out of the key, since every occurrence keeps
96
+ // the documentation of its own when it is replaced by a reference
97
+ const groups = collectOccurrences(definitions, (node) => Array.isArray(node['enum']) ? stableStringify(structure(node)) : undefined);
98
+ const hoistedNames = [];
99
+ for (const [content, occurrences] of groups) {
100
+ if (occurrences.length < 2) {
101
+ continue;
102
+ }
103
+ const name = resolveEnumName(JSON.parse(content), occurrences);
104
+ if (!name || takenNames.has(name)) {
105
+ continue;
106
+ }
107
+ takenNames.add(name);
108
+ hoistedNames.push(name);
109
+ definitions[name] = { title: name, ...JSON.parse(content) };
110
+ for (const occurrence of occurrences) {
111
+ replaceWithReference(occurrence, name);
112
+ }
113
+ }
114
+ return hoistedNames;
115
+ }
116
+ /**
117
+ * Replaces the schema node of an occurrence with a reference to the given definition, keeping
118
+ * the documentation the node carries.
119
+ *
120
+ * @param {Occurrence} occurrence The occurrence to replace.
121
+ * @param {string} name The name of the definition to reference.
122
+ */
123
+ function replaceWithReference({ container, key }, name) {
124
+ const node = container[key];
125
+ container[key] = {
126
+ ...annotations(node),
127
+ $ref: `#/components/schemas/${name}`
128
+ };
129
+ }
130
+ /**
131
+ * Collects every inline schema the matcher accepts into a map of occurrences, grouped by the
132
+ * key the matcher returns for them. A node the matcher accepts is not descended into, so an
133
+ * outer match wins over the nodes nested inside it.
134
+ *
135
+ * @param {Record<string, unknown>} definitions The definitions of the schema.
136
+ * @param {Matcher} match The matcher deciding which nodes are collected.
137
+ * @return {Map<string, Occurrence[]>} The collected occurrences, grouped by matcher key.
138
+ */
139
+ function collectOccurrences(definitions, match) {
140
+ const groups = new Map();
141
+ for (const [key, definition] of Object.entries(definitions)) {
142
+ if (definition && typeof definition === 'object') {
143
+ const owner = definition['title'];
144
+ collect(definition, typeof owner === 'string' ? owner : key, '', match, groups);
145
+ }
146
+ }
147
+ return groups;
148
+ }
149
+ /**
150
+ * Collects every inline schema declared below the given schema node into the groups map.
151
+ *
152
+ * @param {SchemaContainer} container The schema node to collect the schemas of.
153
+ * @param {string} owner The name of the definition the node belongs to.
154
+ * @param {string} property The name of the property the node is declared under, if any.
155
+ * @param {Matcher} match The matcher deciding which nodes are collected.
156
+ * @param {Map<string, Occurrence[]>} groups The collected occurrences, grouped by matcher key.
157
+ */
158
+ function collect(container, owner, property, match, groups) {
159
+ const entries = Array.isArray(container)
160
+ ? container.map((value, index) => [index, value])
161
+ : Object.entries(container);
162
+ for (const [key, value] of entries) {
163
+ if (!value || typeof value !== 'object') {
164
+ continue;
165
+ }
166
+ // The keys of a `properties` object name the schemas below them, everything
167
+ // else keeps the property name of the node it was reached through
168
+ if (key === 'properties' && !Array.isArray(value)) {
169
+ const properties = value;
170
+ for (const name of Object.keys(properties)) {
171
+ visit(properties, name, owner, name, match, groups);
172
+ }
173
+ continue;
174
+ }
175
+ visit(container, key, owner, property, match, groups);
176
+ }
177
+ }
178
+ /**
179
+ * Records the schema held by the container under the given key as an occurrence, or descends
180
+ * into it when the matcher does not accept it.
181
+ *
182
+ * @param {SchemaContainer} container The container holding the schema node.
183
+ * @param {string | number} key The key the schema node is held under.
184
+ * @param {string} owner The name of the definition the node belongs to.
185
+ * @param {string} property The name of the property the node is declared under, if any.
186
+ * @param {Matcher} match The matcher deciding which nodes are collected.
187
+ * @param {Map<string, Occurrence[]>} groups The collected occurrences, grouped by matcher key.
188
+ */
189
+ function visit(container, key, owner, property, match, groups) {
190
+ const node = container[key];
191
+ const group = match(node);
192
+ if (group !== undefined) {
193
+ const occurrences = groups.get(group) ?? [];
194
+ occurrences.push({ container, key, owner, property });
195
+ groups.set(group, occurrences);
196
+ return;
197
+ }
198
+ collect(node, owner, property, match, groups);
199
+ }
200
+ /**
201
+ * Resolves the name of the definition an enum is hoisted into, preferring the name given to
202
+ * its values by {@link SHARED_ENUM_NAMES} and falling back to the common part of the names of
203
+ * the definitions declaring it, followed by the property name they all declare it under.
204
+ *
205
+ * @param {SchemaNode} node The enum schema to resolve the name of.
206
+ * @param {Occurrence[]} occurrences The places the enum is declared in.
207
+ * @return {string | undefined} The resolved name, or undefined when the enum has no name to
208
+ * be hoisted under.
209
+ */
210
+ function resolveEnumName(node, occurrences) {
211
+ const values = node['enum'];
212
+ const knownName = constants_1.SHARED_ENUM_NAMES[values.join('|')];
213
+ if (knownName) {
214
+ return knownName;
215
+ }
216
+ const { property } = occurrences[0];
217
+ if (!property || occurrences.some((o) => o.property !== property)) {
218
+ return undefined;
219
+ }
220
+ const prefix = commonNamePrefix(occurrences.map((o) => o.owner));
221
+ if (!prefix) {
222
+ return undefined;
223
+ }
224
+ return prefix + property.charAt(0).toUpperCase() + property.slice(1);
225
+ }
226
+ /**
227
+ * Resolves the longest prefix the given names share, cut at a word boundary so the result
228
+ * stays a readable name (i.e. `HealthCheckResponse` and `HealthCheckResult` give
229
+ * `HealthCheck` rather than `HealthCheckRes`).
230
+ *
231
+ * @param {string[]} names The names to find the common prefix of.
232
+ * @return {string} The common prefix, empty when the names start with different words.
233
+ */
234
+ function commonNamePrefix(names) {
235
+ const wordsPerName = names.map((name) => name.match(/[A-Z]+(?![a-z])|[A-Z]?[a-z0-9]+|[A-Z]/g) ?? []);
236
+ const words = [];
237
+ for (let i = 0; i < wordsPerName[0].length; i++) {
238
+ const word = wordsPerName[0][i];
239
+ if (wordsPerName.some((other) => other[i] !== word)) {
240
+ break;
241
+ }
242
+ words.push(word);
243
+ }
244
+ return words.join('');
245
+ }
246
+ /**
247
+ * Maps every definition key to the title it is generated under, so a reference can be compared
248
+ * by the name it resolves to rather than by the generated key holding it.
249
+ *
250
+ * @param {Record<string, unknown>} definitions The definitions of the schema.
251
+ * @return {Map<string, string>} The title of every definition, keyed by its definition key.
252
+ */
253
+ function definitionTitles(definitions) {
254
+ const titles = new Map();
255
+ for (const [key, definition] of Object.entries(definitions)) {
256
+ const title = definition?.['title'];
257
+ titles.set(key, typeof title === 'string' ? title : key);
258
+ }
259
+ return titles;
260
+ }
261
+ /**
262
+ * Returns the documentation keys of a schema node, which the node keeps when it is replaced by
263
+ * a reference to a hoisted definition.
264
+ *
265
+ * @param {SchemaNode} node The schema node to take the documentation of.
266
+ * @return {SchemaNode} The documentation keys of the node.
267
+ */
268
+ function annotations(node) {
269
+ return Object.fromEntries(Object.entries(node).filter(([key]) => ANNOTATION_KEYS.includes(key)));
270
+ }
271
+ /**
272
+ * Returns the structural keys of a schema node, so two nodes describing the same shape under a
273
+ * different description compare alike.
274
+ *
275
+ * @param {SchemaNode} node The schema node to take the structure of.
276
+ * @return {SchemaNode} The structural keys of the node.
277
+ */
278
+ function structure(node) {
279
+ return Object.fromEntries(Object.entries(node).filter(([key]) => !ANNOTATION_KEYS.includes(key)));
280
+ }
281
+ /**
282
+ * Serializes a schema with its references resolved to the title of the definition they point
283
+ * to, so a shape declared by {@link SHARED_SCHEMA_SHAPES} compares equal to the same shape of a
284
+ * document, whose references name the generated definition keys instead.
285
+ *
286
+ * @param {unknown} value The schema to serialize.
287
+ * @param {Map<string, string>} titles The title of every definition, keyed by definition key.
288
+ * @return {string} The stable serialization of the schema.
289
+ */
290
+ function schemaSignature(value, titles) {
291
+ if (Array.isArray(value)) {
292
+ return `[${value.map((item) => schemaSignature(item, titles)).join(',')}]`;
293
+ }
294
+ if (value && typeof value === 'object') {
295
+ const entries = Object.keys(value)
296
+ .sort()
297
+ .map((key) => {
298
+ const nested = value[key];
299
+ const resolved = key === '$ref' && typeof nested === 'string'
300
+ ? JSON.stringify(referenceTitle(nested, titles))
301
+ : schemaSignature(nested, titles);
302
+ return `${JSON.stringify(key)}:${resolved}`;
303
+ });
304
+ return `{${entries.join(',')}}`;
305
+ }
306
+ return JSON.stringify(value) ?? 'null';
307
+ }
308
+ /**
309
+ * Resolves a reference to the title of the definition it points to.
310
+ *
311
+ * @param {string} reference The reference to resolve (i.e. `#/components/schemas/def-1`).
312
+ * @param {Map<string, string>} titles The title of every definition, keyed by definition key.
313
+ * @return {string} The title of the referenced definition, or the reference when it points
314
+ * outside of the definitions of the document.
315
+ */
316
+ function referenceTitle(reference, titles) {
317
+ const key = reference.replace('#/components/schemas/', '');
318
+ return titles.get(key) ?? reference;
319
+ }
320
+ /**
321
+ * Serializes a value with its object keys sorted, so two schemas holding the same content
322
+ * in a different key order serialize alike.
323
+ *
324
+ * @param {unknown} value The value to serialize.
325
+ * @return {string} The stable JSON serialization of the value.
326
+ */
327
+ function stableStringify(value) {
328
+ if (Array.isArray(value)) {
329
+ return `[${value.map(stableStringify).join(',')}]`;
330
+ }
331
+ if (value && typeof value === 'object') {
332
+ const entries = Object.keys(value)
333
+ .sort()
334
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`);
335
+ return `{${entries.join(',')}}`;
336
+ }
337
+ return JSON.stringify(value) ?? 'null';
338
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './enum-util';
2
+ export * from './hoist-util';
2
3
  export * from './schema-util';
@@ -15,4 +15,5 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./enum-util"), exports);
18
+ __exportStar(require("./hoist-util"), exports);
18
19
  __exportStar(require("./schema-util"), exports);