@obsidiane/meridiane 1.0.1 → 2.0.1

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.
@@ -0,0 +1,159 @@
1
+ import { schemaNameFromRef } from './schema-utils.js';
2
+
3
+ function resolveRefPointer(spec, ref) {
4
+ const s = String(ref || '');
5
+ if (!s.startsWith('#/')) return undefined;
6
+ const parts = s.slice(2).split('/').filter(Boolean);
7
+ let cur = spec;
8
+ for (const p of parts) {
9
+ if (!cur || typeof cur !== 'object') return undefined;
10
+ cur = cur[p];
11
+ }
12
+ return cur;
13
+ }
14
+
15
+ function deref(spec, maybeRef) {
16
+ if (!maybeRef || typeof maybeRef !== 'object') return maybeRef;
17
+ if (typeof maybeRef.$ref === 'string') return resolveRefPointer(spec, maybeRef.$ref) || maybeRef;
18
+ return maybeRef;
19
+ }
20
+
21
+ export function canonicalizeMediaType(mediaType) {
22
+ return String(mediaType || '').split(';')[0].trim().toLowerCase();
23
+ }
24
+
25
+ function getContentSchema(content, wantedLower) {
26
+ if (!content || typeof content !== 'object') return undefined;
27
+ for (const [k, v] of Object.entries(content)) {
28
+ const canon = canonicalizeMediaType(k);
29
+ if (canon === wantedLower) return v?.schema;
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ function is2xxStatus(code) {
35
+ const s = String(code || '');
36
+ if (/^[0-9]{3}$/.test(s)) return s.startsWith('2');
37
+ return /^2xx$/i.test(s);
38
+ }
39
+
40
+ /**
41
+ * Collects only component schema refs (`#/components/schemas/*`) found in a JSON schema.
42
+ * @param {any} schema
43
+ * @param {Set<string>} out
44
+ */
45
+ function collectComponentSchemaRefs(schema, out) {
46
+ const seen = new Set();
47
+
48
+ function walk(node) {
49
+ if (!node || typeof node !== 'object') return;
50
+ if (seen.has(node)) return;
51
+ seen.add(node);
52
+
53
+ if (node.$ref && typeof node.$ref === 'string') {
54
+ const name = schemaNameFromRef(node.$ref);
55
+ if (name) out.add(name);
56
+ return;
57
+ }
58
+
59
+ if (Array.isArray(node)) {
60
+ for (const it of node) walk(it);
61
+ return;
62
+ }
63
+
64
+ if (node.allOf) walk(node.allOf);
65
+ if (node.oneOf) walk(node.oneOf);
66
+ if (node.anyOf) walk(node.anyOf);
67
+ if (node.items) walk(node.items);
68
+ if (node.properties) walk(node.properties);
69
+ if (node.additionalProperties) walk(node.additionalProperties);
70
+
71
+ for (const v of Object.values(node)) walk(v);
72
+ }
73
+
74
+ walk(schema);
75
+ }
76
+
77
+ /**
78
+ * Extracts root schemas from endpoints for one media type.
79
+ * - requestBody is ignored for PATCH (merge-patch typed as Partial<> elsewhere)
80
+ * - responses keep only 2xx and default
81
+ */
82
+ function listRootsFromPaths(spec, { mediaType } = {}) {
83
+ const wantedLower = canonicalizeMediaType(mediaType);
84
+ const roots = [];
85
+
86
+ const paths = spec?.paths;
87
+ if (!paths || typeof paths !== 'object') return roots;
88
+
89
+ for (const pathItem of Object.values(paths)) {
90
+ if (!pathItem || typeof pathItem !== 'object') continue;
91
+ for (const [method, op0] of Object.entries(pathItem)) {
92
+ const op = deref(spec, op0);
93
+ if (!op || typeof op !== 'object') continue;
94
+
95
+ const isPatch = String(method || '').toLowerCase() === 'patch';
96
+
97
+ if (!isPatch) {
98
+ const rb = deref(spec, op.requestBody);
99
+ const rbSchema = getContentSchema(rb?.content, wantedLower);
100
+ if (rbSchema) roots.push(rbSchema);
101
+ }
102
+
103
+ const responses = op.responses;
104
+ if (!responses || typeof responses !== 'object') continue;
105
+ for (const [code, resp] of Object.entries(responses)) {
106
+ if (!is2xxStatus(code) && String(code).toLowerCase() !== 'default') continue;
107
+ const resolved = deref(spec, resp);
108
+ const schema = getContentSchema(resolved?.content, wantedLower);
109
+ if (schema) roots.push(schema);
110
+ }
111
+ }
112
+ }
113
+
114
+ return roots;
115
+ }
116
+
117
+ /**
118
+ * Returns the transitive closure of schema dependencies (following component-schema $ref).
119
+ * @param {Iterable<string>} start
120
+ * @param {Record<string, any>} schemas
121
+ */
122
+ export function expandWithSchemaDependencies(start, schemas) {
123
+ const out = new Set();
124
+ const queue = [];
125
+
126
+ for (const n of start) {
127
+ if (!n || out.has(n)) continue;
128
+ out.add(n);
129
+ queue.push(n);
130
+ }
131
+
132
+ while (queue.length) {
133
+ const name = queue.pop();
134
+ const schema = schemas[name];
135
+ if (!schema) continue;
136
+ const refs = new Set();
137
+ collectComponentSchemaRefs(schema, refs);
138
+ for (const ref of refs) {
139
+ if (!ref || out.has(ref)) continue;
140
+ out.add(ref);
141
+ queue.push(ref);
142
+ }
143
+ }
144
+
145
+ return out;
146
+ }
147
+
148
+ /**
149
+ * Collects all component schemas used by endpoints for one media type.
150
+ * @param {any} spec
151
+ * @param {{ mediaType: string }} cfg
152
+ */
153
+ export function collectUsedSchemasForMediaType(spec, cfg) {
154
+ const schemas = spec?.components?.schemas || {};
155
+ const roots = listRootsFromPaths(spec, cfg);
156
+ const initialRefs = new Set();
157
+ for (const r of roots) collectComponentSchemaRefs(r, initialRefs);
158
+ return expandWithSchemaDependencies(initialRefs, schemas);
159
+ }
@@ -0,0 +1,71 @@
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
+ }
11
+
12
+ export function parseOpenApiSchemaName(originalName) {
13
+ const raw = String(originalName || '');
14
+ const i = raw.indexOf('-');
15
+ let baseRaw = i >= 0 ? raw.slice(0, i) : raw;
16
+ let groupRaw = i >= 0 ? raw.slice(i + 1) : '';
17
+
18
+ let baseVariant = 'none';
19
+ if (/\.jsonld$/i.test(baseRaw)) {
20
+ baseVariant = 'jsonld';
21
+ baseRaw = baseRaw.replace(/\.jsonld$/i, '');
22
+ } else if (/\.multipart$/i.test(baseRaw)) {
23
+ baseVariant = 'multipart';
24
+ baseRaw = baseRaw.replace(/\.multipart$/i, '');
25
+ }
26
+
27
+ let isMergePatch = false;
28
+ if (/\.jsonmergepatch$/i.test(baseRaw)) {
29
+ isMergePatch = true;
30
+ baseRaw = baseRaw.replace(/\.jsonmergepatch$/i, '');
31
+ }
32
+ if (/\.jsonmergepatch$/i.test(groupRaw)) {
33
+ isMergePatch = true;
34
+ groupRaw = groupRaw.replace(/\.jsonmergepatch$/i, '');
35
+ }
36
+
37
+ const basePath = baseRaw;
38
+ const groupPath = groupRaw;
39
+
40
+ return {
41
+ originalName: raw,
42
+ basePath,
43
+ groupPath,
44
+ baseVariant,
45
+ isMergePatch,
46
+ key: `${basePath.toLowerCase()}|${groupPath.toLowerCase()}`,
47
+ };
48
+ }
49
+
50
+ export function baseTsNameFromBasePath(basePath) {
51
+ const parts = String(basePath || '').split('.').filter(Boolean);
52
+ if (parts.length === 0) return 'Model';
53
+ const a = pascalizeTokens([parts[0]]);
54
+ if (parts.length === 1) return a;
55
+ const b = pascalizeTokens([parts[1]]);
56
+ if (parts.length === 2 && b.toLowerCase().startsWith(a.toLowerCase())) return b;
57
+ return parts.map((p) => pascalizeTokens([p])).join('');
58
+ }
59
+
60
+ export function groupTsNameFromGroupPath(groupPath) {
61
+ const tokens = String(groupPath || '').split(/[._:]+/g).filter(Boolean);
62
+ return pascalizeTokens(tokens);
63
+ }
64
+
65
+ export function formatSuffixToken(mediaType) {
66
+ const canon = canonicalizeMediaType(mediaType);
67
+ const tokens = canon.split(/[\/+.-]+/g).filter(Boolean);
68
+ const trimmed = tokens[0] === 'application' ? tokens.slice(1) : tokens;
69
+ return pascalizeTokens(trimmed.length ? trimmed : tokens) || 'Fmt';
70
+ }
71
+