@llm4ts/shell 2.12.0 → 2.13.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.
- package/flows/epic-stories.js +9 -3
- package/flows/lib/epic-stories.js +24 -4
- package/kits/soap-ace/README.md +313 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/RUNBOOK.md +102 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/design/api-design.md +620 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaConti.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/cercaMovimenti.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/confermaBonifico.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/dettaglioConto.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/inserisciBonifico.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/responses/revocaBonifico.xml +3 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/soapui/DemoBank-soapui-project.xml +39 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/DemoBank.wsdl +186 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/LegacyRpc.wsdl +29 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/banking.xsd +10 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/bonifici.xsd +107 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/common.xsd +93 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/conti.xsd +71 -0
- package/kits/soap-ace/fixtures/demo-bank-soap/wsdl/xsd/movimenti.xsd +55 -0
- package/kits/soap-ace/flows/lib/soap/Analysis.js +508 -0
- package/kits/soap-ace/flows/lib/soap/Auth.js +179 -0
- package/kits/soap-ace/flows/lib/soap/Call.js +186 -0
- package/kits/soap-ace/flows/lib/soap/CallPolicy.js +50 -0
- package/kits/soap-ace/flows/lib/soap/Catalog.js +283 -0
- package/kits/soap-ace/flows/lib/soap/Classification.js +254 -0
- package/kits/soap-ace/flows/lib/soap/Design.js +497 -0
- package/kits/soap-ace/flows/lib/soap/DesignProposal.js +204 -0
- package/kits/soap-ace/flows/lib/soap/Discover.js +139 -0
- package/kits/soap-ace/flows/lib/soap/Envelope.js +132 -0
- package/kits/soap-ace/flows/lib/soap/Epic.js +239 -0
- package/kits/soap-ace/flows/lib/soap/Instance.js +1339 -0
- package/kits/soap-ace/flows/lib/soap/Mapping.js +130 -0
- package/kits/soap-ace/flows/lib/soap/Masking.js +508 -0
- package/kits/soap-ace/flows/lib/soap/OpenApi.js +288 -0
- package/kits/soap-ace/flows/lib/soap/Samples.js +287 -0
- package/kits/soap-ace/flows/lib/soap/Scenarios.js +132 -0
- package/kits/soap-ace/flows/lib/soap/SoapUiImport.js +200 -0
- package/kits/soap-ace/flows/lib/soap/Stub.js +34 -0
- package/kits/soap-ace/flows/lib/soap/Transport.js +167 -0
- package/kits/soap-ace/flows/lib/soap/Wsdl.js +533 -0
- package/kits/soap-ace/flows/lib/soap/Xml.js +455 -0
- package/kits/soap-ace/flows/lib/soap/Xsd.js +720 -0
- package/kits/soap-ace/flows/lib/soap/Yaml.js +309 -0
- package/kits/soap-ace/flows/soap-design.js +167 -0
- package/kits/soap-ace/flows/soap-discover.js +85 -0
- package/kits/soap-ace/flows/soap-epic.js +204 -0
- package/kits/soap-ace/flows/soap-sample.js +265 -0
- package/kits/soap-ace/packs/ace12-rest/api-style.md +63 -0
- package/kits/soap-ace/packs/ace12-rest/pack.md +18 -0
- package/kits/soap-ace/packs/ace12-rest/prompts/implement.md +8 -0
- package/kits/soap-ace/packs/ace12-rest/reviewers/contract.md +5 -0
- package/kits/soap-ace/patterns/PAT-ACE-001-esito-to-problem.md +8 -0
- package/kits/soap-ace/patterns/PAT-ACE-002-fault-to-problem.md +8 -0
- package/kits/soap-ace/patterns/PAT-ACE-003-amounts-and-enums.md +8 -0
- package/kits/soap-ace/patterns/PAT-ACE-004-pagination.md +7 -0
- package/kits/soap-ace/patterns/PAT-ACE-005-nil-and-absence.md +7 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/CONTRIBUTING.md +56 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/README.md +16 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates-container.sh +8 -0
- package/kits/soap-ace/scaffolds/ace12-rest-api/scripts/ace-gates.sh +41 -0
- package/package.json +4 -4
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import { AttributeField, ComplexTypeDef, ElementDecl, ElementField, Facets, isBuiltin, ModelGroup, OpenQuestion, qname, SimpleTypeDef, splitClark, XsdNamespace } from "./Catalog.js";
|
|
2
|
+
import { attribute, elements, firstChild, is, resolveQName, textOf } from "./Xml.js";
|
|
3
|
+
// XSD reading in two passes. `collectSchema` reads one `xs:schema` element
|
|
4
|
+
// into a collector without resolving anything across documents (imports and
|
|
5
|
+
// includes are followed by the WSDL loader, which feeds every schema it finds
|
|
6
|
+
// into the same collector). `finishSchemas` then resolves element refs,
|
|
7
|
+
// flattens `xs:extension` bases, and reports every dangling reference as an
|
|
8
|
+
// open question. Nothing here is guessed: an unmodelled construct is kept
|
|
9
|
+
// out of the typed model and named in an open question.
|
|
10
|
+
const anyType = qname(XsdNamespace, "anyType");
|
|
11
|
+
const stringType = qname(XsdNamespace, "string");
|
|
12
|
+
export const makeSchemaCollector = () => ({
|
|
13
|
+
complex: new Map(),
|
|
14
|
+
simple: new Map(),
|
|
15
|
+
elements: new Map(),
|
|
16
|
+
attributes: new Map(),
|
|
17
|
+
questions: [],
|
|
18
|
+
namespaces: new Set(),
|
|
19
|
+
anonymous: new Set(),
|
|
20
|
+
locations: new Map()
|
|
21
|
+
});
|
|
22
|
+
const xs = (element, local) => is(element, XsdNamespace, local);
|
|
23
|
+
const xsChildren = (element) => elements(element).filter((child) => child.name.namespace === XsdNamespace);
|
|
24
|
+
export const documentationOf = (element) => {
|
|
25
|
+
const annotation = firstChild(element, XsdNamespace, "annotation");
|
|
26
|
+
if (annotation === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const text = elements(annotation)
|
|
29
|
+
.filter((child) => xs(child, "documentation"))
|
|
30
|
+
.map((child) => textOf(child).trim().replace(/\s+/g, " "))
|
|
31
|
+
.filter((value) => value !== "")
|
|
32
|
+
.join(" ");
|
|
33
|
+
return text === "" ? undefined : text;
|
|
34
|
+
};
|
|
35
|
+
const optionalDoc = (documentation) => documentation === undefined ? {} : { documentation };
|
|
36
|
+
const parseOccurs = (value, fallback) => {
|
|
37
|
+
if (value === undefined)
|
|
38
|
+
return fallback;
|
|
39
|
+
if (value === "unbounded")
|
|
40
|
+
return "unbounded";
|
|
41
|
+
const parsed = Number.parseInt(value, 10);
|
|
42
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
43
|
+
};
|
|
44
|
+
const parseMinOccurs = (value) => {
|
|
45
|
+
const parsed = parseOccurs(value, 1);
|
|
46
|
+
return parsed === "unbounded" ? 1 : parsed;
|
|
47
|
+
};
|
|
48
|
+
/** An XSD boolean attribute: `true` or `1`. */
|
|
49
|
+
const flag = (element, name) => {
|
|
50
|
+
const value = attribute(element, name)?.trim();
|
|
51
|
+
return value === "true" || value === "1";
|
|
52
|
+
};
|
|
53
|
+
const facetNames = new Set([
|
|
54
|
+
"enumeration",
|
|
55
|
+
"pattern",
|
|
56
|
+
"length",
|
|
57
|
+
"minLength",
|
|
58
|
+
"maxLength",
|
|
59
|
+
"totalDigits",
|
|
60
|
+
"fractionDigits",
|
|
61
|
+
"minInclusive",
|
|
62
|
+
"maxInclusive",
|
|
63
|
+
"minExclusive",
|
|
64
|
+
"maxExclusive",
|
|
65
|
+
"whiteSpace"
|
|
66
|
+
]);
|
|
67
|
+
const multiply = (left, right) => left === "unbounded" || right === "unbounded" ? "unbounded" : left * right;
|
|
68
|
+
class SchemaReader {
|
|
69
|
+
targetNamespace;
|
|
70
|
+
qualifiedElements;
|
|
71
|
+
qualifiedAttributes;
|
|
72
|
+
/** A schema without targetNamespace included into one: its no-namespace refs mean the includer's. */
|
|
73
|
+
chameleon;
|
|
74
|
+
schema;
|
|
75
|
+
document;
|
|
76
|
+
into;
|
|
77
|
+
constructor(schema, document, into,
|
|
78
|
+
/** For chameleon includes: the including schema's namespace. */
|
|
79
|
+
namespaceOverride) {
|
|
80
|
+
this.schema = schema;
|
|
81
|
+
this.document = document;
|
|
82
|
+
this.into = into;
|
|
83
|
+
this.targetNamespace = attribute(schema, "targetNamespace") ?? namespaceOverride ?? "";
|
|
84
|
+
this.chameleon =
|
|
85
|
+
attribute(schema, "targetNamespace") === undefined && namespaceOverride !== undefined;
|
|
86
|
+
this.qualifiedElements = attribute(schema, "elementFormDefault") === "qualified";
|
|
87
|
+
this.qualifiedAttributes = attribute(schema, "attributeFormDefault") === "qualified";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A name for an anonymous type at `path` (`~op/x`). A global element and
|
|
91
|
+
* a named type may share a local name, so a taken path gets a `#n` suffix.
|
|
92
|
+
*/
|
|
93
|
+
anonymousName(path) {
|
|
94
|
+
const base = `~${path.replace(/^~/, "")}`;
|
|
95
|
+
let name = qname(this.targetNamespace, base);
|
|
96
|
+
for (let n = 2; this.into.anonymous.has(name); n++) {
|
|
97
|
+
name = qname(this.targetNamespace, `${base}#${n}`);
|
|
98
|
+
}
|
|
99
|
+
this.into.anonymous.add(name);
|
|
100
|
+
return name;
|
|
101
|
+
}
|
|
102
|
+
where(element) {
|
|
103
|
+
return `${this.document}:${element.line}`;
|
|
104
|
+
}
|
|
105
|
+
question(code, element, subject, detail) {
|
|
106
|
+
// Anonymous types carry a `~` marker in their synthesized names; the
|
|
107
|
+
// question speaks of the element path the reader sees in the schema.
|
|
108
|
+
this.into.questions.push(new OpenQuestion({
|
|
109
|
+
code,
|
|
110
|
+
location: this.where(element),
|
|
111
|
+
subject: subject.replace(/^~/, ""),
|
|
112
|
+
detail: detail.replace(/(^|\s)~/g, "$1")
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
typeRef(element, value) {
|
|
116
|
+
if (value === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
const resolved = resolveQName(element, value);
|
|
119
|
+
if (resolved === undefined)
|
|
120
|
+
return `{?}${value}`;
|
|
121
|
+
const namespace = this.chameleon && resolved.namespace === "" ? this.targetNamespace : resolved.namespace;
|
|
122
|
+
return qname(namespace, resolved.local);
|
|
123
|
+
}
|
|
124
|
+
read() {
|
|
125
|
+
this.into.namespaces.add(this.targetNamespace);
|
|
126
|
+
const references = [];
|
|
127
|
+
for (const child of xsChildren(this.schema)) {
|
|
128
|
+
const name = attribute(child, "name");
|
|
129
|
+
if (xs(child, "import") || xs(child, "include") || xs(child, "redefine")) {
|
|
130
|
+
const location = attribute(child, "schemaLocation");
|
|
131
|
+
if (location !== undefined) {
|
|
132
|
+
references.push({
|
|
133
|
+
kind: xs(child, "import") ? "import" : "include",
|
|
134
|
+
namespace: xs(child, "import") ? attribute(child, "namespace") : this.targetNamespace,
|
|
135
|
+
location
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const redefined = xsChildren(child)
|
|
139
|
+
.filter((definition) => !xs(definition, "annotation"))
|
|
140
|
+
.map((definition) => attribute(definition, "name") ?? "?");
|
|
141
|
+
if (xs(child, "redefine") && redefined.length > 0) {
|
|
142
|
+
this.question("redefine", child, location ?? "?", `schema ${location ?? "?"} is included, but its redefinitions of ${redefined.join(", ")} are not applied`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else if (xs(child, "attribute") && name !== undefined) {
|
|
146
|
+
this.globalAttribute(child, name);
|
|
147
|
+
}
|
|
148
|
+
else if (xs(child, "element") && name !== undefined) {
|
|
149
|
+
this.globalElement(child, name);
|
|
150
|
+
}
|
|
151
|
+
else if (xs(child, "complexType") && name !== undefined) {
|
|
152
|
+
this.complexType(child, qname(this.targetNamespace, name), false);
|
|
153
|
+
}
|
|
154
|
+
else if (xs(child, "simpleType") && name !== undefined) {
|
|
155
|
+
this.simpleType(child, qname(this.targetNamespace, name), false);
|
|
156
|
+
}
|
|
157
|
+
else if (xs(child, "group") || xs(child, "attributeGroup")) {
|
|
158
|
+
// Definitions are harmless; their use is reported where referenced.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return references;
|
|
162
|
+
}
|
|
163
|
+
globalElement(element, name) {
|
|
164
|
+
const elementName = qname(this.targetNamespace, name);
|
|
165
|
+
if (attribute(element, "substitutionGroup") !== undefined) {
|
|
166
|
+
this.question("substitution-group", element, name, `element ${name} joins substitution group ${attribute(element, "substitutionGroup") ?? ""}; substitutes are not modelled`);
|
|
167
|
+
}
|
|
168
|
+
if (flag(element, "abstract")) {
|
|
169
|
+
this.question("abstract", element, name, `element ${name} is abstract`);
|
|
170
|
+
}
|
|
171
|
+
const type = this.elementType(element, name);
|
|
172
|
+
const fixed = attribute(element, "fixed");
|
|
173
|
+
this.into.locations.set(elementName, this.where(element));
|
|
174
|
+
this.into.elements.set(elementName, new ElementDecl({
|
|
175
|
+
name: elementName,
|
|
176
|
+
type,
|
|
177
|
+
nillable: flag(element, "nillable"),
|
|
178
|
+
...(fixed === undefined ? {} : { fixed }),
|
|
179
|
+
...optionalDoc(documentationOf(element))
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
globalAttribute(element, name) {
|
|
183
|
+
const inline = firstChild(element, XsdNamespace, "simpleType");
|
|
184
|
+
let type = this.typeRef(element, attribute(element, "type"));
|
|
185
|
+
if (type === undefined && inline !== undefined) {
|
|
186
|
+
type = this.anonymousName(`@${name}`);
|
|
187
|
+
this.simpleType(inline, type, true);
|
|
188
|
+
}
|
|
189
|
+
this.into.attributes.set(qname(this.targetNamespace, name), {
|
|
190
|
+
type: type ?? stringType,
|
|
191
|
+
fixed: attribute(element, "fixed"),
|
|
192
|
+
documentation: documentationOf(element)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/** The type of an element: its `type=` or its inline (anonymous) type. */
|
|
196
|
+
elementType(element, anonymousPath) {
|
|
197
|
+
const declared = this.typeRef(element, attribute(element, "type"));
|
|
198
|
+
if (declared !== undefined)
|
|
199
|
+
return declared;
|
|
200
|
+
const inlineComplex = firstChild(element, XsdNamespace, "complexType");
|
|
201
|
+
if (inlineComplex !== undefined) {
|
|
202
|
+
const name = this.anonymousName(anonymousPath);
|
|
203
|
+
this.complexType(inlineComplex, name, true);
|
|
204
|
+
return name;
|
|
205
|
+
}
|
|
206
|
+
const inlineSimple = firstChild(element, XsdNamespace, "simpleType");
|
|
207
|
+
if (inlineSimple !== undefined) {
|
|
208
|
+
const name = this.anonymousName(anonymousPath);
|
|
209
|
+
this.simpleType(inlineSimple, name, true);
|
|
210
|
+
return name;
|
|
211
|
+
}
|
|
212
|
+
return anyType;
|
|
213
|
+
}
|
|
214
|
+
complexType(element, name, anonymous) {
|
|
215
|
+
const local = splitClark(name).local;
|
|
216
|
+
const complexContent = firstChild(element, XsdNamespace, "complexContent");
|
|
217
|
+
const simpleContent = firstChild(element, XsdNamespace, "simpleContent");
|
|
218
|
+
if (flag(element, "mixed") || (complexContent !== undefined && flag(complexContent, "mixed"))) {
|
|
219
|
+
this.question("mixed-content", element, local, `type ${local} allows mixed text content`);
|
|
220
|
+
}
|
|
221
|
+
if (flag(element, "abstract")) {
|
|
222
|
+
this.question("abstract", element, local, `type ${local} is abstract`);
|
|
223
|
+
}
|
|
224
|
+
let base;
|
|
225
|
+
let derivation;
|
|
226
|
+
let textType;
|
|
227
|
+
let content = element;
|
|
228
|
+
if (complexContent !== undefined) {
|
|
229
|
+
const extension = firstChild(complexContent, XsdNamespace, "extension");
|
|
230
|
+
const restriction = firstChild(complexContent, XsdNamespace, "restriction");
|
|
231
|
+
if (extension !== undefined) {
|
|
232
|
+
base = this.typeRef(extension, attribute(extension, "base"));
|
|
233
|
+
derivation = "extension";
|
|
234
|
+
content = extension;
|
|
235
|
+
}
|
|
236
|
+
else if (restriction !== undefined) {
|
|
237
|
+
this.question("complex-restriction", restriction, local, `type ${local} restricts ${attribute(restriction, "base") ?? "?"}; only the restated content is modelled`);
|
|
238
|
+
base = this.typeRef(restriction, attribute(restriction, "base"));
|
|
239
|
+
derivation = "restriction";
|
|
240
|
+
content = restriction;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (simpleContent !== undefined) {
|
|
244
|
+
const extension = firstChild(simpleContent, XsdNamespace, "extension");
|
|
245
|
+
const restriction = firstChild(simpleContent, XsdNamespace, "restriction");
|
|
246
|
+
const step = extension ?? restriction;
|
|
247
|
+
if (step !== undefined) {
|
|
248
|
+
// The base may be a simple type (the text's type) or a complex type
|
|
249
|
+
// with simple content (inherit its text type and attributes); which
|
|
250
|
+
// one is only known once every schema is read.
|
|
251
|
+
base = this.typeRef(step, attribute(step, "base")) ?? stringType;
|
|
252
|
+
derivation = extension === undefined ? "restriction" : "extension";
|
|
253
|
+
textType = base;
|
|
254
|
+
content = step;
|
|
255
|
+
const inline = firstChild(step, XsdNamespace, "simpleType");
|
|
256
|
+
const facets = xsChildren(step).some((child) => facetNames.has(child.name.local));
|
|
257
|
+
if (extension === undefined && (facets || inline !== undefined)) {
|
|
258
|
+
// A restriction narrows the text: an anonymous simple type over
|
|
259
|
+
// the base's text type (resolved in the finish pass) keeps its facets.
|
|
260
|
+
textType = this.anonymousName(`${local}/#text`);
|
|
261
|
+
this.simpleType(step, textType, true, base);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const fields = [];
|
|
266
|
+
const groups = [];
|
|
267
|
+
const attributes = [];
|
|
268
|
+
let counter = 0;
|
|
269
|
+
const next = () => counter++;
|
|
270
|
+
for (const child of xsChildren(content)) {
|
|
271
|
+
if (xs(child, "sequence") || xs(child, "all") || xs(child, "choice")) {
|
|
272
|
+
this.particle(child, local, fields, groups, {
|
|
273
|
+
choice: undefined,
|
|
274
|
+
branch: undefined,
|
|
275
|
+
group: undefined,
|
|
276
|
+
optional: false,
|
|
277
|
+
repeats: 1
|
|
278
|
+
}, next);
|
|
279
|
+
}
|
|
280
|
+
else if (xs(child, "group")) {
|
|
281
|
+
this.question("group-ref", child, local, `type ${local} uses model group ${attribute(child, "ref") ?? "?"}; its fields are not modelled`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
this.attributes(content, local, attributes);
|
|
285
|
+
this.into.complex.set(name, {
|
|
286
|
+
name,
|
|
287
|
+
anonymous,
|
|
288
|
+
base,
|
|
289
|
+
derivation,
|
|
290
|
+
fields,
|
|
291
|
+
groups,
|
|
292
|
+
attributes,
|
|
293
|
+
textType,
|
|
294
|
+
simpleContent: simpleContent !== undefined,
|
|
295
|
+
documentation: documentationOf(element),
|
|
296
|
+
location: this.where(element)
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
particle(group, owner, into, groups, context, next) {
|
|
300
|
+
const minOccurs = parseMinOccurs(attribute(group, "minOccurs"));
|
|
301
|
+
const maxOccurs = parseOccurs(attribute(group, "maxOccurs"), 1);
|
|
302
|
+
const optional = context.optional || minOccurs === 0;
|
|
303
|
+
const repeats = multiply(context.repeats, maxOccurs);
|
|
304
|
+
const isChoice = xs(group, "choice");
|
|
305
|
+
const choice = isChoice ? next() : context.choice;
|
|
306
|
+
// The exact content model: every group, with its own bounds and parent.
|
|
307
|
+
const id = groups.length;
|
|
308
|
+
groups.push(new ModelGroup({
|
|
309
|
+
id,
|
|
310
|
+
kind: isChoice ? "choice" : xs(group, "all") ? "all" : "sequence",
|
|
311
|
+
minOccurs,
|
|
312
|
+
maxOccurs,
|
|
313
|
+
...(context.group === undefined ? {} : { parent: context.group }),
|
|
314
|
+
start: into.length
|
|
315
|
+
}));
|
|
316
|
+
for (const child of xsChildren(group)) {
|
|
317
|
+
// Every alternative of a choice is its own branch; inside a branch,
|
|
318
|
+
// a nested sequence keeps the branch it belongs to.
|
|
319
|
+
const branch = isChoice ? next() : context.branch;
|
|
320
|
+
const inner = { choice, branch, group: id, optional, repeats };
|
|
321
|
+
if (xs(child, "element")) {
|
|
322
|
+
into.push(this.localElement(child, owner, inner));
|
|
323
|
+
}
|
|
324
|
+
else if (xs(child, "sequence") || xs(child, "choice") || xs(child, "all")) {
|
|
325
|
+
this.particle(child, owner, into, groups, inner, next);
|
|
326
|
+
}
|
|
327
|
+
else if (xs(child, "any")) {
|
|
328
|
+
this.question("xsd-any", child, owner, `type ${owner} accepts any element (namespace ${attribute(child, "namespace") ?? "##any"}); its content is open`);
|
|
329
|
+
}
|
|
330
|
+
else if (xs(child, "group")) {
|
|
331
|
+
this.question("group-ref", child, owner, `type ${owner} uses model group ${attribute(child, "ref") ?? "?"}; its fields are not modelled`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
localElement(element, owner, context) {
|
|
336
|
+
const ref = this.typeRef(element, attribute(element, "ref"));
|
|
337
|
+
const own = {
|
|
338
|
+
min: parseMinOccurs(attribute(element, "minOccurs")),
|
|
339
|
+
max: parseOccurs(attribute(element, "maxOccurs"), 1)
|
|
340
|
+
};
|
|
341
|
+
const base = {
|
|
342
|
+
minOccurs: context.optional ? 0 : own.min,
|
|
343
|
+
maxOccurs: multiply(own.max, context.repeats),
|
|
344
|
+
own,
|
|
345
|
+
nillable: flag(element, "nillable"),
|
|
346
|
+
fixed: attribute(element, "fixed"),
|
|
347
|
+
choice: context.choice,
|
|
348
|
+
branch: context.branch,
|
|
349
|
+
group: context.group ?? 0,
|
|
350
|
+
documentation: documentationOf(element),
|
|
351
|
+
location: this.where(element)
|
|
352
|
+
};
|
|
353
|
+
if (ref !== undefined) {
|
|
354
|
+
const target = splitClark(ref);
|
|
355
|
+
return { ...base, name: target.local, namespace: target.namespace, type: undefined, ref };
|
|
356
|
+
}
|
|
357
|
+
const name = attribute(element, "name") ?? "?";
|
|
358
|
+
const form = attribute(element, "form");
|
|
359
|
+
const qualified = form === undefined ? this.qualifiedElements : form === "qualified";
|
|
360
|
+
return {
|
|
361
|
+
...base,
|
|
362
|
+
name,
|
|
363
|
+
namespace: qualified ? this.targetNamespace : "",
|
|
364
|
+
type: this.elementType(element, `${owner}/${name}`),
|
|
365
|
+
ref: undefined
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
attributes(content, owner, into) {
|
|
369
|
+
for (const child of xsChildren(content)) {
|
|
370
|
+
if (xs(child, "attribute")) {
|
|
371
|
+
const common = {
|
|
372
|
+
use: attribute(child, "use"),
|
|
373
|
+
fixed: attribute(child, "fixed"),
|
|
374
|
+
documentation: documentationOf(child),
|
|
375
|
+
location: this.where(child)
|
|
376
|
+
};
|
|
377
|
+
const ref = this.typeRef(child, attribute(child, "ref"));
|
|
378
|
+
if (ref !== undefined) {
|
|
379
|
+
// A global attribute: its name, type and namespace are the
|
|
380
|
+
// declaration's, looked up once every schema is read.
|
|
381
|
+
const target = splitClark(ref);
|
|
382
|
+
into.push({
|
|
383
|
+
...common,
|
|
384
|
+
name: target.local,
|
|
385
|
+
namespace: target.namespace === "" ? undefined : target.namespace,
|
|
386
|
+
type: stringType,
|
|
387
|
+
ref
|
|
388
|
+
});
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
const name = attribute(child, "name") ?? "?";
|
|
392
|
+
const inline = firstChild(child, XsdNamespace, "simpleType");
|
|
393
|
+
let type = this.typeRef(child, attribute(child, "type"));
|
|
394
|
+
if (type === undefined && inline !== undefined) {
|
|
395
|
+
type = this.anonymousName(`${owner}/@${name}`);
|
|
396
|
+
this.simpleType(inline, type, true);
|
|
397
|
+
}
|
|
398
|
+
const form = attribute(child, "form");
|
|
399
|
+
const qualified = form === undefined ? this.qualifiedAttributes : form === "qualified";
|
|
400
|
+
into.push({
|
|
401
|
+
...common,
|
|
402
|
+
name,
|
|
403
|
+
namespace: qualified && this.targetNamespace !== "" ? this.targetNamespace : undefined,
|
|
404
|
+
type: type ?? stringType,
|
|
405
|
+
ref: undefined
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
else if (xs(child, "attributeGroup")) {
|
|
409
|
+
this.question("attribute-group", child, owner, `type ${owner} uses attribute group ${attribute(child, "ref") ?? "?"}; its attributes are not modelled`);
|
|
410
|
+
}
|
|
411
|
+
else if (xs(child, "anyAttribute")) {
|
|
412
|
+
this.question("xsd-any-attribute", child, owner, `type ${owner} accepts any attribute`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* A simple type from its `xs:simpleType` element, or (with `restrictionBase`)
|
|
418
|
+
* from a simpleContent restriction whose base is only known later.
|
|
419
|
+
*/
|
|
420
|
+
simpleType(element, name, anonymous, restrictionBase) {
|
|
421
|
+
const local = splitClark(name).local;
|
|
422
|
+
this.into.locations.set(name, this.where(element));
|
|
423
|
+
const restriction = restrictionBase === undefined ? firstChild(element, XsdNamespace, "restriction") : element;
|
|
424
|
+
const list = firstChild(element, XsdNamespace, "list");
|
|
425
|
+
if (restriction === undefined && list !== undefined) {
|
|
426
|
+
// A list: whitespace-separated items of the item type; length facets
|
|
427
|
+
// of a restriction over it count items.
|
|
428
|
+
const inline = firstChild(list, XsdNamespace, "simpleType");
|
|
429
|
+
let itemType = this.typeRef(list, attribute(list, "itemType"));
|
|
430
|
+
if (itemType === undefined && inline !== undefined) {
|
|
431
|
+
itemType = this.anonymousName(`${local}/#item`);
|
|
432
|
+
this.simpleType(inline, itemType, true);
|
|
433
|
+
}
|
|
434
|
+
this.into.simple.set(name, new SimpleTypeDef({
|
|
435
|
+
name,
|
|
436
|
+
anonymous,
|
|
437
|
+
base: stringType,
|
|
438
|
+
facets: new Facets({}),
|
|
439
|
+
variety: "list",
|
|
440
|
+
itemType: itemType ?? stringType,
|
|
441
|
+
...optionalDoc(documentationOf(element))
|
|
442
|
+
}));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (restriction === undefined) {
|
|
446
|
+
this.question("list-or-union", element, local, `simple type ${local} is a union; values are treated as strings`);
|
|
447
|
+
this.into.simple.set(name, new SimpleTypeDef({
|
|
448
|
+
name,
|
|
449
|
+
anonymous,
|
|
450
|
+
base: stringType,
|
|
451
|
+
facets: new Facets({}),
|
|
452
|
+
variety: "union",
|
|
453
|
+
...optionalDoc(documentationOf(element))
|
|
454
|
+
}));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
// The base is `base=`, or an anonymous simple type written inside.
|
|
458
|
+
let base = this.typeRef(restriction, attribute(restriction, "base")) ?? restrictionBase;
|
|
459
|
+
const inline = firstChild(restriction, XsdNamespace, "simpleType");
|
|
460
|
+
if (inline !== undefined && (base === undefined || restrictionBase !== undefined)) {
|
|
461
|
+
const inlineName = this.anonymousName(`${local}/#base`);
|
|
462
|
+
this.simpleType(inline, inlineName, true);
|
|
463
|
+
base = inlineName;
|
|
464
|
+
}
|
|
465
|
+
this.into.simple.set(name, new SimpleTypeDef({
|
|
466
|
+
name,
|
|
467
|
+
anonymous,
|
|
468
|
+
base: base ?? stringType,
|
|
469
|
+
facets: readFacets(restriction),
|
|
470
|
+
...optionalDoc(documentationOf(element))
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
const intFacet = (restriction, local) => {
|
|
475
|
+
const facet = firstChild(restriction, XsdNamespace, local);
|
|
476
|
+
const value = facet === undefined ? undefined : attribute(facet, "value");
|
|
477
|
+
const parsed = value === undefined ? Number.NaN : Number.parseInt(value, 10);
|
|
478
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
479
|
+
};
|
|
480
|
+
const stringFacet = (restriction, local) => {
|
|
481
|
+
const facet = firstChild(restriction, XsdNamespace, local);
|
|
482
|
+
return facet === undefined ? undefined : attribute(facet, "value");
|
|
483
|
+
};
|
|
484
|
+
const readFacets = (restriction) => {
|
|
485
|
+
const all = (local) => elements(restriction)
|
|
486
|
+
.filter((child) => xs(child, local))
|
|
487
|
+
.map((child) => attribute(child, "value") ?? "");
|
|
488
|
+
const enumeration = all("enumeration");
|
|
489
|
+
const pattern = all("pattern");
|
|
490
|
+
const bound = (key) => stringFacet(restriction, key);
|
|
491
|
+
const minInclusive = bound("minInclusive");
|
|
492
|
+
const maxInclusive = bound("maxInclusive");
|
|
493
|
+
const minExclusive = bound("minExclusive");
|
|
494
|
+
const maxExclusive = bound("maxExclusive");
|
|
495
|
+
const length = intFacet(restriction, "length");
|
|
496
|
+
const minLength = intFacet(restriction, "minLength");
|
|
497
|
+
const maxLength = intFacet(restriction, "maxLength");
|
|
498
|
+
const totalDigits = intFacet(restriction, "totalDigits");
|
|
499
|
+
const fractionDigits = intFacet(restriction, "fractionDigits");
|
|
500
|
+
const whiteSpace = stringFacet(restriction, "whiteSpace")?.trim();
|
|
501
|
+
return new Facets({
|
|
502
|
+
...(enumeration.length > 0 ? { enumeration } : {}),
|
|
503
|
+
...(pattern.length > 0 ? { pattern } : {}),
|
|
504
|
+
...(length === undefined ? {} : { length }),
|
|
505
|
+
...(minLength === undefined ? {} : { minLength }),
|
|
506
|
+
...(maxLength === undefined ? {} : { maxLength }),
|
|
507
|
+
...(totalDigits === undefined ? {} : { totalDigits }),
|
|
508
|
+
...(fractionDigits === undefined ? {} : { fractionDigits }),
|
|
509
|
+
...(minInclusive === undefined ? {} : { minInclusive }),
|
|
510
|
+
...(maxInclusive === undefined ? {} : { maxInclusive }),
|
|
511
|
+
...(minExclusive === undefined ? {} : { minExclusive }),
|
|
512
|
+
...(maxExclusive === undefined ? {} : { maxExclusive }),
|
|
513
|
+
...(whiteSpace === "preserve" || whiteSpace === "replace" || whiteSpace === "collapse"
|
|
514
|
+
? { whiteSpace }
|
|
515
|
+
: {})
|
|
516
|
+
});
|
|
517
|
+
};
|
|
518
|
+
/** Read one `xs:schema` into the collector; returns the imports/includes to follow. */
|
|
519
|
+
export const collectSchema = (schema, document, into, namespaceOverride) => {
|
|
520
|
+
const reader = new SchemaReader(schema, document, into, namespaceOverride);
|
|
521
|
+
return { targetNamespace: reader.targetNamespace, references: reader.read() };
|
|
522
|
+
};
|
|
523
|
+
const typeExists = (collector, name) => isBuiltin(name) || collector.complex.has(name) || collector.simple.has(name);
|
|
524
|
+
/** Resolve refs, flatten extensions, and report dangling references. */
|
|
525
|
+
export const finishSchemas = (collector) => {
|
|
526
|
+
const questions = [...collector.questions];
|
|
527
|
+
const unresolvedType = (name, location, subject) => {
|
|
528
|
+
questions.push(new OpenQuestion({
|
|
529
|
+
code: "unresolved-type",
|
|
530
|
+
location,
|
|
531
|
+
subject,
|
|
532
|
+
detail: `type ${name} is not defined in any schema that was read`
|
|
533
|
+
}));
|
|
534
|
+
};
|
|
535
|
+
const resolveField = (field, owner, offset, groupOffset) => {
|
|
536
|
+
let type = field.type ?? anyType;
|
|
537
|
+
let nillable = field.nillable;
|
|
538
|
+
let fixed = field.fixed;
|
|
539
|
+
if (field.ref !== undefined) {
|
|
540
|
+
const target = collector.elements.get(field.ref);
|
|
541
|
+
if (target === undefined) {
|
|
542
|
+
questions.push(new OpenQuestion({
|
|
543
|
+
code: "unresolved-element",
|
|
544
|
+
location: field.location,
|
|
545
|
+
subject: owner,
|
|
546
|
+
detail: `element ref ${field.ref} is not declared in any schema that was read`
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
type = target.type;
|
|
551
|
+
nillable = nillable || target.nillable;
|
|
552
|
+
fixed = fixed ?? target.fixed;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else if (!typeExists(collector, type)) {
|
|
556
|
+
unresolvedType(type, field.location, owner);
|
|
557
|
+
}
|
|
558
|
+
return new ElementField({
|
|
559
|
+
name: field.name,
|
|
560
|
+
namespace: field.namespace,
|
|
561
|
+
type,
|
|
562
|
+
minOccurs: field.minOccurs,
|
|
563
|
+
maxOccurs: field.maxOccurs,
|
|
564
|
+
nillable,
|
|
565
|
+
...(field.choice === undefined ? {} : { choice: field.choice + offset }),
|
|
566
|
+
...(field.branch === undefined ? {} : { branch: field.branch + offset }),
|
|
567
|
+
group: field.group + groupOffset,
|
|
568
|
+
occurs: field.own,
|
|
569
|
+
...(fixed === undefined ? {} : { fixed }),
|
|
570
|
+
...optionalDoc(field.documentation)
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
const resolveAttribute = (raw, owner) => {
|
|
574
|
+
if (raw.use === "prohibited")
|
|
575
|
+
return undefined;
|
|
576
|
+
let type = raw.type;
|
|
577
|
+
let fixed = raw.fixed;
|
|
578
|
+
let documentation = raw.documentation;
|
|
579
|
+
if (raw.ref !== undefined) {
|
|
580
|
+
const target = collector.attributes.get(raw.ref);
|
|
581
|
+
if (target === undefined) {
|
|
582
|
+
questions.push(new OpenQuestion({
|
|
583
|
+
code: "unresolved-attribute",
|
|
584
|
+
location: raw.location,
|
|
585
|
+
subject: owner,
|
|
586
|
+
detail: `attribute ref ${raw.ref} is not declared in any schema that was read`
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
type = target.type;
|
|
591
|
+
fixed = fixed ?? target.fixed;
|
|
592
|
+
documentation = documentation ?? target.documentation;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (!typeExists(collector, type))
|
|
596
|
+
unresolvedType(type, raw.location, owner);
|
|
597
|
+
return new AttributeField({
|
|
598
|
+
name: raw.name,
|
|
599
|
+
...(raw.namespace === undefined ? {} : { namespace: raw.namespace }),
|
|
600
|
+
type,
|
|
601
|
+
required: raw.use === "required",
|
|
602
|
+
...(fixed === undefined ? {} : { fixed }),
|
|
603
|
+
...optionalDoc(documentation)
|
|
604
|
+
});
|
|
605
|
+
};
|
|
606
|
+
const sameAttribute = (left, right) => left.name === right.name && (left.namespace ?? "") === (right.namespace ?? "");
|
|
607
|
+
const flattened = new Map();
|
|
608
|
+
const flatten = (raw, visiting) => {
|
|
609
|
+
const done = flattened.get(raw.name);
|
|
610
|
+
if (done !== undefined)
|
|
611
|
+
return done;
|
|
612
|
+
const owner = splitClark(raw.name).local;
|
|
613
|
+
let inheritedFields = [];
|
|
614
|
+
let inheritedGroups = [];
|
|
615
|
+
let inheritedAttributes = [];
|
|
616
|
+
let textType = raw.textType;
|
|
617
|
+
if (raw.base !== undefined && !isBuiltin(raw.base)) {
|
|
618
|
+
const baseRaw = collector.complex.get(raw.base);
|
|
619
|
+
if (baseRaw === undefined) {
|
|
620
|
+
if (!collector.simple.has(raw.base))
|
|
621
|
+
unresolvedType(raw.base, raw.location, owner);
|
|
622
|
+
}
|
|
623
|
+
else if (!visiting.has(raw.base)) {
|
|
624
|
+
const base = flatten(baseRaw, new Set([...visiting, raw.name]));
|
|
625
|
+
// A restriction restates the content; only attributes carry over.
|
|
626
|
+
if (raw.derivation !== "restriction" || raw.simpleContent) {
|
|
627
|
+
inheritedFields = base.fields;
|
|
628
|
+
inheritedGroups = base.groups ?? [];
|
|
629
|
+
}
|
|
630
|
+
inheritedAttributes = base.attributes;
|
|
631
|
+
// Simple content over a complex base: the text type is the base's.
|
|
632
|
+
if (raw.simpleContent && raw.textType === raw.base)
|
|
633
|
+
textType = base.textType ?? stringType;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Choice and branch ids of the derived part continue after the base's,
|
|
637
|
+
// so a base choice and a derived choice stay separate groups.
|
|
638
|
+
const offset = inheritedFields.reduce((max, field) => Math.max(max, (field.choice ?? -1) + 1, (field.branch ?? -1) + 1), 0);
|
|
639
|
+
const groupOffset = inheritedGroups.length;
|
|
640
|
+
const fieldOffset = inheritedFields.length;
|
|
641
|
+
if (textType !== undefined && !typeExists(collector, textType)) {
|
|
642
|
+
unresolvedType(textType, raw.location, owner);
|
|
643
|
+
}
|
|
644
|
+
// Derived attributes restate (or prohibit) inherited ones of the same name.
|
|
645
|
+
const attributes = [];
|
|
646
|
+
for (const inherited of inheritedAttributes) {
|
|
647
|
+
const restated = raw.attributes.find((candidate) => sameAttribute(inherited, candidate));
|
|
648
|
+
if (restated === undefined)
|
|
649
|
+
attributes.push(inherited);
|
|
650
|
+
else {
|
|
651
|
+
const resolved = resolveAttribute(restated, owner);
|
|
652
|
+
if (resolved !== undefined)
|
|
653
|
+
attributes.push(resolved);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (const declared of raw.attributes) {
|
|
657
|
+
if (inheritedAttributes.some((inherited) => sameAttribute(inherited, declared)))
|
|
658
|
+
continue;
|
|
659
|
+
const resolved = resolveAttribute(declared, owner);
|
|
660
|
+
if (resolved !== undefined)
|
|
661
|
+
attributes.push(resolved);
|
|
662
|
+
}
|
|
663
|
+
const groups = [
|
|
664
|
+
...inheritedGroups,
|
|
665
|
+
...raw.groups.map((group) => new ModelGroup({
|
|
666
|
+
...group,
|
|
667
|
+
id: group.id + groupOffset,
|
|
668
|
+
...(group.parent === undefined ? {} : { parent: group.parent + groupOffset }),
|
|
669
|
+
start: group.start + fieldOffset
|
|
670
|
+
}))
|
|
671
|
+
];
|
|
672
|
+
const result = new ComplexTypeDef({
|
|
673
|
+
name: raw.name,
|
|
674
|
+
anonymous: raw.anonymous,
|
|
675
|
+
...(raw.base === undefined || (raw.derivation === "restriction" && !raw.simpleContent)
|
|
676
|
+
? {}
|
|
677
|
+
: { base: raw.base }),
|
|
678
|
+
fields: [
|
|
679
|
+
...inheritedFields,
|
|
680
|
+
...raw.fields.map((field) => resolveField(field, owner, offset, groupOffset))
|
|
681
|
+
],
|
|
682
|
+
...(groups.length === 0 ? {} : { groups }),
|
|
683
|
+
attributes,
|
|
684
|
+
...(textType === undefined ? {} : { textType }),
|
|
685
|
+
...optionalDoc(raw.documentation)
|
|
686
|
+
});
|
|
687
|
+
flattened.set(raw.name, result);
|
|
688
|
+
return result;
|
|
689
|
+
};
|
|
690
|
+
const complex = [...collector.complex.values()].map((raw) => flatten(raw, new Set()));
|
|
691
|
+
// A simpleContent restriction's text type restricts its complex base's
|
|
692
|
+
// text type, known only now.
|
|
693
|
+
const simple = [...collector.simple.values()].map((type) => {
|
|
694
|
+
const complexBase = collector.complex.get(type.base);
|
|
695
|
+
if (complexBase === undefined)
|
|
696
|
+
return type;
|
|
697
|
+
return new SimpleTypeDef({
|
|
698
|
+
...type,
|
|
699
|
+
base: flatten(complexBase, new Set()).textType ?? stringType
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
for (const type of simple) {
|
|
703
|
+
const types = [type.base, ...(type.itemType === undefined ? [] : [type.itemType])];
|
|
704
|
+
for (const name of types) {
|
|
705
|
+
if (!typeExists(collector, name)) {
|
|
706
|
+
unresolvedType(name, collector.locations.get(type.name) ?? "(simple type)", splitClark(type.name).local);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
for (const element of collector.elements.values()) {
|
|
711
|
+
if (!typeExists(collector, element.type)) {
|
|
712
|
+
unresolvedType(element.type, collector.locations.get(element.name) ?? "(global element)", splitClark(element.name).local);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
types: [...complex, ...simple],
|
|
717
|
+
elements: [...collector.elements.values()],
|
|
718
|
+
questions
|
|
719
|
+
};
|
|
720
|
+
};
|