UNPKG

xml-helper-ts

Version:

A TypeScript library for XML parsing, validation with XSD schema, and XML/JSON conversion - built from scratch without external dependencies

192 lines 9.84 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.XmlValidator = void 0; class XmlValidator { constructor(schema) { this.errors = []; this.schema = schema; } validate(xmlNode) { this.errors = []; // Find root element in schema const rootElement = this.schema.elements[xmlNode.name]; if (!rootElement) { this.addError(1, 1, `Root element '${xmlNode.name}' not found in schema`, 'ELEMENT_NOT_FOUND'); return this.errors; } this.validateElement(xmlNode, rootElement, 1, 1); return this.errors; } validateElement(xmlNode, xsdElement, line, column) { // Validate element name if (xmlNode.name !== xsdElement.name) { this.addError(line, column, `Expected element '${xsdElement.name}' but found '${xmlNode.name}'`, 'ELEMENT_MISMATCH'); return; } // Validate attributes this.validateAttributes(xmlNode, xsdElement, line, column); // Validate content based on type if (this.isBuiltInType(xsdElement.type)) { this.validateSimpleContent(xmlNode, xsdElement, line, column); } else if (this.schema.complexTypes[xsdElement.type]) { this.validateComplexContent(xmlNode, this.schema.complexTypes[xsdElement.type], line, column); } else if (this.schema.simpleTypes[xsdElement.type]) { this.validateSimpleTypeContent(xmlNode, this.schema.simpleTypes[xsdElement.type], line, column); } else if (xsdElement.type.startsWith('#inline-')) { // Handle inline types this.validateInlineContent(xmlNode, xsdElement, line, column); } } validateAttributes(xmlNode, xsdElement, line, column) { // Check if we have a complex type with attributes const complexType = this.schema.complexTypes[xsdElement.type]; if (!complexType) return; // Validate required attributes for (const xsdAttr of complexType.attributes) { if (xsdAttr.use === 'required' && !xmlNode.attributes[xsdAttr.name]) { this.addError(line, column, `Required attribute '${xsdAttr.name}' is missing`, 'MISSING_REQUIRED_ATTRIBUTE'); } } // Validate attribute values for (const [attrName, attrValue] of Object.entries(xmlNode.attributes)) { const xsdAttr = complexType.attributes.find(a => a.name === attrName); if (!xsdAttr) { this.addError(line, column, `Attribute '${attrName}' is not allowed`, 'UNEXPECTED_ATTRIBUTE'); continue; } if (xsdAttr.fixedValue && attrValue !== xsdAttr.fixedValue) { this.addError(line, column, `Attribute '${attrName}' must have value '${xsdAttr.fixedValue}'`, 'FIXED_VALUE_VIOLATION'); } // Validate attribute type if (!this.validateSimpleValue(attrValue, xsdAttr.type)) { this.addError(line, column, `Invalid value '${attrValue}' for attribute '${attrName}' of type '${xsdAttr.type}'`, 'INVALID_ATTRIBUTE_VALUE'); } } } validateSimpleContent(xmlNode, xsdElement, line, column) { if (xmlNode.children.length > 0 && xmlNode.children.some(child => child.name !== '#text')) { this.addError(line, column, `Element '${xmlNode.name}' should contain simple content but has child elements`, 'INVALID_CONTENT'); return; } const textContent = xmlNode.text || xmlNode.children.find(child => child.name === '#text')?.text || ''; if (!this.validateSimpleValue(textContent, xsdElement.type)) { this.addError(line, column, `Invalid value '${textContent}' for element '${xmlNode.name}' of type '${xsdElement.type}'`, 'INVALID_ELEMENT_VALUE'); } // Apply restrictions if any if (xsdElement.restrictions) { this.validateRestrictions(textContent, xsdElement.restrictions, line, column, xmlNode.name); } } validateSimpleTypeContent(xmlNode, simpleType, line, column) { const textContent = xmlNode.text || xmlNode.children.find(child => child.name === '#text')?.text || ''; if (!this.validateSimpleValue(textContent, simpleType.baseType)) { this.addError(line, column, `Invalid value '${textContent}' for base type '${simpleType.baseType}'`, 'INVALID_SIMPLE_TYPE_VALUE'); } this.validateRestrictions(textContent, simpleType.restrictions, line, column, xmlNode.name); } validateComplexContent(xmlNode, complexType, line, column) { // Validate child elements const childElements = xmlNode.children.filter(child => child.name !== '#text'); // Check required elements for (const xsdElement of complexType.elements) { const matchingChildren = childElements.filter(child => child.name === xsdElement.name); if (matchingChildren.length < xsdElement.minOccurs) { this.addError(line, column, `Element '${xsdElement.name}' occurs ${matchingChildren.length} times but minimum is ${xsdElement.minOccurs}`, 'MIN_OCCURS_VIOLATION'); } if (xsdElement.maxOccurs !== 'unbounded' && matchingChildren.length > xsdElement.maxOccurs) { this.addError(line, column, `Element '${xsdElement.name}' occurs ${matchingChildren.length} times but maximum is ${xsdElement.maxOccurs}`, 'MAX_OCCURS_VIOLATION'); } // Validate each occurrence for (const childElement of matchingChildren) { this.validateElement(childElement, xsdElement, line, column); } } // Check for unexpected elements for (const childElement of childElements) { if (!complexType.elements.some(xsdEl => xsdEl.name === childElement.name)) { this.addError(line, column, `Unexpected element '${childElement.name}'`, 'UNEXPECTED_ELEMENT'); } } } validateInlineContent(xmlNode, xsdElement, line, column) { // Handle inline content validation if (xsdElement.restrictions) { const textContent = xmlNode.text || xmlNode.children.find(child => child.name === '#text')?.text || ''; this.validateRestrictions(textContent, xsdElement.restrictions, line, column, xmlNode.name); } } validateRestrictions(value, restrictions, line, column, elementName) { for (const restriction of restrictions) { switch (restriction.type) { case 'minLength': if (value.length < Number(restriction.value)) { this.addError(line, column, `Value '${value}' in element '${elementName}' is too short. Minimum length is ${restriction.value}`, 'MIN_LENGTH_VIOLATION'); } break; case 'maxLength': if (value.length > Number(restriction.value)) { this.addError(line, column, `Value '${value}' in element '${elementName}' is too long. Maximum length is ${restriction.value}`, 'MAX_LENGTH_VIOLATION'); } break; case 'pattern': const regex = new RegExp(String(restriction.value)); if (!regex.test(value)) { this.addError(line, column, `Value '${value}' in element '${elementName}' does not match pattern '${restriction.value}'`, 'PATTERN_VIOLATION'); } break; case 'enumeration': // Note: This would need to collect all enumeration values break; case 'minInclusive': if (Number(value) < Number(restriction.value)) { this.addError(line, column, `Value '${value}' in element '${elementName}' is below minimum ${restriction.value}`, 'MIN_INCLUSIVE_VIOLATION'); } break; case 'maxInclusive': if (Number(value) > Number(restriction.value)) { this.addError(line, column, `Value '${value}' in element '${elementName}' is above maximum ${restriction.value}`, 'MAX_INCLUSIVE_VIOLATION'); } break; } } } validateSimpleValue(value, type) { switch (type) { case 'string': return true; case 'int': case 'integer': return /^-?\d+$/.test(value); case 'decimal': case 'float': case 'double': return /^-?\d*\.?\d+$/.test(value); case 'boolean': return value === 'true' || value === 'false' || value === '1' || value === '0'; case 'date': return /^\d{4}-\d{2}-\d{2}$/.test(value); case 'dateTime': return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.test(value); case 'time': return /^\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.test(value); default: return true; // Unknown types are assumed valid } } isBuiltInType(type) { const builtInTypes = [ 'string', 'int', 'integer', 'decimal', 'float', 'double', 'boolean', 'date', 'dateTime', 'time', 'base64Binary', 'hexBinary' ]; return builtInTypes.includes(type); } addError(line, column, message, code) { this.errors.push({ line, column, message, code }); } } exports.XmlValidator = XmlValidator; //# sourceMappingURL=xml-validator.js.map