@obsidiane/meridiane 2.0.10 → 2.0.11

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 (91) hide show
  1. package/README.md +19 -2
  2. package/cli.js +21 -37
  3. package/package.json +3 -3
  4. package/src/README.md +9 -0
  5. package/{tools/app → src/application}/bridge-workflow.js +15 -13
  6. package/src/application/generate-bridge.js +50 -0
  7. package/{tools/app → src/application}/run-build.js +1 -1
  8. package/{tools/app → src/application}/run-dev.js +30 -36
  9. package/{tools/app → src/application}/run-generate.js +3 -3
  10. package/{tools → src}/domain/bridge-options.js +8 -21
  11. package/src/domain/list-values.js +6 -0
  12. package/src/generators/bridge/write-bridge-files.js +100 -0
  13. package/src/generators/models/interface-schema.js +116 -0
  14. package/src/generators/models/model-selection.js +148 -0
  15. package/src/generators/models/oas-contract.js +170 -0
  16. package/{tools/generator → src/generators}/models/oas-schema-name.js +6 -15
  17. package/src/generators/models/openapi-to-models.js +136 -0
  18. package/{tools/generator → src/generators}/models/schema-utils.js +27 -3
  19. package/src/generators/models/templates/type-alias.hbs +2 -0
  20. package/src/generators/models/type-expression.js +65 -0
  21. package/src/generators/models/type-resolver.js +240 -0
  22. package/src/generators/models/utils.js +89 -0
  23. package/src/generators/models/write-model-files.js +55 -0
  24. package/src/infrastructure/dev-project.js +48 -0
  25. package/{tools/infra → src/infrastructure}/exec.js +3 -2
  26. package/src/infrastructure/local-package-install.js +104 -0
  27. package/src/infrastructure/spec-loader.js +65 -0
  28. package/{tools/infra → src/infrastructure}/standalone-workspace.js +31 -29
  29. package/templates/{_lib_template → angular-bridge}/README.md +9 -0
  30. package/templates/angular-bridge/src/lib/bridge/rest/api-platform.adapter.ts +82 -0
  31. package/templates/angular-bridge/src/lib/bridge/rest/http-request.options.ts +30 -0
  32. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/rest/query-builder.ts +10 -10
  33. package/templates/angular-bridge/src/lib/bridge/sse/mercure-connections.ts +255 -0
  34. package/templates/angular-bridge/src/lib/bridge/sse/mercure-payload.ts +82 -0
  35. package/templates/angular-bridge/src/lib/bridge/sse/mercure-url.builder.ts +39 -0
  36. package/templates/angular-bridge/src/lib/bridge/sse/mercure.adapter.ts +358 -0
  37. package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/ref-count-topic.registry.ts +3 -12
  38. package/templates/angular-bridge/src/lib/bridge/sse/subscription-claims.ts +76 -0
  39. package/templates/angular-bridge/src/lib/bridge-options.ts +59 -0
  40. package/templates/{_lib_template → angular-bridge}/src/lib/facades/bridge.facade.ts +50 -51
  41. package/templates/{_lib_template → angular-bridge}/src/lib/facades/facade.factory.ts +0 -1
  42. package/templates/angular-bridge/src/lib/facades/facade.interface.ts +13 -0
  43. package/templates/{_lib_template → angular-bridge}/src/lib/facades/resource.facade.ts +24 -31
  44. package/templates/angular-bridge/src/lib/interceptors/bearer-auth.interceptor.ts +33 -0
  45. package/templates/angular-bridge/src/lib/interceptors/bridge-defaults.interceptor.ts +65 -0
  46. package/templates/angular-bridge/src/lib/interceptors/content-type.interceptor.ts +37 -0
  47. package/templates/angular-bridge/src/lib/interceptors/scoped.interceptor.ts +9 -0
  48. package/templates/angular-bridge/src/lib/interceptors/singleflight.interceptor.ts +46 -0
  49. package/templates/{_lib_template → angular-bridge}/src/lib/ports/realtime.port.ts +6 -2
  50. package/templates/{_lib_template → angular-bridge}/src/lib/ports/resource-repository.port.ts +7 -7
  51. package/templates/angular-bridge/src/lib/provide-bridge.ts +98 -0
  52. package/templates/angular-bridge/src/lib/utils/iri.ts +6 -0
  53. package/templates/angular-bridge/src/lib/utils/request-url.ts +31 -0
  54. package/templates/{_lib_template → angular-bridge}/src/public-api.ts +2 -0
  55. package/templates/_lib_template/src/lib/bridge/rest/api-platform.adapter.ts +0 -84
  56. package/templates/_lib_template/src/lib/bridge/rest/http-request.options.ts +0 -21
  57. package/templates/_lib_template/src/lib/bridge/sse/mercure-url.builder.ts +0 -17
  58. package/templates/_lib_template/src/lib/bridge/sse/mercure.adapter.ts +0 -806
  59. package/templates/_lib_template/src/lib/facades/facade.interface.ts +0 -31
  60. package/templates/_lib_template/src/lib/interceptors/bridge-defaults.interceptor.ts +0 -53
  61. package/templates/_lib_template/src/lib/interceptors/content-type.interceptor.ts +0 -49
  62. package/templates/_lib_template/src/lib/interceptors/singleflight.interceptor.ts +0 -55
  63. package/templates/_lib_template/src/lib/provide-bridge.ts +0 -186
  64. package/tools/build.js +0 -1
  65. package/tools/dev.js +0 -1
  66. package/tools/domain/paths.js +0 -10
  67. package/tools/generate.js +0 -1
  68. package/tools/generator/models/oas-contract.js +0 -159
  69. package/tools/generator/models/openapi-to-models.js +0 -240
  70. package/tools/generator/models/type-resolver.js +0 -107
  71. package/tools/generator/models/utils.js +0 -62
  72. package/tools/infra/dev-support.js +0 -189
  73. package/tools/infra/generate/bridge.js +0 -212
  74. package/tools/infra/spec-loader.js +0 -59
  75. /package/{tools → src}/domain/lib-name.js +0 -0
  76. /package/{tools/generator → src/generators}/models/handlebars.js +0 -0
  77. /package/{tools/generator → src/generators}/models/templates/index.hbs +0 -0
  78. /package/{tools/generator → src/generators}/models/templates/model.hbs +0 -0
  79. /package/{tools/infra → src/infrastructure}/json.js +0 -0
  80. /package/{tools/infra → src/infrastructure}/logger.js +0 -0
  81. /package/templates/{_lib_template → angular-bridge}/ng-package.json +0 -0
  82. /package/templates/{_lib_template → angular-bridge}/package.json +0 -0
  83. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/eventsource-wrapper.ts +0 -0
  84. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge/sse/mercure-topic.mapper.ts +0 -0
  85. /package/templates/{_lib_template → angular-bridge}/src/lib/bridge.types.ts +0 -0
  86. /package/templates/{_lib_template → angular-bridge}/src/lib/interceptors/bridge-debug.interceptor.ts +0 -0
  87. /package/templates/{_lib_template → angular-bridge}/src/lib/tokens.ts +0 -0
  88. /package/templates/{_lib_template → angular-bridge}/src/lib/utils/url.ts +0 -0
  89. /package/templates/{_lib_template → angular-bridge}/src/models/index.ts +0 -0
  90. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.json +0 -0
  91. /package/templates/{_lib_template → angular-bridge}/tsconfig.lib.prod.json +0 -0
@@ -0,0 +1,148 @@
1
+ import {
2
+ canonicalizeMediaType,
3
+ collectUsedSchemasForMediaType,
4
+ expandWithSchemaDependencies,
5
+ } from './oas-contract.js';
6
+ import {
7
+ baseTsNameFromBasePath,
8
+ formatSuffixToken,
9
+ groupTsNameFromGroupPath,
10
+ parseOpenApiSchemaName,
11
+ } from './oas-schema-name.js';
12
+ import {sanitizeTypeName} from './utils.js';
13
+
14
+ export const DEFAULT_MODEL_FORMAT = 'application/ld+json';
15
+
16
+ function normalizeFormats(values) {
17
+ const formats = [];
18
+ for (const value of Array.isArray(values) ? values : []) {
19
+ const format = canonicalizeMediaType(value);
20
+ if (format && !formats.includes(format)) formats.push(format);
21
+ }
22
+ return formats.length > 0 ? formats : [DEFAULT_MODEL_FORMAT];
23
+ }
24
+
25
+ function indexFormatsBySchema(spec, formats) {
26
+ const formatsBySchema = new Map();
27
+
28
+ for (const format of formats) {
29
+ const schemaNames = collectUsedSchemasForMediaType(spec, {mediaType: format});
30
+ for (const schemaName of schemaNames) {
31
+ if (!schemaName) continue;
32
+ const schemaFormats = formatsBySchema.get(schemaName) ?? new Set();
33
+ schemaFormats.add(format);
34
+ formatsBySchema.set(schemaName, schemaFormats);
35
+ }
36
+ }
37
+
38
+ return formatsBySchema;
39
+ }
40
+
41
+ function matchesRule(value, rule) {
42
+ if (rule instanceof RegExp) return rule.test(value);
43
+ if (typeof rule === 'function') return !!rule(value);
44
+ return typeof rule === 'string' && rule.length > 0 && value.includes(rule);
45
+ }
46
+
47
+ function isSelected(schemaName, include, exclude) {
48
+ const parsed = parseOpenApiSchemaName(schemaName);
49
+ const canonicalName = parsed.groupPath ? `${parsed.basePath}-${parsed.groupPath}` : parsed.basePath;
50
+ const names = [schemaName, canonicalName];
51
+ const matchesAny = (rules) => rules.some((rule) =>
52
+ names.some((name) => matchesRule(name, rule))
53
+ );
54
+
55
+ return (include.length === 0 || matchesAny(include)) && !matchesAny(exclude);
56
+ }
57
+
58
+ function selectCandidates(schemas, formatsBySchema, include, exclude) {
59
+ const selected = [...formatsBySchema.keys()]
60
+ .filter((name) => name && !/^Hydra/i.test(name))
61
+ .sort((left, right) => left.localeCompare(right))
62
+ .filter((name) => isSelected(name, include, exclude));
63
+
64
+ const candidates = include.length > 0 || exclude.length > 0
65
+ ? expandWithSchemaDependencies(selected, schemas)
66
+ : new Set(selected);
67
+
68
+ return [...candidates]
69
+ .filter((name) => name && !/^Hydra/i.test(name))
70
+ .sort((left, right) => left.localeCompare(right));
71
+ }
72
+
73
+ function choosePreferredFormats(schemaNames, formats, formatsBySchema) {
74
+ const formatPriority = new Map(formats.map((format, index) => [format, index]));
75
+ const preferredFormats = new Map();
76
+
77
+ for (const schemaName of schemaNames) {
78
+ const schemaFormats = [...(formatsBySchema.get(schemaName) ?? [])];
79
+ const preferred = schemaFormats.sort(
80
+ (left, right) => (formatPriority.get(left) ?? 999) - (formatPriority.get(right) ?? 999)
81
+ )[0] ?? formats[0];
82
+ preferredFormats.set(schemaName, preferred);
83
+ }
84
+
85
+ return {preferredFormats, formatPriority};
86
+ }
87
+
88
+ function desiredTypeName(schemaName) {
89
+ const parsed = parseOpenApiSchemaName(schemaName);
90
+ if (parsed.isMergePatch) return undefined;
91
+
92
+ const base = baseTsNameFromBasePath(parsed.basePath);
93
+ const group = groupTsNameFromGroupPath(parsed.groupPath);
94
+ return `${base}${group}` || 'Model';
95
+ }
96
+
97
+ function assignTypeNames(schemaNames, preferredFormats, formatPriority) {
98
+ const schemasByDesiredName = new Map();
99
+
100
+ for (const schemaName of schemaNames) {
101
+ const desiredName = desiredTypeName(schemaName);
102
+ if (!desiredName) continue;
103
+
104
+ const entries = schemasByDesiredName.get(desiredName) ?? [];
105
+ entries.push({schemaName, preferredFormat: preferredFormats.get(schemaName)});
106
+ schemasByDesiredName.set(desiredName, entries);
107
+ }
108
+
109
+ const usedNames = new Set();
110
+ const typeNames = new Map();
111
+ const sortedGroups = [...schemasByDesiredName.entries()]
112
+ .sort(([left], [right]) => left.localeCompare(right));
113
+
114
+ for (const [desiredName, entries] of sortedGroups) {
115
+ entries.sort((left, right) => {
116
+ const byFormat =
117
+ (formatPriority.get(left.preferredFormat) ?? 999) -
118
+ (formatPriority.get(right.preferredFormat) ?? 999);
119
+ return byFormat || left.schemaName.localeCompare(right.schemaName);
120
+ });
121
+
122
+ entries.forEach((entry, index) => {
123
+ const candidateName = index === 0
124
+ ? desiredName
125
+ : `${desiredName}${formatSuffixToken(entry.preferredFormat)}`;
126
+ typeNames.set(entry.schemaName, sanitizeTypeName(candidateName, usedNames));
127
+ });
128
+ }
129
+
130
+ return typeNames;
131
+ }
132
+
133
+ export function selectModelSchemas(spec, options = {}) {
134
+ const schemas = spec?.components?.schemas ?? {};
135
+ const formats = normalizeFormats(options.formats);
136
+ const include = Array.isArray(options.includeSchemaNames) ? options.includeSchemaNames : [];
137
+ const exclude = Array.isArray(options.excludeSchemaNames) ? options.excludeSchemaNames : [];
138
+ const formatsBySchema = indexFormatsBySchema(spec, formats);
139
+ const schemaNames = selectCandidates(schemas, formatsBySchema, include, exclude);
140
+ const {preferredFormats, formatPriority} = choosePreferredFormats(
141
+ schemaNames,
142
+ formats,
143
+ formatsBySchema
144
+ );
145
+ const typeNames = assignTypeNames(schemaNames, preferredFormats, formatPriority);
146
+
147
+ return {schemas, formats, schemaNames, preferredFormats, typeNames};
148
+ }
@@ -0,0 +1,170 @@
1
+ import {resolveLocalRef, schemaNameFromRef} from './schema-utils.js';
2
+
3
+ function deref(spec, maybeRef) {
4
+ if (!maybeRef || typeof maybeRef !== 'object') return maybeRef;
5
+ if (typeof maybeRef.$ref === 'string') return resolveLocalRef(spec, maybeRef.$ref) || maybeRef;
6
+ return maybeRef;
7
+ }
8
+
9
+ export function canonicalizeMediaType(mediaType) {
10
+ return String(mediaType || '').split(';')[0].trim().toLowerCase();
11
+ }
12
+
13
+ function getContentSchema(content, wantedLower) {
14
+ if (!content || typeof content !== 'object') return undefined;
15
+ for (const [mediaType, definition] of Object.entries(content)) {
16
+ if (canonicalizeMediaType(mediaType) === wantedLower) return definition?.schema;
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ function is2xxStatus(code) {
22
+ const status = String(code || '');
23
+ if (/^[0-9]{3}$/.test(status)) return status.startsWith('2');
24
+ return /^2xx$/i.test(status);
25
+ }
26
+
27
+ const HTTP_METHODS = new Set(['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace']);
28
+
29
+ /**
30
+ * Collects only component schema refs (`#/components/schemas/*`) found in a JSON schema.
31
+ * @param {any} schema
32
+ * @param {Set<string>} out
33
+ */
34
+ function collectComponentSchemaRefs(schema, out) {
35
+ const seen = new Set();
36
+
37
+ function walk(node) {
38
+ if (!node || typeof node !== 'object') return;
39
+ if (seen.has(node)) return;
40
+ seen.add(node);
41
+
42
+ if (node.$ref && typeof node.$ref === 'string') {
43
+ const name = schemaNameFromRef(node.$ref);
44
+ if (name) out.add(name);
45
+ }
46
+
47
+ if (Array.isArray(node)) {
48
+ for (const item of node) walk(item);
49
+ return;
50
+ }
51
+
52
+ for (const [key, value] of Object.entries(node)) {
53
+ if (key !== '$ref') walk(value);
54
+ }
55
+ }
56
+
57
+ walk(schema);
58
+ }
59
+
60
+ function listSchemaRoots(spec, {mediaType} = {}) {
61
+ const wantedLower = canonicalizeMediaType(mediaType);
62
+ const roots = [];
63
+ const visited = new Set();
64
+
65
+ function addParameter(parameterOrRef) {
66
+ const parameter = deref(spec, parameterOrRef);
67
+ if (!parameter || typeof parameter !== 'object') return;
68
+ if (parameter.schema) roots.push(parameter.schema);
69
+ const contentSchema = getContentSchema(parameter.content, wantedLower);
70
+ if (contentSchema) roots.push(contentSchema);
71
+ }
72
+
73
+ function addHeaders(headers) {
74
+ for (const header of Object.values(headers ?? {})) addParameter(header);
75
+ }
76
+
77
+ function visitOperation(method, operationOrRef) {
78
+ const operation = deref(spec, operationOrRef);
79
+ if (!operation || typeof operation !== 'object') return;
80
+ if (visited.has(operation)) return;
81
+ visited.add(operation);
82
+
83
+ for (const parameter of operation.parameters ?? []) addParameter(parameter);
84
+
85
+ if (method !== 'patch') {
86
+ const requestBody = deref(spec, operation.requestBody);
87
+ const requestSchema = getContentSchema(requestBody?.content, wantedLower);
88
+ if (requestSchema) roots.push(requestSchema);
89
+ }
90
+
91
+ for (const [code, responseOrRef] of Object.entries(operation.responses ?? {})) {
92
+ if (!is2xxStatus(code) && String(code).toLowerCase() !== 'default') continue;
93
+ const response = deref(spec, responseOrRef);
94
+ const responseSchema = getContentSchema(response?.content, wantedLower);
95
+ if (responseSchema) roots.push(responseSchema);
96
+ addHeaders(response?.headers);
97
+ }
98
+
99
+ for (const callbackOrRef of Object.values(operation.callbacks ?? {})) {
100
+ const callback = deref(spec, callbackOrRef);
101
+ for (const pathItem of Object.values(callback ?? {})) visitPathItem(pathItem);
102
+ }
103
+ }
104
+
105
+ function visitPathItem(pathItemOrRef) {
106
+ const pathItem = deref(spec, pathItemOrRef);
107
+ if (!pathItem || typeof pathItem !== 'object') return;
108
+ if (visited.has(pathItem)) return;
109
+ visited.add(pathItem);
110
+
111
+ for (const parameter of pathItem.parameters ?? []) addParameter(parameter);
112
+ for (const [method, operation] of Object.entries(pathItem)) {
113
+ const normalizedMethod = method.toLowerCase();
114
+ if (HTTP_METHODS.has(normalizedMethod)) visitOperation(normalizedMethod, operation);
115
+ }
116
+ }
117
+
118
+ function visitPaths(paths) {
119
+ for (const pathItem of Object.values(paths ?? {})) visitPathItem(pathItem);
120
+ }
121
+
122
+ visitPaths(spec?.paths);
123
+ visitPaths(spec?.webhooks);
124
+
125
+ return roots;
126
+ }
127
+
128
+ /**
129
+ * Returns the transitive closure of schema dependencies (following component-schema $ref).
130
+ * @param {Iterable<string>} start
131
+ * @param {Record<string, any>} schemas
132
+ */
133
+ export function expandWithSchemaDependencies(start, schemas) {
134
+ const out = new Set();
135
+ const queue = [];
136
+
137
+ for (const schemaName of start) {
138
+ if (!schemaName || out.has(schemaName)) continue;
139
+ out.add(schemaName);
140
+ queue.push(schemaName);
141
+ }
142
+
143
+ while (queue.length) {
144
+ const name = queue.pop();
145
+ const schema = schemas[name];
146
+ if (!schema) continue;
147
+ const refs = new Set();
148
+ collectComponentSchemaRefs(schema, refs);
149
+ for (const dependency of refs) {
150
+ if (!dependency || out.has(dependency)) continue;
151
+ out.add(dependency);
152
+ queue.push(dependency);
153
+ }
154
+ }
155
+
156
+ return out;
157
+ }
158
+
159
+ /**
160
+ * Collects all component schemas used by endpoints for one media type.
161
+ * @param {any} spec
162
+ * @param {{ mediaType: string }} cfg
163
+ */
164
+ export function collectUsedSchemasForMediaType(spec, cfg) {
165
+ const schemas = spec?.components?.schemas || {};
166
+ const roots = listSchemaRoots(spec, cfg);
167
+ const initialRefs = new Set();
168
+ for (const root of roots) collectComponentSchemaRefs(root, initialRefs);
169
+ return expandWithSchemaDependencies(initialRefs, schemas);
170
+ }
@@ -1,13 +1,5 @@
1
1
  import { canonicalizeMediaType } from './oas-contract.js';
2
-
3
- function pascalizeTokens(tokens) {
4
- return tokens
5
- .filter(Boolean)
6
- .map((t) => String(t).replace(/^[^A-Za-z0-9]+|[^A-Za-z0-9]+$/g, ''))
7
- .filter(Boolean)
8
- .map((t) => t.charAt(0).toUpperCase() + t.slice(1))
9
- .join('');
10
- }
2
+ import {pascalCase} from './utils.js';
11
3
 
12
4
  export function parseOpenApiSchemaName(originalName) {
13
5
  const raw = String(originalName || '');
@@ -50,22 +42,21 @@ export function parseOpenApiSchemaName(originalName) {
50
42
  export function baseTsNameFromBasePath(basePath) {
51
43
  const parts = String(basePath || '').split('.').filter(Boolean);
52
44
  if (parts.length === 0) return 'Model';
53
- const a = pascalizeTokens([parts[0]]);
45
+ const a = pascalCase([parts[0]]);
54
46
  if (parts.length === 1) return a;
55
- const b = pascalizeTokens([parts[1]]);
47
+ const b = pascalCase([parts[1]]);
56
48
  if (parts.length === 2 && b.toLowerCase().startsWith(a.toLowerCase())) return b;
57
- return parts.map((p) => pascalizeTokens([p])).join('');
49
+ return parts.map((part) => pascalCase([part])).join('');
58
50
  }
59
51
 
60
52
  export function groupTsNameFromGroupPath(groupPath) {
61
53
  const tokens = String(groupPath || '').split(/[._:]+/g).filter(Boolean);
62
- return pascalizeTokens(tokens);
54
+ return pascalCase(tokens);
63
55
  }
64
56
 
65
57
  export function formatSuffixToken(mediaType) {
66
58
  const canon = canonicalizeMediaType(mediaType);
67
59
  const tokens = canon.split(/[\/+.-]+/g).filter(Boolean);
68
60
  const trimmed = tokens[0] === 'application' ? tokens.slice(1) : tokens;
69
- return pascalizeTokens(trimmed.length ? trimmed : tokens) || 'Fmt';
61
+ return pascalCase(trimmed.length ? trimmed : tokens) || 'Fmt';
70
62
  }
71
-
@@ -0,0 +1,136 @@
1
+ import {canonicalizeMediaType} from './oas-contract.js';
2
+ import {collectInterfaceTypeNames, resolveInterfaceSchema} from './interface-schema.js';
3
+ import {DEFAULT_MODEL_FORMAT, selectModelSchemas} from './model-selection.js';
4
+ import {isObject} from './schema-utils.js';
5
+ import {createTypeResolver} from './type-resolver.js';
6
+ import {quoteKeyIfNeeded} from './utils.js';
7
+
8
+ /**
9
+ * @typedef {{
10
+ * requiredMode?: 'all'|'spec',
11
+ * formats?: string[],
12
+ * includeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
13
+ * excludeSchemaNames?: Array<RegExp|string|((name: string) => boolean)>,
14
+ * }} BuildOptions
15
+ */
16
+
17
+ /** @typedef {{name: string, tsKey: string, type: string, optional: boolean}} ModelProp */
18
+
19
+ /**
20
+ * @typedef {{
21
+ * name: string,
22
+ * props: ModelProp[],
23
+ * imports: string[],
24
+ * extendsTypes: string[],
25
+ * extendsItem?: boolean,
26
+ * isAlias?: boolean,
27
+ * type?: string,
28
+ * }} ModelDefinition
29
+ */
30
+
31
+ function normalizeRequiredMode(value) {
32
+ return String(value ?? '').trim().toLowerCase() === 'spec' ? 'spec' : 'all';
33
+ }
34
+
35
+ function createModelBuilder({spec, schemas, typeNames, interfaceTypeNames, requiredMode}) {
36
+ const knownTypeNames = new Set(typeNames.values());
37
+
38
+ function importsFor(dependencies, currentTypeName) {
39
+ return [...dependencies]
40
+ .filter((dependency) => dependency !== currentTypeName && knownTypeNames.has(dependency))
41
+ .sort((left, right) => left.localeCompare(right));
42
+ }
43
+
44
+ function resolverFor(dependencies) {
45
+ return createTypeResolver({dependencies, typeNames, requiredMode, spec});
46
+ }
47
+
48
+ function buildAlias(raw, typeName) {
49
+ const dependencies = new Set();
50
+ const type = resolverFor(dependencies).typeOf(raw);
51
+ return {
52
+ name: typeName,
53
+ type,
54
+ imports: importsFor(dependencies, typeName),
55
+ isAlias: true,
56
+ props: [],
57
+ extendsTypes: [],
58
+ };
59
+ }
60
+
61
+ function buildInterface(schema, typeName, extendsTypes, preferredFormat) {
62
+ const isJsonLd = canonicalizeMediaType(preferredFormat) === DEFAULT_MODEL_FORMAT;
63
+ const requiredProperties = new Set(schema.required ?? []);
64
+ const dependencies = new Set(extendsTypes);
65
+ const resolver = resolverFor(dependencies);
66
+
67
+ const props = Object.entries(schema.properties ?? {})
68
+ .filter(([propertyName]) => !isJsonLd || !['@id', '@type', '@context'].includes(propertyName))
69
+ .map(([propertyName, propertySchema]) => ({
70
+ name: propertyName,
71
+ tsKey: quoteKeyIfNeeded(propertyName),
72
+ type: requiredMode === 'spec'
73
+ ? resolver.typeOf(propertySchema)
74
+ : resolver.nullableTypeOf(propertySchema),
75
+ optional: requiredMode === 'spec' ? !requiredProperties.has(propertyName) : true,
76
+ }));
77
+
78
+ return {
79
+ name: typeName,
80
+ props,
81
+ imports: importsFor(dependencies, typeName),
82
+ extendsTypes,
83
+ extendsItem: isJsonLd,
84
+ };
85
+ }
86
+
87
+ return function buildModel(schemaName, preferredFormat) {
88
+ const raw = schemas[schemaName];
89
+ const typeName = typeNames.get(schemaName);
90
+ if (!typeName || raw === undefined) return undefined;
91
+
92
+ const {schema, extendsTypes, compatible} = resolveInterfaceSchema(
93
+ raw,
94
+ typeName,
95
+ typeNames,
96
+ interfaceTypeNames
97
+ );
98
+ const isObjectLike = schema?.type === 'object' || isObject(schema?.properties) || extendsTypes.length > 0;
99
+ if (!compatible || !isObjectLike || !interfaceTypeNames.has(typeName)) {
100
+ return buildAlias(raw, typeName);
101
+ }
102
+
103
+ return buildInterface(schema, typeName, extendsTypes, preferredFormat);
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Builds TypeScript model definitions from OpenAPI `components.schemas`.
109
+ * @param {any} spec
110
+ * @param {BuildOptions} [options]
111
+ * @returns {{models: ModelDefinition[]}}
112
+ */
113
+ export function buildModelsFromOpenAPI(spec, options = {}) {
114
+ const requiredMode = normalizeRequiredMode(options.requiredMode);
115
+ const {schemas, formats, schemaNames, preferredFormats, typeNames} = selectModelSchemas(
116
+ spec,
117
+ options
118
+ );
119
+ const interfaceTypeNames = collectInterfaceTypeNames(schemas, schemaNames, typeNames);
120
+ const buildModel = createModelBuilder({
121
+ spec,
122
+ schemas,
123
+ typeNames,
124
+ interfaceTypeNames,
125
+ requiredMode,
126
+ });
127
+
128
+ const models = [];
129
+ for (const schemaName of schemaNames) {
130
+ const preferredFormat = preferredFormats.get(schemaName) ?? formats[0];
131
+ const model = buildModel(schemaName, preferredFormat);
132
+ if (model) models.push(model);
133
+ }
134
+
135
+ return {models};
136
+ }
@@ -10,14 +10,35 @@ export function isObject(v) {
10
10
  return !!v && typeof v === 'object' && !Array.isArray(v);
11
11
  }
12
12
 
13
+ export function hasOwn(object, key) {
14
+ return Object.prototype.hasOwnProperty.call(object, key);
15
+ }
16
+
17
+ function decodeJsonPointerSegment(segment) {
18
+ return segment.replace(/~1/g, '/').replace(/~0/g, '~');
19
+ }
20
+
21
+ export function resolveLocalRef(document, ref) {
22
+ const pointer = String(ref ?? '');
23
+ if (pointer === '#') return document;
24
+ if (!pointer.startsWith('#/')) return undefined;
25
+
26
+ let current = document;
27
+ for (const segment of pointer.slice(2).split('/').map(decodeJsonPointerSegment)) {
28
+ if (!current || typeof current !== 'object') return undefined;
29
+ current = current[segment];
30
+ }
31
+ return current;
32
+ }
33
+
13
34
  /**
14
35
  * Extrait le nom du schéma depuis une référence $ref
15
36
  * @param {string} $ref
16
37
  * @returns {string|undefined}
17
38
  */
18
39
  export function schemaNameFromRef($ref) {
19
- const m = $ref.match(/#\/components\/schemas\/(.+)$/);
20
- return m ? m[1] : undefined;
40
+ const match = String($ref ?? '').match(/^#\/components\/schemas\/([^/]+)$/);
41
+ return match ? decodeJsonPointerSegment(match[1]) : undefined;
21
42
  }
22
43
 
23
44
  /**
@@ -38,7 +59,10 @@ export function mergeAllOf(schemas) {
38
59
  return null;
39
60
  }
40
61
  if (s.type === 'object') {
41
- Object.assign(out.properties, s.properties || {});
62
+ for (const [name, property] of Object.entries(s.properties ?? {})) {
63
+ if (hasOwn(out.properties, name)) return null;
64
+ out.properties[name] = property;
65
+ }
42
66
  if (Array.isArray(s.required)) out.required.push(...s.required);
43
67
  } else {
44
68
  return null;
@@ -0,0 +1,2 @@
1
+ {{#each imports}}import type { {{this}} } from './{{this}}';
2
+ {{/each}}{{{separator}}}export type {{name}} = {{type}};
@@ -0,0 +1,65 @@
1
+ const PRECEDENCE = {
2
+ union: 1,
3
+ intersection: 2,
4
+ primary: 3,
5
+ };
6
+
7
+ function expression(code, precedence, kind, members, acceptsNull) {
8
+ return {code, precedence, kind, members, acceptsNull};
9
+ }
10
+
11
+ export function atomicType(code, acceptsNull = false) {
12
+ return expression(code, PRECEDENCE.primary, 'atomic', [], acceptsNull);
13
+ }
14
+
15
+ export function formatType(type, parentPrecedence = PRECEDENCE.primary) {
16
+ return type.precedence < parentPrecedence ? `(${type.code})` : type.code;
17
+ }
18
+
19
+ function uniqueTypes(types) {
20
+ return [...new Map(types.map((type) => [type.code, type])).values()];
21
+ }
22
+
23
+ function flatten(types, kind) {
24
+ return types.flatMap((type) => type.kind === kind ? type.members : [type]);
25
+ }
26
+
27
+ export function unionType(types) {
28
+ const members = uniqueTypes(flatten(types, 'union')).filter((member) => member.code !== 'never');
29
+ if (members.length === 0) return atomicType('any');
30
+ if (members.some((member) => member.code === 'any')) return atomicType('any');
31
+ if (members.length === 1) return members[0];
32
+
33
+ const ordered = members.filter((member) => member.code !== 'null');
34
+ ordered.push(...members.filter((member) => member.code === 'null'));
35
+ return expression(
36
+ ordered.map((member) => formatType(member, PRECEDENCE.union)).join(' | '),
37
+ PRECEDENCE.union,
38
+ 'union',
39
+ ordered,
40
+ members.some((member) => member.acceptsNull)
41
+ );
42
+ }
43
+
44
+ export function intersectionType(types) {
45
+ const members = uniqueTypes(flatten(types, 'intersection')).filter((member) => member.code !== 'any');
46
+ if (members.length === 0) return atomicType('any');
47
+ if (members.some((member) => member.code === 'never')) return atomicType('never');
48
+ if (members.length === 1) return members[0];
49
+
50
+ return expression(
51
+ members.map((member) => formatType(member, PRECEDENCE.intersection)).join(' & '),
52
+ PRECEDENCE.intersection,
53
+ 'intersection',
54
+ members,
55
+ members.every((member) => member.acceptsNull)
56
+ );
57
+ }
58
+
59
+ export function arrayType(item) {
60
+ return atomicType(`${formatType(item)}[]`);
61
+ }
62
+
63
+ export function nullableType(type) {
64
+ return type.acceptsNull ? type : unionType([type, atomicType('null', true)]);
65
+ }