# Extending xml-xsd-engine

> How to extend the library with custom types, entity resolvers, SAX hooks, streaming hooks, and plugins.
> **v1.7.0**

---

## Custom Type Validators

Register a validator for a custom XSD simple type:

```ts
import { PluginRegistry } from 'xml-xsd-engine';

PluginRegistry.registerTypeValidator('http://myns:EmailType', (value) => {
  if (!value.includes('@')) {
    return { valid: false, message: `'${value}' is not a valid email` };
  }
  return { valid: true };
});
```

The type name must match the `{namespaceURI}localName` form used in the schema model.

---

## Custom Entity Resolvers

```ts
PluginRegistry.registerEntityResolver((entityName) => {
  const entities: Record<string, string> = {
    myapp: 'My Application Name',
    version: '1.7.0',
  };
  return entities[entityName] ?? null; // return null to let built-in handling proceed
});
```

---

## SAX Event Hooks

```ts
import { SaxInstrumentation } from 'xml-xsd-engine';

const instr = new SaxInstrumentation();

instr.onElementStart = ({ path, localName, namespaceURI, depth, line, col }) => {
  console.log(`OPEN  ${path} (${namespaceURI}) @${line}:${col}`);
};

instr.onElementEnd = ({ path, localName, depth }) => {
  console.log(`CLOSE ${path}`);
};

instr.onText = ({ path, value }) => {
  console.log(`TEXT  ${path}: ${value.trim()}`);
};

instr.parse(xmlSource);
```

---

## Consuming PSVI in Plugins

```ts
import { validate, extractPsvi } from 'xml-xsd-engine';

function myValidationHook(doc, schema) {
  const result = validate(doc, schema, { psvi: true });

  if (result.psvi) {
    const annotated = extractPsvi(result.psvi, doc);
    for (const [path, ann] of annotated) {
      if (ann.validity === 'invalid') {
        console.warn(`[myPlugin] ${path} failed type check as ${ann.xsdType}`);
      }
    }
  }

  return result;
}
```

---

## Hooking into Streaming Validation *(v1.7)*

Use `validateStreamingGenerator` to receive issues as a stream and react in real-time:

```ts
import { validateStreamingGenerator, compileSchema, parseXsd } from 'xml-xsd-engine';

const compiled = compileSchema(parseXsd(xsdSource));

async function validateWithProgress(xmlSource: string) {
  let errorCount = 0;

  for await (const issue of validateStreamingGenerator(xmlSource, compiled)) {
    if (issue.severity === 'error') {
      errorCount++;
      console.error(`[${issue.path}] ${issue.message}`);

      // Abort early if too many errors
      if (errorCount >= 10) {
        console.warn('Too many errors — aborting');
        break;
      }
    }
  }
}
```

---

## Custom Streaming Keyref Tracker *(v1.7)*

Use `StreamingKeyrefTracker` directly when building your own SAX pipeline:

```ts
import { StreamingKeyrefTracker, SaxParser } from 'xml-xsd-engine';

const tracker = new StreamingKeyrefTracker(schema);
const parser = new SaxParser(xmlSource, {});

parser.on('endElement', (ev) => {
  const e = ev.event;
  tracker.onElement(
    e.localName,
    buildPath(e),
    e.attributes,
    accumulatedText,
  );
});

// After parse
const issues = tracker.validate();
issues.forEach(i => console.error(i.message));
```

---

## Extending the DFA Engine *(v1.7)*

Build custom content-model DFAs for test fixtures, schema editors, or documentation tools:

```ts
import { buildDfa, runDfa } from 'xml-xsd-engine';
import type { ParticleDefinition } from 'xml-xsd-engine';

const particles: ParticleDefinition[] = [
  { particleKind: 'element', occurrence: { min: 1, max: 1 },
    elementDecl: { name: 'title', /* ... */ } },
  { particleKind: 'element', occurrence: { min: 0, max: 'unbounded' },
    elementDecl: { name: 'author', /* ... */ } },
];

const dfa = buildDfa(particles, 'sequence');
const result = runDfa(dfa, ['title', 'author', 'author']);
console.log(result.valid);     // true
console.log(result.missing);   // []
```

`DfaModel.particleIndex` is populated automatically — O(1) lookup for all element names.

---

## Custom XPath Functions (Advanced)

XPath 2.0 functions can be added to the function registry:

```ts
import { XPath2Functions } from 'xml-xsd-engine';

// Register a custom function (experimental API — may change in v1.9)
XPath2Functions.register('my-ns:format-date', (args, context) => {
  const dateStr = args[0] as string;
  return new Date(dateStr).toLocaleDateString();
});
```

---

## XPath Cache Tuning for Plugins

If your plugin executes many XPath queries, tune the cache to match your workload:

```ts
import { configureXPathCache } from 'xml-xsd-engine';

// Larger cold cache for plugins with many unique XPath expressions
configureXPathCache({ coldMax: 2048, hotMax: 512 });
```

---

## Schema Loader Plugins

Implement custom resolution of `xs:import` and `xs:include`:

```ts
import { parseXsd, SchemaLoader } from 'xml-xsd-engine';

// HTTP loader (synchronous, for pre-cached schemas)
const httpLoader: SchemaLoader = (location, namespace) => {
  return schemaCache.get(location) ?? null;
};

const schema = parseXsd(xsdSource, httpLoader);
```

For async resolution:

```ts
import { parseXsdAsync, AsyncSchemaLoader } from 'xml-xsd-engine';

const asyncLoader: AsyncSchemaLoader = async (location, namespace) => {
  const res = await fetch(location);
  if (!res.ok) return null;
  return res.text();
};

const schema = await parseXsdAsync(xsdSource, asyncLoader);
```

---

## Output Formatter Plugins

Create a custom output format for `ValidationResult`:

```ts
import { ValidationResult } from 'xml-xsd-engine';

function myFormatter(result: ValidationResult, filePath: string): string {
  if (result.valid) return `OK: ${filePath}`;
  return result.issues
    .map(i => `${filePath}:${i.line}:${i.col} [${i.code}] ${i.message}`)
    .join('\n');
}
```

---

## Streaming Result Formatter *(v1.7)*

Create a custom formatter for `StreamingValidationResult`:

```ts
import { StreamingValidationResult, StreamingIssue } from 'xml-xsd-engine';

function streamingFormatter(result: StreamingValidationResult, filePath: string): string {
  if (result.valid) return `OK: ${filePath} (${result.durationMs}ms, SAX)`;
  return [
    `FAIL: ${filePath} (${result.errorCount} errors, ${result.durationMs}ms)`,
    ...result.issues
      .filter(i => i.severity === 'error')
      .map(i => `  ${i.path}:${i.line ?? '?'}:${i.col ?? '?'} ${i.message}`),
  ].join('\n');
}
```

---

## Polymorphic Validators with IValidator *(v1.7)*

`IValidator` is the shared interface implemented by both `ValidationEngine` (DOM) and `StreamingValidator` (SAX). Use it for dependency injection or to write code that works with either validator:

```ts
import { IValidator, ValidationEngine, StreamingValidator,
         compileSchema, parseXsd } from 'xml-xsd-engine';

function validateWithAny(validator: IValidator, xml: string, schema: SchemaModel) {
  return validator.validate(xml, schema, { recover: true });
}

const compiled = compileSchema(parseXsd(xsdSource));
// Swap between DOM and streaming transparently:
validateWithAny(new ValidationEngine(compiled), xmlString, compiled.schema);
validateWithAny(new StreamingValidator(compiled), xmlString, compiled.schema);
```

---

## Custom DFA-based Validators with flattenGroupParticles *(v1.7)*

If you build a custom content-model validator, use `flattenGroupParticles` to pre-process particles before passing them to `buildDfa`. This eliminates synthetic `__group_sequence` wrappers from `xs:group ref` particles:

```ts
import { flattenGroupParticles, buildDfa, runDfa } from 'xml-xsd-engine';

// particles may contain xs:group ref wrappers — flatten first
const flat = flattenGroupParticles(particles);
const dfa = buildDfa(flat, 'sequence');
const { valid, missing, unexpected } = runDfa(dfa, observedElementNames);
```

