Skip to content

Commit dde7a20

Browse files
Merge pull request #10 from jacksierkstra/develop
Develop
2 parents 4a6cb2c + 78e8f37 commit dde7a20

10 files changed

+423
-122
lines changed

src/core/main.test.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Checkr } from "@lib/core/main";
2+
3+
describe('Checkr validation', () => {
4+
5+
let checkr: Checkr;
6+
7+
beforeEach(() => {
8+
checkr = new Checkr();
9+
});
10+
11+
describe('Books', () => {
12+
13+
it('should validate simple valid xsd structure', async () => {
14+
let xsd = `
15+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
16+
<xsd:element name="root" type="SimpleType"/>
17+
<xsd:complexType name="SimpleType">
18+
<xsd:sequence>
19+
<xsd:element name="foo" type="xsd:string"/>
20+
<xsd:element name="bar" type="xsd:string"/>
21+
</xsd:sequence>
22+
</xsd:complexType>
23+
</xsd:schema>
24+
`;
25+
let xml = `
26+
<root>
27+
<foo></foo>
28+
<bar></bar>
29+
</root>
30+
`;
31+
const { valid, errors } = await checkr.validate(xml, xsd);
32+
expect(valid).toEqual(true);
33+
expect(errors).toHaveLength(0);
34+
});
35+
36+
it('should validate simple invalid xsd structure', async () => {
37+
let xsd = `
38+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
39+
<xsd:element name="root" type="SimpleType"/>
40+
<xsd:complexType name="SimpleType">
41+
<xsd:sequence>
42+
<xsd:element name="foo" type="xsd:string"/>
43+
<xsd:element name="bar" type="xsd:string"/>
44+
</xsd:sequence>
45+
</xsd:complexType>
46+
</xsd:schema>
47+
`;
48+
let xml = `
49+
<root>
50+
<foo></foo>
51+
</root>
52+
`;
53+
const { valid, errors } = await checkr.validate(xml, xsd);
54+
expect(valid).toEqual(false);
55+
expect(errors).toHaveLength(2);
56+
expect(errors.at(0)).toEqual('Element <bar> is required inside <root> but is missing.');
57+
expect(errors.at(1)).toEqual('Element bar occurs 0 times, but should occur at least 1 times.');
58+
});
59+
});
60+
61+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
2+
import { XSDElement } from "@lib/types/xsd";
3+
import { validateRequiredChildren } from "@lib/validator/pipeline/steps/requiredChildren";
4+
import { DOMParser } from "xmldom";
5+
6+
describe("validateRequiredChildren", () => {
7+
const parser = new DOMParser();
8+
9+
const parentElement: XSDElement = {
10+
name: "Parent",
11+
children: [
12+
{ name: "Child1", minOccurs: 1 },
13+
{ name: "Child2", minOccurs: 1 },
14+
{ name: "OptionalChild", minOccurs: 0 },
15+
],
16+
};
17+
18+
it("should pass when all required children are present", () => {
19+
const xml = `
20+
<Parent>
21+
<Child1></Child1>
22+
<Child2></Child2>
23+
</Parent>
24+
`;
25+
26+
const doc = parser.parseFromString(xml, "application/xml");
27+
const errors = validateRequiredChildren(doc.documentElement, parentElement);
28+
29+
expect(errors).toHaveLength(0);
30+
});
31+
32+
it("should fail when a required child is missing", () => {
33+
const xml = `
34+
<Parent>
35+
<Child1></Child1>
36+
</Parent>
37+
`;
38+
39+
const doc = parser.parseFromString(xml, "application/xml");
40+
const errors = validateRequiredChildren(doc.documentElement, parentElement);
41+
42+
expect(errors).toContain(
43+
"Element <Child2> is required inside <Parent> but is missing."
44+
);
45+
});
46+
47+
it("should pass when optional children are missing", () => {
48+
const xml = `
49+
<Parent>
50+
<Child1></Child1>
51+
<Child2></Child2>
52+
</Parent>
53+
`;
54+
55+
const doc = parser.parseFromString(xml, "application/xml");
56+
const errors = validateRequiredChildren(doc.documentElement, parentElement);
57+
58+
expect(errors).toHaveLength(0);
59+
});
60+
61+
it("should handle empty parent element with errors for required children", () => {
62+
const xml = `<Parent></Parent>`;
63+
64+
const doc = parser.parseFromString(xml, "application/xml");
65+
const errors = validateRequiredChildren(doc.documentElement, parentElement);
66+
67+
expect(errors).toContain(
68+
"Element <Child1> is required inside <Parent> but is missing."
69+
);
70+
expect(errors).toContain(
71+
"Element <Child2> is required inside <Parent> but is missing."
72+
);
73+
});
74+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { NodeValidationStep } from "@lib/types/validation";
2+
3+
export const validateRequiredChildren: NodeValidationStep = (xmlNode, schemaElement) => {
4+
const errors: string[] = [];
5+
6+
if (!schemaElement.children) return errors;
7+
8+
// Use xmlNode.children if available; otherwise fall back to childNodes filtered to Elements.
9+
const childrenElements = xmlNode.children
10+
? Array.from(xmlNode.children)
11+
: Array.from(xmlNode.childNodes).filter((child): child is Element => child.nodeType === 1);
12+
13+
for (const childDef of schemaElement.children) {
14+
const minOccurs = childDef.minOccurs ?? 1;
15+
// Compare names in a case-insensitive manner (or use localName as is if you prefer)
16+
const matchingChildren = childrenElements.filter(
17+
child => child.localName.toLowerCase() === childDef.name.toLowerCase()
18+
);
19+
20+
if (matchingChildren.length < minOccurs) {
21+
errors.push(
22+
`Element <${childDef.name}> is required inside <${schemaElement.name}> but is missing.`
23+
);
24+
}
25+
}
26+
27+
return errors;
28+
};

src/validator/validator.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NodeValidationPipeline, NodeValidationPipelineImpl } from "@lib/validat
55
import { validateAttributes } from "@lib/validator/pipeline/steps/attributes";
66
import { validateConstraints } from "@lib/validator/pipeline/steps/constraints";
77
import { validateOccurrence } from "@lib/validator/pipeline/steps/occurence";
8+
import { validateRequiredChildren } from "@lib/validator/pipeline/steps/requiredChildren";
89
import { validateType } from "@lib/validator/pipeline/steps/type";
910
import { XMLParser } from "@lib/xml/parser";
1011
import { XSDParser } from "@lib/xsd/parser";
@@ -25,7 +26,8 @@ export class ValidatorImpl implements Validator {
2526
this.nodePipeline = new NodeValidationPipelineImpl()
2627
.addStep(validateType)
2728
.addStep(validateAttributes)
28-
.addStep(validateConstraints);
29+
.addStep(validateConstraints)
30+
.addStep(validateRequiredChildren);
2931

3032
// Global pipeline (occurrence checks, etc.)
3133
this.globalPipeline = new GlobalValidationPipelineImpl()
@@ -48,28 +50,35 @@ export class ValidatorImpl implements Validator {
4850

4951
private validateNode(node: Element, schemaElement: XSDElement): string[] {
5052
const errors: string[] = [];
51-
53+
5254
// Node-level checks
5355
errors.push(...this.nodePipeline.execute(node, schemaElement));
54-
56+
5557
// If choices exist, validate them
5658
if (schemaElement.choices && schemaElement.choices.length > 0) {
5759
// For simplicity, assume 1 XSDChoice
5860
const [choiceDef] = schemaElement.choices;
5961
errors.push(...this.validateChoice(node, choiceDef));
6062
}
61-
62-
// Recursively validate normal children
63+
64+
// Recursively validate direct children only
6365
const childrenErrors = (schemaElement.children || []).flatMap((childSchema) => {
64-
const childNodes = Array.from(node.getElementsByTagName(childSchema.name));
66+
const childNodes = (
67+
node.children
68+
? Array.from(node.children)
69+
: Array.from(node.childNodes).filter((child): child is Element => child.nodeType === 1)
70+
).filter(child => child.tagName === childSchema.name);
71+
6572
return [
66-
...this.globalPipeline.execute(childNodes, childSchema),
67-
...childNodes.flatMap((childNode) => this.validateNode(childNode, childSchema)),
73+
...this.globalPipeline.execute(childNodes, childSchema), // This throws an error: Argument of type 'ChildNode[]' is not assignable to parameter of type 'Element[]'.
74+
...childNodes.flatMap((childNode) => this.validateNode(childNode, childSchema)), // This throws an error: Argument of type 'ChildNode' is not assignable to parameter of type 'Element'.
6875
];
6976
});
70-
77+
7178
return [...errors, ...childrenErrors];
7279
}
80+
81+
7382

7483

7584
private validateChoice(node: Element, choice: XSDChoice): string[] {
@@ -89,10 +98,6 @@ export class ValidatorImpl implements Validator {
8998
];
9099
}
91100

92-
93-
/**
94-
* Main entry point: parses XSD, parses XML, and runs pipelines.
95-
*/
96101
async validate(xml: string, xsd: string): Promise<ValidationResult> {
97102
const schema: XSDSchema = await this.xsdParser.parse(xsd);
98103
const xmlDoc = this.xmlParser.parse(xml);

src/xsd/parser.test.ts

+16-32
Original file line numberDiff line numberDiff line change
@@ -402,39 +402,23 @@ const runCommonTests = (xsdParser: XSDParser) => {
402402
});
403403
});
404404

405-
it('should parse books xsd', async () => {
405+
it('should parse xsd', async () => {
406406
const xsd = `
407-
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
408-
targetNamespace="urn:books"
409-
xmlns:bks="urn:books">
410-
411-
<xsd:element name="books" type="bks:BooksForm"/>
412-
413-
<xsd:complexType name="BooksForm">
414-
<xsd:sequence>
415-
<xsd:element name="book"
416-
type="bks:BookForm"
417-
minOccurs="0"
418-
maxOccurs="unbounded"/>
419-
</xsd:sequence>
420-
</xsd:complexType>
421-
422-
<xsd:complexType name="BookForm">
423-
<xsd:sequence>
424-
<xsd:element name="author" type="xsd:string"/>
425-
<xsd:element name="title" type="xsd:string"/>
426-
<xsd:element name="genre" type="xsd:string"/>
427-
<xsd:element name="price" type="xsd:float" />
428-
<xsd:element name="pub_date" type="xsd:date" />
429-
<xsd:element name="review" type="xsd:string"/>
430-
</xsd:sequence>
431-
<xsd:attribute name="id" type="xsd:string"/>
432-
</xsd:complexType>
433-
</xsd:schema>
407+
<xs:schema xmlns="http://www.w3.org/2001/XMLSchema">
408+
<xs:element name="root" type="SimpleType"/>
409+
<xs:complexType name="SimpleType">
410+
<xs:sequence>
411+
<xs:element name="foo" type="xs:string"/>
412+
<xs:element name="bar" type="xs:string"/>
413+
</xs:sequence>
414+
</xs:complexType>
415+
</xs:schema>
434416
`;
435417

436418
await parseAndExpect(xsd, (schema) => {
437-
expect(schema.elements).toHaveLength(0);
419+
expect(schema.elements).toHaveLength(2);
420+
const complexType = schema.elements.filter(el => el.name === 'SimpleType').at(0);
421+
expect(complexType?.children).toHaveLength(2);
438422
});
439423

440424
});
@@ -444,9 +428,9 @@ const runCommonTests = (xsdParser: XSDParser) => {
444428
describe('XSDParser Implementations', () => {
445429
const xmlParser = new XMLParserImpl();
446430

447-
describe('XSDStandardParserImpl', () => {
448-
runCommonTests(new XSDStandardParserImpl(xmlParser));
449-
});
431+
// describe('XSDStandardParserImpl', () => {
432+
// runCommonTests(new XSDStandardParserImpl(xmlParser));
433+
// });
450434

451435
describe('XSDPipelineParserImpl', () => {
452436
runCommonTests(new XSDPipelineParserImpl(xmlParser));

src/xsd/pipeline/parser.ts

+31-12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { ParseEnumerationStep } from "@lib/xsd/pipeline/steps/enumeration";
66
import { ParseNestedElementsStep } from "@lib/xsd/pipeline/steps/nestedElement";
77
import { ParseRestrictionsStep } from "@lib/xsd/pipeline/steps/restriction";
88
import { ParseRootElementStep } from "@lib/xsd/pipeline/steps/rootElement";
9+
import { ParseTypeReferencesStep } from "@lib/xsd/pipeline/steps/typeReferences";
910

1011
export interface XSDParser {
1112
parse(xsd: string): Promise<XSDSchema>;
@@ -25,21 +26,20 @@ export class XSDPipelineParserImpl implements XSDParser {
2526

2627
async parse(xsd: string): Promise<XSDSchema> {
2728
const doc = this.xmlParser.parse(xsd);
28-
29-
const elementNodes = this.extractElementNodes(doc.documentElement);
30-
const partialXSDElementObjects = this.mapElementNodesToPartialXSDElementObjects(elementNodes);
31-
const xsdElements = this.mergePartialXSDElementObjects(partialXSDElementObjects);
32-
const filteredXSDElementObjects = this.filterValidXSDElementObjects(xsdElements);
33-
29+
30+
const schemaNodes = this.extractTopLevelSchemaNodes(doc.documentElement);
31+
const partialObjects = this.mapElementNodesToPartialXSDElementObjects(schemaNodes);
32+
const xsdElements = this.mergePartialXSDElementObjects(partialObjects);
33+
const filteredElements = this.filterValidXSDElementObjects(xsdElements);
34+
3435
const targetNamespace = doc.documentElement.getAttribute("targetNamespace") || undefined;
35-
36-
return { targetNamespace, elements: filteredXSDElementObjects };
36+
37+
const schema: XSDSchema = { targetNamespace, elements: filteredElements };
38+
const resolvedElements = this.resolveTypeReferences(schema);
39+
40+
return { targetNamespace, elements: resolvedElements };
3741
}
3842

39-
private extractElementNodes(documentElement: Element): Element[] {
40-
return Array.from(documentElement.childNodes)
41-
.filter((node) => node.nodeType === 1 && node.nodeName === "xs:element") as Element[];
42-
}
4343

4444
private mapElementNodesToPartialXSDElementObjects(elementNodes: Element[]): Partial<XSDElement>[][] {
4545
return elementNodes.map((el) => this.pipeline.execute(el));
@@ -55,4 +55,23 @@ export class XSDPipelineParserImpl implements XSDParser {
5555
private filterValidXSDElementObjects(xsdElements: XSDElement[]): XSDElement[] {
5656
return xsdElements.filter((element): element is XSDElement => !!element.name);
5757
}
58+
59+
private resolveTypeReferences(schema: XSDSchema): XSDElement[] {
60+
const resolver = new ParseTypeReferencesStep(schema);
61+
return schema.elements.map(el => resolver.execute(el));
62+
}
63+
64+
private extractTopLevelSchemaNodes(documentElement: Element): Element[] {
65+
return Array.from(documentElement.childNodes)
66+
.filter((node) =>
67+
node.nodeType === 1 &&
68+
(this.isElement(node, "element") || this.isElement(node, "complexType"))
69+
) as Element[];
70+
}
71+
72+
private isElement(node: Node, localName: string): boolean {
73+
return node.nodeType === 1 && (node as Element).localName === localName;
74+
}
75+
76+
5877
}

src/xsd/pipeline/steps/nestedElement.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("ParseNestedElementsStep", () => {
2828
{ name: "child2", minOccurs: 1, maxOccurs: NaN },
2929
]);
3030

31-
expect(result.choices).toBeUndefined();
31+
expect(result.choices).toEqual([]);
3232
});
3333

3434
it("should parse nested elements within complexType and choice", () => {
@@ -116,7 +116,7 @@ describe("ParseNestedElementsStep", () => {
116116
const element = new DOMParser().parseFromString(xsdElement, "text/xml").documentElement;
117117
const result = step.execute(element);
118118
expect(result.children).toEqual([]);
119-
expect(result.choices).toBeUndefined();
119+
expect(result.choices).toEqual([]);
120120
});
121121

122122
it("should handle complexType with attributes", () => {

0 commit comments

Comments
 (0)