xsd-lookup
Version:
Multi-schema XSD lookup utility
657 lines (500 loc) ⢠25.9 kB
Markdown
# XSD schema validation for XML nodes, attributes and values
A comprehensive TypeScript-based XSD schema validation system for XML files, which provides nodes, attributes, and values validation against XSD schemas, including the strict hierarchical dependencies.
Developed to support scripting language validation in **X4: Foundations** game modding, but can be used for any XML validation needs, based on XSD schemas.
## šÆ Features
- **š XSD-Based Validation**: Pure XSD schema validation without hardcoded logic
- **š Comprehensive Attribute Validation**: Validates attribute existence, types, and values
- **š§ Infrastructure Attribute Filtering**: Automatically ignores XML namespace attributes (`xmlns`, `xmlns:*`, `xsi:*`)
- **š Type Inheritance Support**: Handles complex XSD type hierarchies and restrictions
- **š§© Hierarchical Context**: Getting list of possible child elements based on current element hierarchy
- **š Previous Sibling Context**: Optional previous sibling element name to filter results based on sequence constraints
- **š Union Type Processing**: Merges validation rules from multiple member types
- **š Enumeration Support**: Complete enumeration value extraction and validation with annotations
- **šÆ SimpleType Enumeration Discovery**: Extract enumeration values from named SimpleTypes, including union types
- **š Range Validation**: Numeric and length constraint validation
- **šļø Multi-line Normalization**: Handles multi-line XML attribute values correctly
- **š Performance Optimized**: Caching and indexing for fast validation
## š Quick Start
### Installation
```powershell
npm install xsd_lookup --save
```
### Basic Usage
```typescript
// ES6 imports (from TypeScript/modern JavaScript)
import { XsdReference } from 'xsd_lookup';
// Initialize the validation system
const xsdRef = new XsdReference('./tests/data/xsd');
const elementName = 'set_value';
const elementHierarchy = ['actions', 'attention', 'aiscript']; // Please pay attention to the hierarchy order, it should be bottom-up (from immediate parent to root element)
const elementDefinition = xsdRef.getElementDefinition(elementName, elementHierarchy);
console.log(`Element ${elementName} is ${elementDefinition ? 'defined' : 'not defined'} in the schema.`);
const elementAttributes = xsdRef.getElementAttributesWithTypes(elementName, elementHierarchy);
console.log(`Element ${elementName} has ${elementAttributes.length} attributes.`);
const attributeValues = xsdRef.getAttributePossibleValues(elementAttributes, 'operation');
console.log(`Possible values for 'operation': ${Array.from(attributeValues.keys()).join(', ')}`);
const checkAttributeValue = xsdRef.validateAttributeValueAgainstRules(elementAttributes, 'operation', 'unknown');
console.log(`Attribute 'operation' value 'unknown' is ${checkAttributeValue.isValid ? 'valid' : 'invalid'}.`);
// Get enumeration values from SimpleType definitions
const classEnums = xsdRef.getSimpleTypeEnumerationValues('common', 'classlookup');
if (classEnums) {
console.log(`Found ${classEnums.values.length} class types: ${classEnums.values.slice(0, 3).join(', ')}...`);
}
// Clean up resources when done (optional but recommended)
xsdRef.dispose();
```
## š API Reference
### š·ļø Type Definitions
#### Exported Interfaces
```typescript
// Basic attribute information
export interface AttributeInfo {
name: string;
node: Element; // DOM element reference
}
// Enhanced attribute information with validation rules
interface EnhancedAttributeInfo {
name: string;
type: string;
required: boolean;
patterns?: string[];
enumValues?: string[];
enumValuesAnnotations?: Map<string, string>;
minLength?: number;
maxLength?: number;
minInclusive?: number;
maxInclusive?: number;
minExclusive?: number;
maxExclusive?: number;
}
// Attribute validation result
interface AttributeValidationResult {
isValid: boolean;
errorMessage?: string;
expectedType?: string;
restrictions?: string[];
allowedValues?: string[];
}
```
### š·ļø Input parameters
- `schemaName`: Name of the schema to operate on (e.g., 'aiscripts'). Equal to the XSD file name without extension.
- `elementName`: Name of the XML element to validate (e.g., 'do_if').
- `attributeName`: Name of the attribute to validate (e.g., 'value').
- `value`: The value to validate against the attribute's XSD rules (e.g., 'player.money gt 1000').
- `hierarchy`: Parameter to specify the hierarchy of elements in bottom-up order (from immediate parent to root element) in XML-file. This is crucial for correct validation context.
#### šļø Hierarchy Parameter Usage
**Important**: All methods that accept a `hierarchy` parameter expect it in **bottom-up order** (from immediate parent to root element).
##### Hierarchy Examples
```typescript
// For XML structure:
// <aiscript>
// <attention>
// <actions>
// <do_if value="$condition">
// <debug_text text="message" />
// </do_if>
// <set_value name="$value" exact="100" />
// </actions>
// </attention>
// </aiscript>
// For 'do_if' element:
const doIfHierarchy = ['actions', 'attention', 'aiscript'];
// For 'debug_text' element:
const debugTextHierarchy = ['do_if', 'actions', 'attention', 'aiscript'];
// Usage:
const attributes = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', doIfHierarchy);
const validation = xsdRef.validateAttributeValue('aiscripts', 'debug_text', 'text', 'Hello', debugTextHierarchy);
```
### XsdReference Class
The main entry point for any operations.
#### Constructor
```typescript
new XsdReference(xsdDirectory: string)
```
#### Core Methods
##### `getSchema(schemaName: string): Schema | null`
Load and return a schema by name.
```typescript
const schema = xsdRef.getSchema('aiscripts');
```
##### `getAvailableSchemas(): string[]`
Get all currently loaded schema types.
```typescript
const loadedSchemas: string[] = xsdRef.getAvailableSchemas();
// Returns: ['aiscripts', 'md'] (schemas that are currently loaded in memory)
```
##### `getDiscoverableSchemas(): string[]`
Get all discoverable schema files in the XSD directory.
```typescript
const availableSchemas: string[] = xsdRef.getDiscoverableSchemas();
// Returns: ['aiscripts', 'common', 'md'] (all .xsd files found in directory, without extension)
```
##### `getElementDefinition(schemaName: string, elementName: string, hierarchy?: string[]): Element | undefined`
Get the element definition for a specific element in a schema, considering hierarchy context.
**Important**: The `hierarchy` parameter should be provided in **bottom-up order** (from immediate parent to root element).
```typescript
// For element structure: <aiscript><attention><actions><set_value>
// Hierarchy for 'set_value' element should be: ['actions', 'attention', 'aiscript']
const elementDef = xsdRef.getElementDefinition('aiscripts', 'set_value', ['actions', 'attention', 'aiscript']);
// Returns: Element definition object or undefined if not found
```
##### `getElementAttributes(schemaName: string, elementName: string, hierarchy?: string[]): AttributeInfo[]`
Get basic attribute information for an element.
**Important**: The `hierarchy` parameter should be provided in **bottom-up order** (from immediate parent to root element).
```typescript
// For element structure: <aiscript><attention><actions><set_value>
// Hierarchy for 'set_value' element should be: ['actions', 'attention', 'aiscript']
const attributes: AttributeInfo[] = xsdRef.getElementAttributes('aiscripts', 'set_value', ['actions', 'attention', 'aiscript']);
// Returns:
// [{
// name: 'name',
// node: Element // DOM element reference
// },
// {
// name: 'value',
// node: Element // DOM element reference
// },
// ...]
```
##### `getElementAttributesWithTypes(schemaName: string, elementName: string, hierarchy?: string[]): EnhancedAttributeInfo[]`
Get all attributes for an element with complete type information including:
- Type name
- Required status
- Enumeration values (if applicable)
- Pattern restrictions
- Numeric/length constraints
**Important**: The `hierarchy` parameter should be provided in **bottom-up order** (from immediate parent to root element).
```typescript
// For element structure: <aiscript><attention><actions><do_if>
// Hierarchy for 'do_if' element should be: ['actions', 'attention', 'aiscript']
const attributes: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
// Returns:
// [{
// name: 'value',
// type: 'expression',
// required: true,
// patterns: ['[pattern regex]'],
// enumValues: undefined
// }
// ...]
```
##### `validateAttributeValue(schemaName: string, elementName: string, attributeName: string, value: string, hierarchy?: string[]): AttributeValidationResult`
Validate an attribute value against XSD constraints.
**Important**: The `hierarchy` parameter should be provided in **bottom-up order** (from immediate parent to root element).
```typescript
// For element structure: <aiscript><attention><actions><do_if><debug_text>
// Hierarchy for 'debug_text' element should be: ['do_if', 'actions', 'attention', 'aiscript']
const result: AttributeValidationResult = xsdRef.validateAttributeValue('aiscript', 'debug_text', 'chance', '50', ['do_if', 'actions', 'attention', 'aiscript']);
// Returns:
// {
// isValid: true,
// expectedType: 'expression',
// restrictions: ['Pattern: ...']
// }
```
##### `getPossibleChildElements(schemaName: string, elementName: string, hierarchy?: string[], previousSibling?: string): Map<string, string>`
Get possible child elements for a given element, with their annotation text.
**Important**: The `hierarchy` parameter should be provided in **bottom-up order** (from immediate parent to root element).
**Important**: The `previousSibling` parameter can be used to filter results based on sequence constraints (e.g., if the element must come after a specific sibling).
```typescript
// For element structure: <aiscript><attention><actions>
// Get possible children of 'actions' element
const children: Map<string, string> = xsdRef.getPossibleChildElements('aiscripts', 'actions', ['attention', 'aiscript']);
// Returns: Map where key is child element name, value is annotation text
// Example: Map { 'do_if' => 'Conditional execution', 'do_while' => 'Loop execution', ... }
// Usage examples:
if (children.size > 0) {
console.log('Possible child elements:');
for (const [elementName, annotation] of children) {
console.log(` ${elementName}: ${annotation || '(no description)'}`);
}
}
```
##### `getSimpleTypesWithBaseType(schemaName: string, baseType: string): string[]`
Get all simple types that use a specific type as their base.
```typescript
// Find all simple types based on 'lvalueexpression'
const derivedTypes: string[] = xsdRef.getSimpleTypesWithBaseType('aiscripts', 'lvalueexpression');
// Returns: ['expression', 'objectref', 'paramname', ...] (types that extend lvalueexpression)
// Find all types based on xs:string
const stringTypes: string[] = xsdRef.getSimpleTypesWithBaseType('common', 'xs:string');
// Returns: ['name', 'comment', 'text', ...] (string-based types)
```
##### `getSimpleTypeEnumerationValues(schemaName: string, simpleTypeName: string): { values: string[], annotations: Map<string, string> } | null`
Get enumeration values for a named SimpleType, including support for union types.
**Union Type Support**: This method properly handles union types by extracting enumeration values from all member types and combining them into a single result.
```typescript
// Get enumeration values from a simple enumeration type
const quadrantResult = xsdRef.getSimpleTypeEnumerationValues('common', 'quadrantlookup');
// Returns:
// {
// values: ['quadrant.none', 'quadrant.front', 'quadrant.back'],
// annotations: Map {
// 'quadrant.none' => 'No specific quadrant',
// 'quadrant.front' => 'Front quadrant',
// 'quadrant.back' => 'Back quadrant'
// }
// }
// Get enumeration values from a union type (combines multiple enumerations)
const classResult = xsdRef.getSimpleTypeEnumerationValues('common', 'classlookup');
// Returns:
// {
// values: ['class.component', 'class.buildprocessor', 'class.ship', 'class.station', ...],
// annotations: Map {
// 'class.component' => 'Component',
// 'class.buildprocessor' => 'Build processor',
// 'class.ship' => 'Ship',
// // ... 80+ values from union of 'positionalclasslookup' and 'expression' types
// }
// }
// Handle non-existent or non-enumeration types
const nonExistent = xsdRef.getSimpleTypeEnumerationValues('common', 'nonexistent');
// Returns: null
// Usage example:
if (classResult) {
console.log(`Found ${classResult.values.length} enumeration values`);
for (const value of classResult.values) {
const annotation = classResult.annotations.get(value);
console.log(` ${value}: ${annotation || '(no description)'}`);
}
// Check if a specific value exists
if (classResult.values.includes('class.ship')) {
console.log('class.ship is a valid enumeration value');
}
}
```
##### `dispose(): void`
Clear all internal caches to release resources.
```typescript
// Get a schema instance
const schema = xsdRef.getSchema('aiscripts');
// Use the schema for validation...
const result = schema.validateAttributeValue('do_if', 'value', 'condition');
// Clean up schema resources when done
schema.dispose();
```
**Note**: Not recommended for frequent use in performance-sensitive scenarios.
#### Static Methods
##### `XsdReference.validateAttributeNames(attributeInfos: EnhancedAttributeInfo[], providedAttributes: string[]): AttributeNameValidationResult`
Validate attribute names against schema definitions. This static method checks which attributes are valid and identifies missing required attributes.
**š§ Infrastructure Attribute Handling**: XML infrastructure attributes (`xmlns`, `xmlns:*`, `xsi:*`) are automatically filtered out before validation.
```typescript
// Get attribute info first
const attributeInfos: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
const providedAttrs = ['value', 'chance', 'xmlns:xsi', 'invalid_attr'];
const nameValidation: AttributeNameValidationResult = XsdReference.validateAttributeNames(attributeInfos, providedAttrs);
// Returns:
// {
// wrongAttributes: ['invalid_attr'], // Infrastructure attrs filtered out
// missingRequiredAttributes: ['negate', ...] // Required attributes missing
// }
// Note: 'xmlns:xsi' is ignored and doesn't appear in wrongAttributes
```
##### `XsdReference.validateAttributeValueAgainstRules(attributeInfos: EnhancedAttributeInfo[], attributeName: string, attributeValue: string)`
Validate an attribute value against all XSD rules (patterns, enumerations, ranges, etc.). This static method provides detailed validation with rule violation information.
**š§ Infrastructure Attribute Handling**: XML infrastructure attributes (`xmlns`, `xmlns:*`, `xsi:*`) are automatically ignored and always return `{ isValid: true }`.
```typescript
// Get attribute info first
const attributeInfos: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
const valueValidation = XsdReference.validateAttributeValueAgainstRules(
attributeInfos,
'value',
'player.money gt 1000'
);
// Returns:
// {
// isValid: true,
// errorMessage: undefined, // Only present if invalid
// violatedRules: undefined // Only present if invalid - array of specific rule violations
// }
// Example with invalid value:
const invalidValidation = XsdReference.validateAttributeValueAgainstRules(
attributeInfos,
'chance',
'150'
);
// Returns:
// {
// isValid: false,
// errorMessage: "Value must be <= 100",
// violatedRules: ["Value must be <= 100"]
// }
// Infrastructure attributes are ignored:
const infraValidation = XsdReference.validateAttributeValueAgainstRules(
attributeInfos,
'xmlns:xsi',
'http://www.w3.org/2001/XMLSchema-instance'
);
// Returns:
// {
// isValid: true
// }
```
##### `XsdReference.getAttributePossibleValues(attributeInfos: EnhancedAttributeInfo[], attributeName: string): Map<string, string>`
Get all possible enumeration values for an attribute, if it has enumeration restrictions.
**š§ Infrastructure Attribute Handling**: XML infrastructure attributes always return an empty Map.
```typescript
// Get attribute info first
const attributeInfos: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'set_value', ['actions', 'attention', 'aiscript']);
const possibleValues: Map<string, string> = XsdReference.getAttributePossibleValues(attributeInfos, 'operator');
// Returns: Map<string, string> where key is enum value, value is annotation
// Example: Map { 'set' => 'Set value', 'add' => 'Add to value', 'subtract' => 'Subtract from value', ... }
// Returns: Map() (empty map if no enumeration exists or if it's an infrastructure attribute)
// Usage examples:
if (possibleValues.size > 0) {
console.log('Available values:');
for (const [value, annotation] of possibleValues) {
console.log(` ${value}: ${annotation || '(no description)'}`);
}
// Get just the values as an array
const valueArray = Array.from(possibleValues.keys());
console.log('Values only:', valueArray); // ['set', 'add', 'subtract', 'insert']
}
```
##### `XsdReference.filterAttributesByType(attributeInfos: EnhancedAttributeInfo[], attributeType: string): string[]`
Filter attributes by their XSD type (e.g., 'xs:string', 'xs:int', 'expression').
**š§ Infrastructure Attribute Handling**: XML infrastructure attributes are automatically excluded from results.
```typescript
// Get attribute info first
const attributeInfos: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
const stringAttributes: string[] = XsdReference.filterAttributesByType(attributeInfos, 'xs:string');
// Returns: ['comment', 'name'] (example - attributes with xs:string type)
const expressionAttributes: string[] = XsdReference.filterAttributesByType(attributeInfos, 'expression');
// Returns: ['value', 'chance'] (example - attributes with expression type)
```
##### `XsdReference.filterAttributesByRestriction(attributeInfos: EnhancedAttributeInfo[], restrictionType: 'enumeration' | 'pattern' | 'length' | 'range'): string[]`
Filter attributes by the type of XSD restriction they have.
**š§ Infrastructure Attribute Handling**: XML infrastructure attributes are automatically excluded from results.
```typescript
// Get attribute info first
const attributeInfos: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
const enumAttributes: string[] = XsdReference.filterAttributesByRestriction(attributeInfos, 'enumeration');
// Returns: ['operator', 'type'] (example - attributes with enumeration restrictions)
const patternAttributes: string[] = XsdReference.filterAttributesByRestriction(attributeInfos, 'pattern');
// Returns: ['ref', 'name'] (example - attributes with pattern restrictions)
const rangeAttributes: string[] = XsdReference.filterAttributesByRestriction(attributeInfos, 'range');
// Returns: ['min', 'max'] (example - attributes with numeric range restrictions)
const lengthAttributes: string[] = XsdReference.filterAttributesByRestriction(attributeInfos, 'length');
// Returns: ['text'] (example - attributes with length restrictions)
```
##### `XsdReference.extractAnnotationText(element: Element): string | undefined`
Extract annotation text from an XML element. This provides access to Schema's annotation extraction functionality.
```typescript
// This is typically used internally, but can be useful for custom schema processing
const element: Element = /* DOM element from XSD */;
const annotation: string | undefined = XsdReference.extractAnnotationText(element);
// Returns: "Description text from xs:annotation/xs:documentation" or undefined
// Example usage with element definitions:
const elementDef = xsdRef.getElementDefinition('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
if (elementDef) {
const description = XsdReference.extractAnnotationText(elementDef);
console.log('Element description:', description || 'No description available');
}
```
#### Typical Validation Workflow
```typescript
// 1. Get schema and attribute info once
const xsdRef = new XsdReference('./tests/data/xsd');
const attributeInfos: EnhancedAttributeInfo[] = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', ['actions', 'attention', 'aiscript']);
// 2. Validate attribute names (fast, no schema lookup needed)
// Infrastructure attributes (xmlns, xsi:) are automatically filtered out
const nameValidation: AttributeNameValidationResult = XsdReference.validateAttributeNames(attributeInfos, ['value', 'xmlns:xsi', 'invalid_attr']);
if (nameValidation.wrongAttributes.length > 0) {
console.error('Invalid attributes:', nameValidation.wrongAttributes); // Only shows 'invalid_attr'
}
// 3. Validate attribute values (fast, no schema lookup needed)
// Infrastructure attributes automatically return { isValid: true }
for (const attrName of validAttributes) {
const valueValidation = XsdReference.validateAttributeValueAgainstRules(
attributeInfos,
attrName,
attributeValue
);
if (!valueValidation.isValid) {
console.error(`Invalid value for ${attrName}:`, valueValidation.violatedRules);
}
}
// 4. Use helper methods to analyze attributes by type or restriction
const enumAttributes: string[] = XsdReference.filterAttributesByRestriction(attributeInfos, 'enumeration');
const possibleValues: Map<string, string> = XsdReference.getAttributePossibleValues(attributeInfos, 'operator');
// Work with possible values Map
if (possibleValues.size > 0) {
console.log('Operator values:', Array.from(possibleValues.keys()));
// Check if a specific value is valid
if (possibleValues.has('eq')) {
console.log('eq annotation:', possibleValues.get('eq') || '(no description)');
}
}
```
### Schema Class
There is a main engine for validating XML files against XSD schemas. It provides detailed validation capabilities for a specific XSD schema.
**Note**: Not recommended to use directly, prefer using `XsdReference` for schema management.
### XsdDetector Class
Automatically detects the appropriate XSD schema for XML files.
#### `getSchemaName(xmlFilePath: string): string | null`
Detect schema name from XML file path and content.
```typescript
const schemaName = XsdDetector.getSchemaName('./scripts/my-script.xml');
// Returns: 'aiscripts'
```
### ļæ½ Infrastructure Attribute Handling
The validation system automatically handles XML infrastructure attributes to focus validation on content-relevant attributes:
#### What are Infrastructure Attributes?
Infrastructure attributes are XML namespace and schema-related attributes that are part of the XML specification itself, not your content:
- `xmlns` - Default namespace declaration
- `xmlns:*` - Namespace prefix declarations (e.g., `xmlns:xsi`)
- `xsi:*` - XML Schema Instance attributes (e.g., `xsi:schemaLocation`)
#### Automatic Filtering
All static validation methods automatically filter out infrastructure attributes:
```xml
<!-- Example XML with infrastructure attributes -->
<do_if value="player.money gt 1000"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="..."
chance="50">
<!-- content -->
</do_if>
```
```typescript
// Only 'value' and 'chance' are validated; xmlns/xsi attributes are ignored
const attributeInfos = xsdRef.getElementAttributesWithTypes('aiscripts', 'do_if', hierarchy);
const nameValidation = XsdReference.validateAttributeNames(
attributeInfos,
['value', 'chance', 'xmlns:xsi', 'xsi:schemaLocation']
);
// Result: { wrongAttributes: [], missingRequiredAttributes: [] }
// Infrastructure attributes don't appear in wrongAttributes
const valueValidation = XsdReference.validateAttributeValueAgainstRules(
attributeInfos,
'xmlns:xsi',
'http://www.w3.org/2001/XMLSchema-instance'
);
// Result: { isValid: true } - Infrastructure attributes always pass
```
## š§ Development
### Project Structure
```text
āāā src/ # TypeScript source files
ā āāā Schema.ts # Core validation engine
ā āāā XsdReference.ts # Main API interface
ā āāā XsdDetector.ts # Schema auto-detection
āāā dist/ # Compiled JavaScript output
āāā tests/ # Comprehensive test suite
ā āāā data/ # Test data and schemas
ā ā āāā xsd/ # XSD schema definitions
ā ā āāā aiscripts/ # AI script test files
ā ā āāā md/ # Mission director test files
ā āāā test_all_files_comprehensive.js
āāā package.json # Project configuration
āāā tsconfig.json # TypeScript configuration
```
## š License
This project is licensed under the MIT License - see the LICENSE file for details.
## š Acknowledgments
- **X4 Foundations** by Egosoft for the complex and rich modding system
- **XML Schema (XSD)** specification for comprehensive validation standards
- **TypeScript** and **Node.js** ecosystem for excellent development tools
### Performance Tips
- Reuse `XsdReference` instances when validating multiple files with the same schemas
- Call `dispose()` only when completely finished with validation tasks
- Use static methods (`XsdReference.validateAttributeNames`, etc.) for batch operations with pre-fetched attribute info