From a5337af3cf25569dbb2b059eb9724c8f7ca90c05 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 02:00:00 -0500 Subject: [PATCH 01/10] feat(cli): unify generation options and add target-aware completion Unify backend, style, and feature options across CLI and manifests, retain deprecated aliases, and validate target compatibility before generation. Add shell completion, startup benchmarks, documentation, and regression coverage. Refs #50 --- .../skills/polyxml-codegen-workflow/SKILL.md | 31 + README.md | 20 +- benchmarks/README.md | 2 + benchmarks/cli/.gitignore | 1 + benchmarks/cli/README.md | 32 + benchmarks/cli/benchmark.sh | 17 + crates/polyxml-cli/README.md | 24 +- crates/polyxml-cli/src/completions.rs | 159 +++++ crates/polyxml-cli/src/completions/bash | 33 + crates/polyxml-cli/src/completions/fish | 22 + crates/polyxml-cli/src/completions/zsh | 38 ++ crates/polyxml-cli/src/config.rs | 5 + crates/polyxml-cli/src/main.rs | 268 ++++---- crates/polyxml-cli/src/options.rs | 249 ++++++++ crates/polyxml-cli/tests/test_cli.rs | 578 ++++++++++++++++++ docs/guides/compiler.md | 109 +++- 16 files changed, 1396 insertions(+), 192 deletions(-) create mode 100644 benchmarks/cli/.gitignore create mode 100644 benchmarks/cli/README.md create mode 100755 benchmarks/cli/benchmark.sh create mode 100644 crates/polyxml-cli/src/completions.rs create mode 100644 crates/polyxml-cli/src/completions/bash create mode 100644 crates/polyxml-cli/src/completions/fish create mode 100644 crates/polyxml-cli/src/completions/zsh create mode 100644 crates/polyxml-cli/src/options.rs diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index f1e2cd7a..8e5eea30 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -225,3 +225,34 @@ derived types inherit their base's patterns at parse time): - Field-level facets (`FieldDef.facets`) are never populated by the parser today, so `emit_struct`'s field path needs no `AfterValidator` wiring; if that ever changes, mirror the type-level handling there. + +## 9. Unified CLI Options (issue #50) + +- `polyxml-cli/src/options.rs` normalizes and validates backend/style/features + for direct generation and both manifest forms. Validate all targets before + schema parsing, dry-run success, or output creation. Core `from_str_loose` + parsers can still return `None`; the CLI must reject invalid values before + emitter defaults are applied. +- New opt-ins belong in repeatable `--feature` and manifest `features` arrays. + Legacy booleans remain hidden with warnings; reject contradictions rather + than silently overriding an explicit false. Keep CLI and manifest parity. +- Defaults remain unchanged, including C# record classes, Rust zero-copy, and + Python slots/kw-only. Expose only implemented styles/features. The issue's + future examples (phf, aot, Python plain class, C# mutable struct) are not yet + generator capabilities. +- Shared CLI options apply to every `--lang`, not just the preceding one. Use + per-target manifest entries for heterogeneous configurations. Schema-less + `generate` must not silently ignore generation overrides. + +- Shell completion adapters live under `polyxml-cli/src/completions/`. Their + hidden `__complete` helper filters candidates through the same option resolver + used for generation; preserve multi-target intersection and language aliases. + Bash is exercised in CLI integration tests. Zsh/Fish tests run when those + shells are available on PATH, and skip otherwise. +- `benchmarks/cli/benchmark.sh` uses hyperfine with `--shell=none` to avoid shell + calibration error for sub-5ms startup measurements. Results are fresh processes + with warm OS caches, not machine-reboot or cold-disk startup measurements. + +- Color diagnostics respect `NO_COLOR`. When checking terminal color in a PTY, + unset `NO_COLOR` in the test subprocess and use a color-capable `TERM`; test + the opt-out separately. This development environment sets `NO_COLOR=1`. diff --git a/README.md b/README.md index c9b700ea..6313ae42 100644 --- a/README.md +++ b/README.md @@ -64,17 +64,15 @@ curl -fsSL https://raw.githubusercontent.com/nth-bailey/PolyXML/main/scripts/ins Generate strongly-typed code for all 7 languages from any W3C XML Schema in a single command: ```bash -# 1. Multi-target compilation in a single invocation -polyxml generate \ - --lang python --backend pydantic-v2 \ - --lang rust --zero-copy --codecs \ - --lang csharp --namespace Enterprise.Banking \ - --lang java --backend jackson --package com.enterprise.banking \ - --lang typescript --zod \ - --lang go --package payments \ - --lang cpp --mode modules --backend glaze \ - --out ./generated \ - schemas/pain.001.001.09.xsd +# 1. Generate all targets with their defaults +polyxml generate schemas/pain.001.001.09.xsd \ + --lang python --lang rust --lang csharp --lang java \ + --lang typescript --lang go --lang cpp --out ./generated + +# Target-specific backend, style, and enhancements +polyxml generate schemas/pain.001.001.09.xsd --lang java \ + --backend jackson --style pojo --feature builder --feature direct-codec \ + --package com.enterprise.banking --out ./generated/java # 2. Or build an entire enterprise project declaratively polyxml build --config polyxml.toml diff --git a/benchmarks/README.md b/benchmarks/README.md index 344bbaa9..b9d2da29 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -11,6 +11,7 @@ benchmarks/ ├── README.md # This index: catalog, methodology, conventions ├── run_all.sh # Orchestrator: builds the extension, runs Rust + Python suites ├── __init__.py # Package marker for `python -m benchmarks.python` +├── cli/ # CLI startup and argument validation (hyperfine) ├── python/ # Python bindings vs the Python XML ecosystem (custom CLI suite) └── java/ # Java bindings vs JAXB, Jackson, and the Panama native binding (JMH) ``` @@ -25,6 +26,7 @@ equivalent) and never at this root. | :--- | :--- | :--- | :--- | | Rust core engine | [`crates/polyxml-core/benches/`](../crates/polyxml-core/benches/) | [Criterion.rs](https://github.com/bheisler/criterion.rs) | `cargo bench --bench core_benchmarks` | | Python comparative | [`python/`](python/README.md) | Custom CLI suite | `python -m benchmarks.python` or `./benchmarks/run_all.sh` | +| CLI startup | [`cli/`](cli/README.md) | hyperfine | `./benchmarks/cli/benchmark.sh` | | Java four-runtime | [`java/`](java/README.md) | JMH (Maven) | See [`java/README.md`](java/README.md) | The Rust Criterion suite lives inside its crate because `cargo bench` requires diff --git a/benchmarks/cli/.gitignore b/benchmarks/cli/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/benchmarks/cli/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/benchmarks/cli/README.md b/benchmarks/cli/README.md new file mode 100644 index 00000000..f85e8de3 --- /dev/null +++ b/benchmarks/cli/README.md @@ -0,0 +1,32 @@ +# CLI startup and argument validation + +Run from the repository root with Rust and `hyperfine` installed: + +```bash +./benchmarks/cli/benchmark.sh +``` + +The runner builds the release CLI, creates an empty temporary XSD, and measures +100 fresh processes per command after five warmups. `--shell=none` excludes +shell startup and avoids shell-calibration noise for sub-5ms commands. JSON +results are saved to `benchmarks/cli/target/cli.json` (gitignored). + +| Case | Work measured | +| --- | --- | +| `help` | Process startup and top-level help rendering | +| `generate-help` | Process startup and generation help rendering | +| `parse-and-validate` | Argument parsing, Java backend/style/features validation, and parsing an empty XSD in dry-run mode | + +A local Linux x86-64 release build measured on 2026-09-22 with hyperfine 1.19.0: + +| Case | Mean ± standard deviation | +| --- | --- | +| `help` | 1.4 ± 0.2 ms | +| `generate-help` | 1.5 ± 0.2 ms | +| `parse-and-validate` | 1.6 ± 0.2 ms | + +These measurements meet issue #50's 5ms target on this host. They use warm OS +caches: they do **not** establish cold-disk or post-reboot startup latency. +The parsing case includes tiny-schema I/O and parsing, so it is not an isolated +argument-parser microbenchmark. Results depend on hardware and system load; +rerun the script to compare changes on the same host. diff --git a/benchmarks/cli/benchmark.sh b/benchmarks/cli/benchmark.sh new file mode 100755 index 00000000..c890700c --- /dev/null +++ b/benchmarks/cli/benchmark.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Measure fresh-process CLI startup and argument validation with hyperfine. +set -euo pipefail +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +cd "$repo_root" +command -v hyperfine >/dev/null || { echo 'Install hyperfine to run this benchmark.' >&2; exit 1; } +cargo build --release -p polyxml-cli +scratch_dir=$(mktemp -d) +trap 'rm -rf "$scratch_dir"' EXIT +export POLYXML_CLI_BENCH_SCHEMA="$scratch_dir/minimal.xsd" +printf '%s\n' '' > "$POLYXML_CLI_BENCH_SCHEMA" +mkdir -p benchmarks/cli/target +hyperfine --shell=none --warmup 5 --runs 100 \ + --export-json benchmarks/cli/target/cli.json \ + --command-name help 'target/release/polyxml --help' \ + --command-name generate-help 'target/release/polyxml generate --help' \ + --command-name parse-and-validate "target/release/polyxml generate \"$POLYXML_CLI_BENCH_SCHEMA\" --lang java --backend jackson --style pojo --feature builder --feature direct-codec --dry-run" diff --git a/crates/polyxml-cli/README.md b/crates/polyxml-cli/README.md index 879bc117..cebad7e3 100644 --- a/crates/polyxml-cli/README.md +++ b/crates/polyxml-cli/README.md @@ -49,12 +49,12 @@ cargo install polyxml-cli | Target | Flag (`--lang`) | Generated Artifacts & Features | | :--- | :--- | :--- | | **Python** | `python` | Modern Python 3.12+ `@dataclass` or Pydantic v2 models, field constraints, zero-copy streaming codecs | -| **Rust** | `rust` | Zero-copy `Cow<'a, str>` & owned structs, automatic recursive boxing (`Box`), streaming serializers/deserializers, rkyv wire format (`--rkyv`) | +| **Rust** | `rust` | Zero-copy `Cow<'a, str>` & owned structs, automatic recursive boxing (`Box`), streaming serializers/deserializers, rkyv wire format (`--feature rkyv`) | | **C++** | `cpp` | Modern C++20/C++23 value types, C++20 Modules (`--mode modules`), Glaze reflection (`--backend glaze`), CMake/Meson export | -| **Java** | `java` | Java 21+ records or mutable JavaBeans (`--style pojo`), fluent builders (`--builder`), direct StAX codecs (`--codec direct`), Jackson XML/JSON annotations (`--backend jackson`) | +| **Java** | `java` | Java 21+ records or mutable JavaBeans (`--style pojo`), fluent builders (`--feature builder`), direct StAX codecs (`--feature direct-codec`), Jackson XML/JSON annotations (`--backend jackson`) | | **TypeScript** | `typescript` | TypeScript 5+ interfaces, discriminated unions, runtime validation schemas via Zod, Valibot, or TypeBox (`--backend`) | | **Go** | `go` | Idiomatic Go 1.22+ structs with `encoding/xml` tags, reflectionless EasyJSON (`--backend easyjson`) & ByteDance Sonic JIT (`--backend sonic`) | -| **C#** | `csharp` | Modern C# 12 / .NET 8+ mutable classes (`--style class`), records and record structs (`--record-kind struct`), compile-time Native AOT source generation (`--source-gen`) | +| **C#** | `csharp` | Modern C# 12 / .NET 8+ mutable classes (`--style class`), records and record structs (`--style record-struct`), compile-time Native AOT source generation (`--backend source-gen`) | --- @@ -78,7 +78,7 @@ polyxml generate --lang java --backend jackson --package com.enterprise.banking polyxml generate --lang cpp --mode modules --backend glaze --package enterprise::crm --out ./generated/cpp schemas/order.xsd # Generate zero-copy Rust models with codecs and rkyv wire format -polyxml generate --lang rust --zero-copy --codecs --rkyv --out ./generated/rust schemas/order.xsd +polyxml generate --lang rust --feature zero-copy --codecs --feature rkyv --out ./generated/rust schemas/order.xsd # Generate TypeScript with tree-shakeable Valibot schemas polyxml generate --lang ts --backend valibot --out ./generated/ts schemas/order.xsd @@ -87,7 +87,7 @@ polyxml generate --lang ts --backend valibot --out ./generated/ts schemas/order. polyxml generate --lang go --backend sonic --package crm --out ./generated/go schemas/order.xsd # Generate C# record structs with Native AOT source generation -polyxml generate --lang csharp --record-kind struct --source-gen --namespace Enterprise.Crm --out ./generated/csharp schemas/order.xsd +polyxml generate --lang csharp --style record-struct --backend source-gen --namespace Enterprise.Crm --out ./generated/csharp schemas/order.xsd # Multi-target compilation in a single invocation polyxml generate \ @@ -141,9 +141,8 @@ codecs = true [[generate]] target = "rust" output = "src/generated/rust" -zero_copy = true +features = ["zero-copy", "rkyv"] codecs = true -rkyv = true [[generate]] target = "java" @@ -172,8 +171,8 @@ backend = "sonic" target = "csharp" output = "src/generated/csharp" namespace = "Enterprise.Banking.Iso20022" -source_gen = true -record_kind = "struct" +backend = "source-gen" +style = "record-struct" ``` --- @@ -185,3 +184,10 @@ Explore full-scale repositories using PolyXML CLI manifests across all 7 languag - **[💳 FinTech & Banking](https://github.com/nth-bailey/polyxml-finance-examples)**: Instant Payments ↔ ISO 20022 pacs.008 XML - **[🚍 Smart Cities & Transit](https://github.com/nth-bailey/polyxml-transit-examples)**: Google GTFS-Realtime ↔ CEN SIRI v2.0 & NeTEx XML + +## Shell completion + +Run `polyxml completions bash`, `polyxml completions zsh`, or +`polyxml completions fish` to generate a shell script. Backend, style, and feature +suggestions are filtered by the selected language(s). See the +[compiler guide](../../docs/guides/compiler.md#shell-completion) for installation. diff --git a/crates/polyxml-cli/src/completions.rs b/crates/polyxml-cli/src/completions.rs new file mode 100644 index 00000000..fe805777 --- /dev/null +++ b/crates/polyxml-cli/src/completions.rs @@ -0,0 +1,159 @@ +//! Shell adapters use the validator itself to keep suggestions target-aware. +use super::{Cli, TargetEmitOptions}; +use clap::{CommandFactory, ValueEnum}; + +#[derive(Debug, Clone, Copy, ValueEnum)] +pub enum Shell { + Bash, + Zsh, + Fish, +} + +pub fn print_script(shell: Shell) { + print!( + "{}", + match shell { + Shell::Bash => include_str!("completions/bash"), + Shell::Zsh => include_str!("completions/zsh"), + Shell::Fish => include_str!("completions/fish"), + } + ); +} + +/// Tokens exclude the executable and the current incomplete word. Ignore +/// unrelated options, but retain every selected language and backend. +pub fn candidates(kind: &str, words: &[String]) -> Vec { + let mut languages = Vec::new(); + let mut backend = None; + let mut iter = words.iter(); + while let Some(word) = iter.next() { + match word.as_str() { + "--" => break, + "--lang" | "-l" => { + if let Some(value) = iter.next() { + let value = if value == "=" { + iter.next() + } else { + Some(value) + }; + if let Some(value) = value { + languages.push(value.as_str()); + } + } + } + "--backend" | "-b" => { + backend = iter + .next() + .and_then(|value| { + if value == "=" { + iter.next() + } else { + Some(value) + } + }) + .map(String::as_str); + } + _ => { + if let Some(value) = word + .strip_prefix("--lang=") + .or_else(|| word.strip_prefix("-l").filter(|v| !v.is_empty())) + { + languages.push(value); + } else if let Some(value) = word + .strip_prefix("--backend=") + .or_else(|| word.strip_prefix("-b").filter(|v| !v.is_empty())) + { + backend = Some(value); + } + } + } + } + if languages.is_empty() { + languages.push("python"); + } + let choices: &[&str] = match kind { + "lang" => &[ + "python", + "rust", + "cpp", + "java", + "typescript", + "go", + "csharp", + ], + "backend" => &[ + "dataclass", + "pydantic", + "interfaces", + "zod", + "valibot", + "typebox", + "standard", + "jackson", + "source-gen", + "glaze", + "easyjson", + "sonic", + ], + "style" => &[ + "dataclass", + "record", + "pojo", + "class", + "record-class", + "record-struct", + ], + "feature" => &[ + "zero-copy", + "rkyv", + "builder", + "direct-codec", + "slots", + "kw-only", + ], + "command" => { + return Cli::command() + .get_subcommands() + .filter(|c| !c.is_hide_set()) + .map(|c| c.get_name().to_string()) + .collect() + } + "flag" => { + let command = Cli::command(); + let Some(subcommand) = words.first().and_then(|word| command.find_subcommand(word)) + else { + return vec!["--help".into(), "--version".into()]; + }; + return subcommand + .get_arguments() + .filter(|a| !a.is_hide_set()) + .filter_map(|a| a.get_long().map(|name| format!("--{name}"))) + .chain(["--help".into(), "--version".into()]) + .collect(); + } + _ => &[], + }; + choices + .iter() + .filter(|value| { + if kind == "lang" { + return true; + } + let features = vec![value.to_string()]; + let opts = TargetEmitOptions { + backend: if kind == "backend" { + Some(**value) + } else { + backend + }, + style: (kind == "style").then_some(**value), + features: if kind == "feature" { &features } else { &[] }, + ..Default::default() + }; + languages + .iter() + .all(|lang| opts.resolve(lang, false).is_ok()) + }) + .map(|value| value.to_string()) + .collect() +} diff --git a/crates/polyxml-cli/src/completions/bash b/crates/polyxml-cli/src/completions/bash new file mode 100644 index 00000000..a8778791 --- /dev/null +++ b/crates/polyxml-cli/src/completions/bash @@ -0,0 +1,33 @@ +# Bash completion for PolyXML. Install with: source <(polyxml completions bash) +_polyxml() { + local cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" + local kind=flag prefix= candidates + local -a words=("${COMP_WORDS[@]:1:COMP_CWORD-1}") + COMPREPLY=() + if [[ "$prev" == = && "$COMP_CWORD" -ge 2 ]]; then prev="${COMP_WORDS[COMP_CWORD-2]}"; fi + if [[ "$cur" == = ]]; then cur=; fi + case "$prev" in + --lang|-l) kind=lang ;; + --backend|-b) kind=backend ;; + --style) kind=style ;; + --feature) kind=feature ;; + --out|--out-dir|-o|--config|-c|--schema|-s) return ;; + esac + case "$cur" in + --lang=*|--backend=*|--style=*|--feature=*) + kind="${cur%%=*}"; kind="${kind#--}" + prefix="${cur%%=*}="; cur="${cur#*=}" ;; + esac + if [[ "$kind" == feature && "$cur" == *,* ]]; then + prefix+="${cur%,*},"; cur="${cur##*,}" + fi + if (( COMP_CWORD == 1 )); then kind=command; fi + if [[ "$kind" == flag && "$cur" != -* ]]; then return; fi + candidates=$("${COMP_WORDS[0]}" __complete "$kind" -- "${words[@]}" 2>/dev/null) + local value + while IFS= read -r value; do + [[ -n "$value" && "$value" == "$cur"* ]] && COMPREPLY+=("$prefix$value") + done <<< "$candidates" + return 0 +} +complete -o default -F _polyxml polyxml diff --git a/crates/polyxml-cli/src/completions/fish b/crates/polyxml-cli/src/completions/fish new file mode 100644 index 00000000..798643d6 --- /dev/null +++ b/crates/polyxml-cli/src/completions/fish @@ -0,0 +1,22 @@ +# Fish completion for PolyXML. Save to ~/.config/fish/completions/polyxml.fish +function __polyxml_values + set -l words (commandline -opc) + command $words[1] __complete $argv[1] -- $words[2..-1] 2>/dev/null +end +complete -c polyxml -n 'not __fish_seen_subcommand_from generate build validate transcode completions' -f -a 'generate build validate transcode completions' +complete -c polyxml -n '__fish_seen_subcommand_from generate' -s l -l lang -r -f -a '(__polyxml_values lang)' +complete -c polyxml -n '__fish_seen_subcommand_from generate' -s b -l backend -r -f -a '(__polyxml_values backend)' +complete -c polyxml -n '__fish_seen_subcommand_from generate' -l style -r -f -a '(__polyxml_values style)' +complete -c polyxml -n '__fish_seen_subcommand_from generate' -l feature -r -f -a '(__polyxml_values feature)' +complete -c polyxml -n '__fish_seen_subcommand_from generate build' -l dry-run +complete -c polyxml -n '__fish_seen_subcommand_from generate build' -l format +complete -c polyxml -n '__fish_seen_subcommand_from generate build' -s c -l config -r +complete -c polyxml -n '__fish_seen_subcommand_from generate transcode' -s o -l out -r +complete -c polyxml -n '__fish_seen_subcommand_from generate' -s p -l package -r -f +complete -c polyxml -n '__fish_seen_subcommand_from generate' -s m -l mode -r -f -a 'header modules' +complete -c polyxml -n '__fish_seen_subcommand_from generate' -l strict-facets +complete -c polyxml -n '__fish_seen_subcommand_from generate' -l codecs -f -a 'true false' +complete -c polyxml -n '__fish_seen_subcommand_from generate' -l custom-header -r -f +complete -c polyxml -n '__fish_seen_subcommand_from completions' -f -a 'bash zsh fish' +complete -c polyxml -s h -l help +complete -c polyxml -s V -l version diff --git a/crates/polyxml-cli/src/completions/zsh b/crates/polyxml-cli/src/completions/zsh new file mode 100644 index 00000000..3cec7847 --- /dev/null +++ b/crates/polyxml-cli/src/completions/zsh @@ -0,0 +1,38 @@ +#compdef polyxml +# Save as _polyxml in a directory on $fpath before running compinit. +_polyxml_values() { + local kind=$1 + local -a values + values=("${(@f)$("${words[1]}" __complete "$kind" -- "${words[@]:1:$((CURRENT-2))}" 2>/dev/null)}") + compadd -a values +} +_polyxml() { + if (( CURRENT == 2 )); then + _polyxml_values command + return + fi + case "$words[2]" in + generate) + _arguments -s \ + '*'{-l,--lang}'[Target language]:language:_polyxml_values lang' \ + {-b,--backend}'[Serialization backend]:backend:_polyxml_values backend' \ + '--style[Type representation]:style:_polyxml_values style' \ + '*--feature[Opt-in enhancement]:feature:_polyxml_values feature' \ + {-o,--out}'[Output directory]:directory:_files -/' \ + {-c,--config}'[Workspace manifest]:file:_files' \ + {-p,--package}'[Package or namespace]:package:' \ + {-m,--mode}'[C++ packaging mode]:mode:(header modules)' \ + '--codecs[Emit codecs]:boolean:(true false)' \ + '--custom-header[Custom header]:text:' \ + '--strict-facets[Enforce facets]' '--dry-run[Validate without writing]' \ + '--format[Format generated code]' '--help[Show help]' \ + '*:schema:_files' ;; + *) + if [[ "$PREFIX" == -* ]]; then _polyxml_values flag; else _files; fi ;; + esac +} +if [[ "$funcstack[1]" == _polyxml ]]; then + _polyxml "$@" +else + compdef _polyxml polyxml +fi diff --git a/crates/polyxml-cli/src/config.rs b/crates/polyxml-cli/src/config.rs index 888c637e..4dcbf848 100644 --- a/crates/polyxml-cli/src/config.rs +++ b/crates/polyxml-cli/src/config.rs @@ -50,6 +50,8 @@ pub struct TargetConfig { pub output: String, pub enabled: Option, pub backend: Option, + #[serde(default)] + pub features: Vec, pub package: Option, pub namespace: Option, pub strict_facets: Option, @@ -78,6 +80,8 @@ pub struct CodegenTargetConfig { pub enabled: Option, pub output: Option, pub backend: Option, + #[serde(default)] + pub features: Vec, pub package: Option, pub namespace: Option, pub strict_facets: Option, @@ -151,6 +155,7 @@ impl WorkspaceManifest { output, enabled: cfg.enabled, backend: cfg.backend.clone(), + features: cfg.features.clone(), package: cfg.package.clone(), namespace: cfg.namespace.clone(), strict_facets: cfg.strict_facets, diff --git a/crates/polyxml-cli/src/main.rs b/crates/polyxml-cli/src/main.rs index b72d6a7e..069f539a 100644 --- a/crates/polyxml-cli/src/main.rs +++ b/crates/polyxml-cli/src/main.rs @@ -1,10 +1,13 @@ +mod completions; pub mod config; +mod options; +use options::target_options; use std::fs; use std::path::{Path, PathBuf}; use std::process::{self, Command}; -use clap::{Args, Parser, Subcommand}; +use clap::{Args, CommandFactory, Parser, Subcommand}; use config::WorkspaceManifest; use heck::AsPascalCase; use polyxml::codegen::cpp::{CppBackend, CppCodegen, CppMode, CppOptions}; @@ -42,6 +45,19 @@ pub enum Commands { /// Bidirectionally transcode XML ↔ JSON with zero-copy streaming Transcode(TranscodeArgs), + + /// Print a shell completion script with target-aware option suggestions + Completions { + #[arg(value_enum)] + shell: completions::Shell, + }, + + #[command(name = "__complete", hide = true)] + Complete { + kind: String, + #[arg(last = true)] + words: Vec, + }, } #[derive(Debug, Args)] @@ -58,6 +74,10 @@ pub struct GenerateArgs { #[arg(short = 'b', long = "backend", value_name = "BACKEND")] pub backend: Option, + /// Opt-in target enhancement (repeatable; see compiler guide for supported values) + #[arg(long = "feature", value_name = "NAME", value_delimiter = ',')] + pub features: Vec, + /// Package or namespace for generated code (e.g. 'com.example.models' for Java, 'polyxml::models' for C++) #[arg(short = 'p', long = "package", alias = "namespace", value_name = "PKG")] pub package: Option, @@ -67,7 +87,7 @@ pub struct GenerateArgs { pub mode: Option, /// Zero-copy mode for Rust models (borrow Cow<'a, str> instead of owned String) - #[arg(long = "zero-copy", default_missing_value = "true", num_args = 0..=1)] + #[arg(long = "zero-copy", hide = true, default_missing_value = "true", num_args = 0..=1)] pub zero_copy: Option, /// Emit streaming serialization and deserialization codecs (default: true) @@ -75,31 +95,31 @@ pub struct GenerateArgs { pub codecs: Option, /// Emit runtime Zod validation schemas for TypeScript (default: false) - #[arg(long = "zod", default_missing_value = "true", num_args = 0..=1)] + #[arg(long = "zod", hide = true, default_missing_value = "true", num_args = 0..=1)] pub zod: Option, /// C# System.Text.Json compile-time source generation context (default: false) - #[arg(long = "source-gen", default_missing_value = "true", num_args = 0..=1)] + #[arg(long = "source-gen", hide = true, default_missing_value = "true", num_args = 0..=1)] pub source_gen: Option, /// C# record representation ('class' or 'struct', default: 'class') - #[arg(long = "record-kind", value_name = "KIND")] + #[arg(long = "record-kind", hide = true, value_name = "KIND")] pub record_kind: Option, - /// Java/C# model style: record (default), pojo, or class - #[arg(long, value_parser = ["record", "pojo", "class"])] + /// Target model style (e.g. record, pojo, class, record-class, record-struct) + #[arg(long, value_name = "STYLE")] pub style: Option, /// Emit fluent Java builders - #[arg(long, default_missing_value = "true", num_args = 0..=1)] + #[arg(long, hide = true, default_missing_value = "true", num_args = 0..=1)] pub builder: Option, /// Java XML binding: annotation (default) or direct StAX companion codecs - #[arg(long, value_parser = ["annotation", "direct"])] + #[arg(long, hide = true, value_parser = ["annotation", "direct"])] pub codec: Option, /// Rust rkyv zero-copy binary wire format serialization (default: false) - #[arg(long = "rkyv", default_missing_value = "true", num_args = 0..=1)] + #[arg(long = "rkyv", hide = true, default_missing_value = "true", num_args = 0..=1)] pub rkyv: Option, /// Output directory for generated source files @@ -193,9 +213,27 @@ fn main() { Commands::Build(args) => run_build(args), Commands::Validate(args) => run_validate(args), Commands::Transcode(args) => run_transcode(args), + Commands::Completions { shell } => { + completions::print_script(shell); + Ok(()) + } + Commands::Complete { kind, words } => { + for value in completions::candidates(&kind, &words) { + println!("{value}"); + } + Ok(()) + } }; if let Err(err) = result { + if err + .downcast_ref::() + .is_some_and(|error| error.kind() == std::io::ErrorKind::InvalidInput) + { + Cli::command() + .error(clap::error::ErrorKind::InvalidValue, err.to_string()) + .exit(); + } eprintln!("Error: {}", err); process::exit(1); } @@ -204,6 +242,26 @@ fn main() { fn run_generate(args: GenerateArgs) -> Result<(), Box> { // If no schemas are passed directly, check for polyxml.toml config if args.schemas.is_empty() { + if !args.lang.is_empty() + || args.backend.is_some() + || args.style.is_some() + || !args.features.is_empty() + || args.package.is_some() + || args.mode.is_some() + || args.zero_copy.is_some() + || args.codecs.is_some() + || args.zod.is_some() + || args.source_gen.is_some() + || args.record_kind.is_some() + || args.builder.is_some() + || args.codec.is_some() + || args.rkyv.is_some() + || args.out.is_some() + || args.custom_header.is_some() + || args.strict_facets + { + return Err("Generation options require explicit schema paths. For manifest builds, set target options in polyxml.toml.".into()); + } let config_path = args.config.unwrap_or_else(|| PathBuf::from("polyxml.toml")); if config_path.exists() { return run_build(BuildArgs { @@ -218,6 +276,35 @@ fn run_generate(args: GenerateArgs) -> Result<(), Box> { } } + let languages = if args.lang.is_empty() { + vec!["python".to_string()] + } else { + args.lang.clone() + }; + + let emit_opts = TargetEmitOptions { + backend: args.backend.as_deref(), + features: &args.features, + slots: None, + kw_only: None, + package: args.package.as_deref(), + mode: args.mode.as_deref(), + zero_copy: args.zero_copy, + codecs: args.codecs, + zod: args.zod, + source_gen: args.source_gen, + record_kind: args.record_kind.as_deref(), + style: args.style.as_deref(), + builder: args.builder, + codec: args.codec.as_deref(), + rkyv: args.rkyv, + custom_header: args.custom_header.as_deref(), + }; + let resolved_options = languages + .iter() + .map(|lang| emit_opts.resolve(lang, true)) + .collect::>>()?; + let mut parser = XsdParser::new(); let mut compiled_schemas = Vec::new(); @@ -237,15 +324,9 @@ fn run_generate(args: GenerateArgs) -> Result<(), Box> { return Ok(()); } - let languages = if args.lang.is_empty() { - vec!["python".to_string()] - } else { - args.lang - }; - let base_out = args.out.unwrap_or_else(|| PathBuf::from("generated")); - for lang in &languages { + for (lang, emit_opts) in languages.iter().zip(resolved_options) { let lang_out = if languages.len() > 1 { base_out.join(lang) } else { @@ -253,21 +334,6 @@ fn run_generate(args: GenerateArgs) -> Result<(), Box> { }; fs::create_dir_all(&lang_out)?; - let emit_opts = TargetEmitOptions { - backend: args.backend.as_deref(), - package: args.package.as_deref(), - mode: args.mode.as_deref(), - zero_copy: args.zero_copy, - codecs: args.codecs, - zod: args.zod, - source_gen: args.source_gen, - record_kind: args.record_kind.as_deref(), - style: args.style.as_deref(), - builder: args.builder, - codec: args.codec.as_deref(), - rkyv: args.rkyv, - custom_header: args.custom_header.as_deref(), - }; for (schema_path, ir) in &compiled_schemas { emit_target_code(lang, emit_opts, &lang_out, schema_path, ir)?; @@ -291,6 +357,11 @@ fn run_build(args: BuildArgs) -> Result<(), Box> { let manifest = WorkspaceManifest::from_file(&args.config)?; let base_dir = args.config.parent().unwrap_or_else(|| Path::new(".")); + let targets = manifest.resolved_targets(); + for target in &targets { + target_options(target).resolve(&target.target, true)?; + } + let schema_files = manifest.expand_schemas(base_dir)?; if schema_files.is_empty() { println!("No schema files matched workspace schema patterns."); @@ -307,7 +378,6 @@ fn run_build(args: BuildArgs) -> Result<(), Box> { compiled_schemas.push((schema_path.clone(), ir)); } - let targets = manifest.resolved_targets(); if targets.is_empty() { println!("No generation targets configured in manifest."); return Ok(()); @@ -344,29 +414,7 @@ fn run_build(args: BuildArgs) -> Result<(), Box> { target_dir.display() ); - let mode_str = target.mode.as_deref().or_else(|| { - if target.modules.unwrap_or(false) { - Some("modules") - } else { - None - } - }); - - let emit_opts = TargetEmitOptions { - backend: target.backend.as_deref(), - package: target.package.as_deref().or(target.namespace.as_deref()), - mode: mode_str, - zero_copy: target.zero_copy, - codecs: target.codecs, - zod: target.zod, - source_gen: target.source_gen, - record_kind: target.record_kind.as_deref(), - style: target.style.as_deref(), - builder: target.builder, - codec: target.codec.as_deref(), - rkyv: target.rkyv, - custom_header: target.custom_header.as_deref(), - }; + let emit_opts = target_options(target).resolve(&target.target, false)?; for (schema_path, ir) in &compiled_schemas { emit_target_code(&target.target, emit_opts, &target_dir, schema_path, ir)?; @@ -456,6 +504,9 @@ fn report_schema_ir(ir: &SchemaIR) { #[derive(Debug, Default, Clone, Copy)] pub struct TargetEmitOptions<'a> { pub backend: Option<&'a str>, + pub features: &'a [String], + pub slots: Option, + pub kw_only: Option, pub package: Option<&'a str>, pub mode: Option<&'a str>, pub zero_copy: Option, @@ -477,33 +528,9 @@ fn emit_target_code( schema_path: &Path, ir: &SchemaIR, ) -> std::io::Result<()> { - let use_records = match opts.style.unwrap_or("record") { - "record" => true, - "pojo" | "class" => false, - value => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Unknown style: {value}"), - )) - } - }; - let direct_codec = match opts.codec.unwrap_or("annotation") { - "annotation" => false, - "direct" => true, - value => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Unknown codec: {value}"), - )) - } - }; + let use_records = !matches!(opts.style, Some("pojo" | "class" | "dataclass")); + let direct_codec = opts.codec == Some("direct"); let language = lang.to_lowercase(); - if (opts.style.is_some() && !matches!(language.as_str(), "java" | "csharp" | "cs" | "c#")) - || ((opts.builder.unwrap_or(false) || opts.codec.is_some()) && language != "java") - || (!use_records && opts.record_kind.is_some_and(|k| k != "class")) - { - return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "style is Java/C# only; builder and codec are Java only; mutable classes cannot use record structs")); - } match language.as_str() { "python" | "py" => { let py_backend = opts @@ -513,8 +540,8 @@ fn emit_target_code( let options = PythonOptions { backend: py_backend, - slots: true, - kw_only: true, + slots: opts.slots.unwrap_or(true), + kw_only: opts.kw_only.unwrap_or(true), pep695_aliases: true, emit_meta: true, emit_root_aliases: true, @@ -743,76 +770,11 @@ fn emit_target_code( } Ok(()) } - _ => emit_target_placeholder(lang, opts, out_dir, schema_path, ir), - } -} - -fn emit_target_placeholder( - lang: &str, - opts: TargetEmitOptions<'_>, - out_dir: &Path, - schema_path: &Path, - ir: &SchemaIR, -) -> std::io::Result<()> { - let file_stem = schema_path - .file_stem() - .map(|s| s.to_string_lossy()) - .unwrap_or_else(|| "models".into()); - - let (filename, header) = match lang.to_lowercase().as_str() { - "python" | "py" => ( - format!("{}.py", file_stem), - "# @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\nfrom __future__ import annotations\n", - ), - "rust" | "rs" => ( - format!("{}.rs", file_stem), - "// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\nuse polyxml::ir::*;\n", - ), - "cpp" | "c++" => ( - format!("{}.hpp", file_stem), - "// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\n#pragma once\n", - ), - "java" => ( - format!("{}.java", heck::AsPascalCase(file_stem.as_ref())), - "// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\n", - ), - "ts" | "typescript" => ( - format!("{}.ts", file_stem), - "// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\n", - ), - "go" => ( - format!("{}.go", file_stem), - "// Code generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML). DO NOT EDIT.\n// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\npackage models\n", - ), - "csharp" | "c#" | "cs" => ( - format!("{}.cs", heck::AsPascalCase(file_stem.as_ref())), - "// \n// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\nnamespace Generated;\n", - ), - _ => ( - format!("{}.txt", file_stem), - "// @generated by PolyXML Compiler\n", - ), - }; - - let file_path = out_dir.join(filename); - let mut content = String::new(); - if let Some(custom) = opts.custom_header { - let trimmed = custom.trim(); - if !trimmed.is_empty() { - content.push_str(trimmed); - content.push_str("\n\n"); - } + _ => Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Unknown target: {lang}"), + )), } - content.push_str(header); - content.push_str(&format!( - "\n// Schema: {}\n// Target Namespace: {}\n// Total Types: {}\n", - schema_path.display(), - ir.target_namespace.as_deref().unwrap_or("None"), - ir.types.len() - )); - - fs::write(file_path, content)?; - Ok(()) } fn run_language_formatter(lang: &str, dir: &Path) { diff --git a/crates/polyxml-cli/src/options.rs b/crates/polyxml-cli/src/options.rs new file mode 100644 index 00000000..51c24974 --- /dev/null +++ b/crates/polyxml-cli/src/options.rs @@ -0,0 +1,249 @@ +//! Shared validation and normalization for direct generation and both manifest forms. +use super::*; +use std::io::{Error, ErrorKind, Result}; + +fn invalid(message: impl Into) -> Error { + Error::new(ErrorKind::InvalidInput, message.into()) +} + +pub(crate) fn target_options(target: &config::TargetConfig) -> TargetEmitOptions<'_> { + TargetEmitOptions { + backend: target.backend.as_deref(), + features: &target.features, + slots: target.slots, + kw_only: target.kw_only, + package: target.package.as_deref().or(target.namespace.as_deref()), + mode: target + .mode + .as_deref() + .or_else(|| target.modules.filter(|v| *v).map(|_| "modules")), + zero_copy: target.zero_copy, + codecs: target.codecs, + zod: target.zod, + source_gen: target.source_gen, + record_kind: target.record_kind.as_deref(), + style: target.style.as_deref(), + builder: target.builder, + codec: target.codec.as_deref(), + rkyv: target.rkyv, + custom_header: target.custom_header.as_deref(), + } +} + +impl<'a> TargetEmitOptions<'a> { + pub(crate) fn resolve(mut self, lang: &str, warn: bool) -> Result { + let language = lang.to_lowercase(); + let target = match language.as_str() { + "python" | "py" => "python", + "rust" | "rs" => "rust", + "typescript" | "ts" => "typescript", + "c++" | "cpp" => "cpp", + "cs" | "c#" | "csharp" => "csharp", + "java" => "java", + "go" => "go", + _ => return Err(invalid(format!("Unknown target '{lang}'. Supported targets: python, rust, cpp, java, typescript, go, csharp."))), + }; + let supported_backends = match target { + "python" => "dataclass, pydantic", + "typescript" => "interfaces, zod, valibot, typebox", + "java" => "standard, jackson", + "csharp" => "standard, source-gen", + "cpp" => "standard, glaze", + "go" => "standard, easyjson, sonic", + _ => "standard", + }; + if let Some(backend) = self.backend { + let valid = match target { + "python" => PythonBackend::from_str_loose(backend).is_some(), + "typescript" => TypeScriptBackend::from_str_loose(backend).is_some(), + "java" => JavaBackend::from_str_loose(backend).is_some(), + "cpp" => CppBackend::from_str_loose(backend).is_some(), + "go" => GoBackend::from_str_loose(backend).is_some(), + "csharp" => matches!(backend, "standard" | "source-gen"), + _ => backend == "standard", + }; + if !valid { + return Err(invalid(format!("backend '{backend}' is not supported for target '{target}'. Supported backends: {supported_backends}."))); + } + } + + // Reject even explicit false legacy flags on unrelated targets: they are + // almost always mistakes in scripts, and must not silently do nothing. + for (name, present, required, replacement) in [ + ("zod", self.zod.is_some(), "typescript", "--backend zod"), + ( + "source-gen", + self.source_gen.is_some(), + "csharp", + "--backend source-gen", + ), + ( + "zero-copy", + self.zero_copy.is_some(), + "rust", + "--feature zero-copy", + ), + ("rkyv", self.rkyv.is_some(), "rust", "--feature rkyv"), + ( + "builder", + self.builder.is_some(), + "java", + "--feature builder", + ), + ( + "codec", + self.codec.is_some(), + "java", + "--feature direct-codec (or omit for annotations)", + ), + ( + "record-kind", + self.record_kind.is_some(), + "csharp", + "--style record-class or --style record-struct", + ), + ] { + if present { + if target != required { + return Err(invalid(format!("option '{name}' is not supported for target '{target}'; use it with '{required}'."))); + } + if warn { + eprintln!("warning: option '--{name}' is deprecated. Use '{replacement}' instead (manifest: backend/style/features)."); + } + } + } + if self.zod == Some(true) { + if self.backend.is_some_and(|v| { + TypeScriptBackend::from_str_loose(v) != Some(TypeScriptBackend::Zod) + }) { + return Err(invalid( + "--zod conflicts with the selected backend; use --backend zod.", + )); + } + self.backend = Some("zod"); + } else if self.zod == Some(false) + && self.backend.is_some_and(|v| { + TypeScriptBackend::from_str_loose(v) == Some(TypeScriptBackend::Zod) + }) + { + return Err(invalid("--zod false conflicts with --backend zod.")); + } + if target == "csharp" { + if let Some(backend) = self.backend { + let enabled = backend == "source-gen"; + if self.source_gen.is_some_and(|value| value != enabled) { + return Err(invalid("source-gen conflicts with the selected backend.")); + } + self.source_gen = Some(enabled); + } + } + let supported_styles: &[&str] = match target { + "java" => &["record", "pojo", "class"], + "csharp" => &["record", "pojo", "class", "record-class", "record-struct"], + "python" => &["dataclass"], + _ => &[], + }; + if let Some(style) = self.style { + if !supported_styles.contains(&style) { + return Err(invalid(format!( + "style '{style}' is not supported for target '{target}'. Supported styles: {}.", + if supported_styles.is_empty() { + "none".into() + } else { + supported_styles.join(", ") + } + ))); + } + if target == "python" + && self.backend.is_some_and(|v| { + PythonBackend::from_str_loose(v) == Some(PythonBackend::Pydantic) + }) + { + return Err(invalid( + "style 'dataclass' requires the Python dataclass backend.", + )); + } + } + if let Some(kind) = self.record_kind { + if CSharpRecordKind::from_str_loose(kind).is_none() { + return Err(invalid(format!( + "Unknown record-kind '{kind}'; use class or struct." + ))); + } + } + if target == "csharp" { + let kind = match self.style { + Some("record-class" | "class" | "pojo") => Some("class"), + Some("record-struct") => Some("struct"), + _ => None, + }; + if let Some(kind) = kind { + if self.record_kind.is_some_and(|v| { + CSharpRecordKind::from_str_loose(v) != CSharpRecordKind::from_str_loose(kind) + }) { + return Err(invalid("style conflicts with record-kind.")); + } + self.record_kind = Some(kind); + } + } + if self + .codec + .is_some_and(|v| !matches!(v, "annotation" | "direct")) + { + return Err(invalid("Unknown codec; use annotation or direct.")); + } + if let Some(mode) = self.mode { + if target != "cpp" || CppMode::from_str_loose(mode).is_none() { + return Err(invalid(format!("mode '{mode}' is not supported for target '{target}'; C++ supports header or modules."))); + } + } + if target != "python" && (self.slots.is_some() || self.kw_only.is_some()) { + return Err(invalid("slots and kw_only are Python-only options.")); + } + let supported_features: &[&str] = match target { + "rust" => &["zero-copy", "rkyv"], + "java" => &["builder", "direct-codec"], + "python" => &["slots", "kw-only"], + _ => &[], + }; + for feature in self.features { + if !supported_features.contains(&feature.as_str()) { + return Err(invalid(format!("feature '{feature}' is not supported for target '{target}'. Supported features: {}.", if supported_features.is_empty() { "none".into() } else { supported_features.join(", ") }))); + } + let option = match feature.as_str() { + "zero-copy" => &mut self.zero_copy, + "rkyv" => &mut self.rkyv, + "builder" => &mut self.builder, + "slots" => &mut self.slots, + "kw-only" => &mut self.kw_only, + "direct-codec" => { + if self.codec == Some("annotation") { + return Err(invalid( + "feature 'direct-codec' conflicts with codec 'annotation'.", + )); + } + self.codec = Some("direct"); + continue; + } + _ => unreachable!(), + }; + if *option == Some(false) { + return Err(invalid(format!( + "feature '{feature}' conflicts with its explicitly disabled legacy option." + ))); + } + *option = Some(true); + } + if target == "python" + && self + .backend + .is_some_and(|v| PythonBackend::from_str_loose(v) == Some(PythonBackend::Pydantic)) + && (self.slots.is_some() || self.kw_only.is_some()) + { + return Err(invalid( + "slots and kw-only require the Python dataclass backend.", + )); + } + Ok(self) + } +} diff --git a/crates/polyxml-cli/tests/test_cli.rs b/crates/polyxml-cli/tests/test_cli.rs index d0298ab5..4aee46de 100644 --- a/crates/polyxml-cli/tests/test_cli.rs +++ b/crates/polyxml-cli/tests/test_cli.rs @@ -1712,3 +1712,581 @@ codec="direct" assert_eq!(target.builder, Some(true)); assert_eq!(target.codec.as_deref(), Some("direct")); } + +#[test] +fn unified_options_match_legacy_output_and_warn() { + let dir = tempdir().unwrap(); + let schema = dir.path().join("model.xsd"); + fs::write(&schema, r#""#).unwrap(); + for (lang, old, new) in [ + ("ts", vec!["--zod"], vec!["--backend", "zod"]), + ("cs", vec!["--source-gen"], vec!["--backend", "source-gen"]), + ( + "rs", + vec!["--zero-copy", "--rkyv"], + vec!["--feature", "zero-copy", "--feature", "rkyv"], + ), + ( + "java", + vec!["--builder", "--codec", "direct"], + vec!["--feature", "builder,direct-codec"], + ), + ( + "csharp", + vec!["--record-kind", "struct"], + vec!["--style", "record-struct"], + ), + ] { + let legacy = dir.path().join(format!("{lang}-old")); + let unified = dir.path().join(format!("{lang}-new")); + for (flags, out, warning) in [(old, &legacy, true), (new, &unified, false)] { + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .arg("generate") + .arg(&schema) + .args(["--lang", lang]) + .args(flags) + .arg("--out") + .arg(out) + .output() + .unwrap(); + let stderr = String::from_utf8_lossy(&result.stderr); + assert!(result.status.success(), "{lang}: {stderr}"); + assert_eq!(stderr.contains("deprecated"), warning, "{stderr}"); + } + let mut files = 0; + for entry in fs::read_dir(&legacy).unwrap() { + let entry = entry.unwrap(); + assert_eq!( + fs::read(entry.path()).unwrap(), + fs::read(unified.join(entry.file_name())).unwrap(), + "{lang}: {:?}", + entry.file_name() + ); + files += 1; + } + assert!(files > 0); + assert_eq!(files, fs::read_dir(&unified).unwrap().count()); + } +} + +#[test] +fn unified_options_fail_before_schema_io_including_dry_run() { + let dir = tempdir().unwrap(); + let out = dir.path().join("output"); + for (flags, expected) in [ + ( + vec!["--lang", "python", "--backend", "jackson"], + "Supported backends: dataclass, pydantic", + ), + (vec!["--lang", "ts", "--backend", "zodd"], "backend 'zodd'"), + ( + vec!["--lang", "python", "--feature", "rkyv"], + "feature 'rkyv'", + ), + (vec!["--lang", "rust", "--feature", "phf"], "feature 'phf'"), + (vec!["--lang", "python", "--zod"], "option 'zod'"), + ( + vec!["--lang", "ts", "--zod", "--backend", "valibot"], + "conflicts", + ), + ( + vec![ + "--lang", + "rust", + "--zero-copy=false", + "--feature", + "zero-copy", + ], + "conflicts", + ), + ( + vec![ + "--lang", + "java", + "--codec", + "annotation", + "--feature", + "direct-codec", + ], + "conflicts", + ), + ( + vec![ + "--lang", + "cs", + "--source-gen=false", + "--backend", + "source-gen", + ], + "conflicts", + ), + ( + vec![ + "--lang", + "cs", + "--style", + "record-class", + "--record-kind", + "struct", + ], + "conflicts", + ), + ( + vec!["--lang", "python", "--style", "class"], + "Supported styles", + ), + ( + vec!["--lang", "cs", "--style", "struct"], + "Supported styles", + ), + ( + vec!["--lang", "java", "--lang", "python", "--feature", "builder"], + "feature 'builder'", + ), + (vec!["--lang", "typo"], "Unknown target"), + ] { + for dry_run in [false, true] { + let mut cmd = Command::new(env!("CARGO_BIN_EXE_polyxml")); + cmd.args(["generate", "missing.xsd"]) + .args(&flags) + .arg("--out") + .arg(&out); + if dry_run { + cmd.arg("--dry-run"); + } + let result = cmd.output().unwrap(); + let stderr = String::from_utf8_lossy(&result.stderr); + assert!(!result.status.success()); + assert!(stderr.contains(expected), "{flags:?}: {stderr}"); + assert!(!out.exists()); + } + } +} + +#[test] +fn unified_manifest_forms_generate_identical_output_and_validate_early() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("model.xsd"), r#""#).unwrap(); + let manifest = dir.path().join("polyxml.toml"); + fs::write( + &manifest, + r#" +[workspace] +schemas = ["model.xsd"] +[[generate]] +target = "java" +output = "array" +backend = "standard" +style = "pojo" +features = ["builder", "direct-codec"] +[codegen.java] +output = "table" +backend = "standard" +style = "pojo" +features = ["builder", "direct-codec"] +"#, + ) + .unwrap(); + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["build", "--config"]) + .arg(&manifest) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + assert!(!String::from_utf8_lossy(&result.stderr).contains("deprecated")); + for file in ["Item.java", "ItemCodec.java"] { + assert_eq!( + fs::read(dir.path().join("array").join(file)).unwrap(), + fs::read(dir.path().join("table").join(file)).unwrap() + ); + } + let model = fs::read_to_string(dir.path().join("array/Item.java")).unwrap(); + assert!(model.contains("class Item")); + assert!(model.contains("builder()")); + for section in ["[[generate]]\ntarget = 'python'", "[codegen.python]"] { + fs::write(&manifest, format!("[workspace]\nschemas = ['missing.xsd']\n{section}\noutput = 'invalid'\nfeatures = ['builder']\n")).unwrap(); + for command in ["build", "generate"] { + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args([command, "--dry-run", "--config"]) + .arg(&manifest) + .output() + .unwrap(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("feature 'builder'")); + assert!(!dir.path().join("invalid").exists()); + } + } +} + +#[test] +fn unified_help_hides_legacy_flags() { + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["generate", "--help"]) + .output() + .unwrap(); + let help = String::from_utf8_lossy(&result.stdout); + for flag in ["--backend", "--style", "--feature"] { + assert!(help.contains(flag)); + } + for flag in [ + "--zod", + "--source-gen", + "--zero-copy", + "--rkyv", + "--builder", + "--record-kind", + ] { + assert!(!help.contains(flag)); + } +} + +#[test] +fn unified_supported_backends_and_styles_validate() { + let dir = tempdir().unwrap(); + let schema = dir.path().join("model.xsd"); + fs::write( + &schema, + r#""#, + ) + .unwrap(); + for (lang, backends, styles) in [ + ( + "python", + vec!["dataclass", "pydantic", "pydantic-v2"], + vec!["dataclass"], + ), + ("rust", vec!["standard"], vec![]), + ( + "ts", + vec!["interfaces", "zod", "valibot", "typebox", "none"], + vec![], + ), + ( + "java", + vec!["standard", "jackson"], + vec!["record", "pojo", "class"], + ), + ( + "c#", + vec!["standard", "source-gen"], + vec!["record", "record-class", "record-struct", "class", "pojo"], + ), + ("c++", vec!["standard", "glaze"], vec![]), + ("go", vec!["standard", "easyjson", "sonic"], vec![]), + ] { + for (flag, values) in [("--backend", backends), ("--style", styles)] { + for value in values { + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .arg("generate") + .arg(&schema) + .args(["--lang", lang, flag, value, "--dry-run"]) + .output() + .unwrap(); + assert!( + result.status.success(), + "{lang} {flag} {value}: {}", + String::from_utf8_lossy(&result.stderr) + ); + } + } + } +} + +#[test] +fn manifest_generation_does_not_ignore_cli_overrides() { + let dir = tempdir().unwrap(); + fs::write(dir.path().join("polyxml.toml"), "").unwrap(); + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .current_dir(dir.path()) + .args(["generate", "--feature", "builder"]) + .output() + .unwrap(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("set target options in polyxml.toml")); +} + +#[test] +fn python_features_respect_manifest_false_values_and_backend() { + let dir = tempdir().unwrap(); + let schema = dir.path().join("model.xsd"); + fs::write(&schema, r#""#).unwrap(); + let manifest = dir.path().join("polyxml.toml"); + fs::write(&manifest, "[workspace]\nschemas=['model.xsd']\n[codegen.python]\noutput='generated'\nslots=false\nkw_only=false\n").unwrap(); + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["build", "--config"]) + .arg(&manifest) + .output() + .unwrap(); + assert!(result.status.success()); + assert!(fs::read_to_string(dir.path().join("generated/model.py")) + .unwrap() + .contains("@dataclass(slots=False, kw_only=False)")); + for backend in ["dataclass", "pydantic"] { + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .arg("generate") + .arg(&schema) + .args([ + "--lang", + "py", + "--backend", + backend, + "--feature", + "slots", + "--feature", + "kw-only", + ]) + .arg("--out") + .arg(dir.path().join("features")) + .output() + .unwrap(); + assert_eq!(result.status.success(), backend == "dataclass"); + if backend == "dataclass" { + assert!(fs::read_to_string(dir.path().join("features/model.py")) + .unwrap() + .contains("@dataclass(slots=True, kw_only=True)")); + } else { + assert!(String::from_utf8_lossy(&result.stderr) + .contains("require the Python dataclass backend")); + } + } + fs::write( + &manifest, + "[workspace]\nschemas=['model.xsd']\n[codegen.python]\nslots=false\nfeatures=['slots']\n", + ) + .unwrap(); + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["build", "--config"]) + .arg(&manifest) + .output() + .unwrap(); + assert!(!result.status.success()); + assert!(String::from_utf8_lossy(&result.stderr).contains("conflicts")); +} + +#[test] +fn completion_candidates_follow_target_validation() { + for (kind, words, expected) in [ + ("backend", vec!["generate"], vec!["dataclass", "pydantic"]), + ( + "backend", + vec!["generate", "--lang", "ts"], + vec!["interfaces", "zod", "valibot", "typebox", "standard"], + ), + ( + "backend", + vec!["generate", "--lang=java"], + vec!["standard", "jackson"], + ), + ( + "backend", + vec!["generate", "-lcs"], + vec!["standard", "source-gen"], + ), + ( + "backend", + vec!["generate", "-l", "java", "-l", "cpp"], + vec!["standard"], + ), + ( + "backend", + vec!["generate", "-l", "python", "-l", "java"], + vec![], + ), + ("backend", vec!["generate", "--lang", "unknown"], vec![]), + ( + "feature", + vec!["generate", "--lang", "rust"], + vec!["zero-copy", "rkyv"], + ), + ("feature", vec!["generate", "--backend=pydantic"], vec![]), + ( + "style", + vec!["generate", "--lang", "java"], + vec!["record", "pojo", "class"], + ), + ] { + let output = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["__complete", kind, "--"]) + .args(&words) + .output() + .unwrap(); + assert!(output.status.success()); + assert_eq!( + String::from_utf8_lossy(&output.stdout) + .lines() + .collect::>(), + expected, + "{kind}: {words:?}" + ); + } + let output = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["__complete", "flag", "--", "generate"]) + .output() + .unwrap(); + let flags = String::from_utf8_lossy(&output.stdout); + assert!(flags.contains("--feature")); + assert!(!flags.contains("--zod")); +} + +#[test] +fn bash_completion_filters_values_in_the_shell() { + let dir = tempdir().unwrap(); + let script = dir.path().join("polyxml.bash"); + let output = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["completions", "bash"]) + .output() + .unwrap(); + assert!(output.status.success()); + fs::write(&script, output.stdout).unwrap(); + for (words, expected) in [ + ( + vec!["generate", "--lang", "java", "--backend", ""], + vec!["standard", "jackson"], + ), + ( + vec!["generate", "--lang", "=", "ts", "--backend", "=", "v"], + vec!["valibot"], + ), + ( + vec!["generate", "--lang=ts", "--backend", "v"], + vec!["valibot"], + ), + ( + vec!["generate", "--lang", "rust", "--feature", "zero-copy,r"], + vec!["zero-copy,rkyv"], + ), + ( + vec!["generate", "-l", "cs", "--style=record-s"], + vec!["--style=record-struct"], + ), + ( + vec!["generate", "-l", "java", "-l", "go", "--backend", ""], + vec!["standard"], + ), + ] { + let output = Command::new("bash") + .args([ + "--noprofile", + "--norc", + "-c", + r#" +source "$1" +shift +COMP_WORDS=("$@") +COMP_CWORD=$((${#COMP_WORDS[@]} - 1)) +_polyxml +printf '%s\n' "${COMPREPLY[@]}" +"#, + "completion-test", + ]) + .arg(&script) + .arg(env!("CARGO_BIN_EXE_polyxml")) + .args(&words) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&output.stdout) + .lines() + .collect::>(), + expected, + "{words:?}" + ); + } +} + +#[test] +fn zsh_completion_filters_values_when_available() { + if Command::new("zsh").arg("--version").output().is_err() { + return; + } + let dir = tempdir().unwrap(); + let script = dir.path().join("_polyxml"); + let generated = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["completions", "zsh"]) + .output() + .unwrap(); + assert!(generated.status.success()); + fs::write(&script, generated.stdout).unwrap(); + let result = Command::new("zsh") + .args([ + "-f", + "-c", + r#" +fpath=("${1:h}" $fpath) +autoload -Uz _polyxml +compadd() { print -l -- "${values[@]}"; } +_arguments() { _polyxml_values backend; } +words=("$2" generate --lang java --backend '') +CURRENT=6 +_polyxml +"#, + "completion-test", + ]) + .arg(&script) + .arg(env!("CARGO_BIN_EXE_polyxml")) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&result.stdout) + .lines() + .collect::>(), + ["standard", "jackson"] + ); +} + +#[test] +fn fish_completion_filters_values_when_available() { + if Command::new("fish").arg("--version").output().is_err() { + return; + } + let dir = tempdir().unwrap(); + let script = dir.path().join("polyxml.fish"); + let generated = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args(["completions", "fish"]) + .output() + .unwrap(); + assert!(generated.status.success()); + fs::write(&script, generated.stdout).unwrap(); + let binary_dir = std::path::Path::new(env!("CARGO_BIN_EXE_polyxml")) + .parent() + .unwrap(); + let result = Command::new("fish") + .args([ + "--no-config", + "-c", + r#" +source $argv[1] +set -gx PATH $argv[2] $PATH +complete -C 'polyxml generate --lang java --backend ' +"#, + ]) + .arg(&script) + .arg(binary_dir) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + assert_eq!( + String::from_utf8_lossy(&result.stdout) + .lines() + .collect::>(), + ["jackson", "standard"] + ); +} diff --git a/docs/guides/compiler.md b/docs/guides/compiler.md index a905052a..0c138c1b 100644 --- a/docs/guides/compiler.md +++ b/docs/guides/compiler.md @@ -50,7 +50,7 @@ polyxml generate --lang python --out ./generated/python schema.xsd polyxml generate --lang python --backend pydantic-v2 --out ./generated/python schema.xsd # Generate zero-copy Rust models with inherent streaming codecs -polyxml generate --lang rust --zero-copy --codecs --out ./generated/rust schema.xsd +polyxml generate --lang rust --feature zero-copy --codecs --out ./generated/rust schema.xsd # Generate all 7 languages simultaneously polyxml generate \ @@ -69,26 +69,70 @@ polyxml generate \ | Option | Flag | Description | Default | |---|---|---|---| -| **Target Language** | `-l`, `--lang` | Target language (`python`, `rust`, `cpp`, `java`, `typescript`, `go`, `csharp`). Can be specified multiple times. | Required | -| **Output Directory** | `-o`, `--out` | Target directory for generated source files. | `.` | +| **Target Language** | `-l`, `--lang` | Target language (`python`, `rust`, `cpp`, `java`, `typescript`, `go`, `csharp`). Can be specified multiple times. | `python` | +| **Output Directory** | `-o`, `--out` | Target directory for generated source files. | `generated` | +| **Model Style** | `--style` | Target-specific type representation; see the table below. | Existing target default | +| **Enhancements** | `--feature NAME` | Repeatable; also accepts comma-separated names. | None added | | **Target Backend** | `-b`, `--backend` | Target backend (`dataclass`/`pydantic-v2` for Python; `standard`/`jackson` for Java; `standard`/`glaze` for C++; `none`/`zod`/`valibot`/`typebox` for TypeScript; `standard`/`easyjson`/`sonic` for Go). | Target default | | **Compilation Mode** | `-m`, `--mode` | Target packaging mode (`header` or `modules` for C++). | Target default | -| **Rust Zero-Copy** | `--zero-copy` | Use `Cow<'a, str>` string slices instead of owned `String`. | `true` | -| **Rust rkyv** | `--rkyv` | Derive `rkyv::{Archive, Serialize, Deserialize}` for zero-copy wire format serialization. | `false` | | **Streaming Codecs**| `--codecs` | Emit inherent zero-copy streaming XML serializers and deserializers. | `true` | -| **Validation Schemas** | `--zod` | Emit runtime Zod validation schemas for TypeScript (equivalent to `--backend zod`). | `false` | -| **C# Source-Gen** | `--source-gen` | Emit Native AOT compile-time `JsonSerializerContext` for C#. | `false` | -| **C# Record Kind** | `--record-kind` | C# record representation (`class` or `struct`). | `class` | -| **Model Style (Java/C#)** | `--style` | Model representation: `record` (default), `pojo` for JavaBeans (alias `class`), or `class` for mutable C# classes. | `record` | -| **Java Builders** | `--builder` | Emit fluent `Type.builder()...build()` factories for Java models (records and POJOs alike). | `false` | -| **Java Codec** | `--codec` | Java XML binding: `annotation` (default) or `direct`, which adds reflection-free StAX companion `TypeCodec` classes. Rejects wildcards, `xs:anyType`, and runtime `xsi:type` dispatch. | `annotation` | | **Package / Namespace** | `-p`, `--package` | Namespace or package name for Java, Go, C#, or C++. | Target default | | **Custom Header** | `--custom-header` | Custom comment, license, or linter directive text to prepend to generated files. | `None` | -| **Dry Run** | `--dry-run` | Parse and print generated output without writing to disk. | `false` | -| **Format** | `--format` | Automatically format generated code using host toolchains (`ruff`, `cargo fmt`, `clang-format`, `gofmt`). | `true` | +| **Dry Run** | `--dry-run` | Validate options and parse schemas without writing to disk. | `false` | +| **Format** | `--format` | Automatically format generated code using host toolchains (`ruff`, `cargo fmt`, `clang-format`, `gofmt`). | `false` | --- +### Backend, style, and feature compatibility + +Options apply to every language selected in one invocation. Use `polyxml.toml` +when targets need different options. Invalid values, unsupported combinations, +and conflicting legacy options fail before schema parsing or output creation, +including with `--dry-run`. + +| Target | Backends (first is default) | Styles | Features | +| --- | --- | --- | --- | +| Python | `dataclass`, `pydantic` | `dataclass` (dataclass backend only) | `slots`, `kw-only` (dataclass backend only) | +| Rust | `standard` | — | `zero-copy`, `rkyv` | +| TypeScript | `interfaces`, `zod`, `valibot`, `typebox` | — | — | +| Java | `standard`, `jackson` | `record` (default), `pojo` (alias `class`) | `builder`, `direct-codec` | +| C# | `standard`, `source-gen` | `record-class` (default), `record-struct`, `class` (mutable); legacy aliases `record`, `pojo` | — | +| C++ | `standard`, `glaze` | — | — | +| Go | `standard`, `easyjson`, `sonic` | — | — | + +Defaults are unchanged: Rust zero-copy and Python slots/keyword-only fields are +already enabled. Future features such as `phf` and `aot`, Python plain `class`, +and C# mutable `struct` are rejected until their generators support them. + +```bash +polyxml generate schema.xsd --lang rust --feature zero-copy --feature rkyv +polyxml generate schema.xsd --lang java --style pojo --feature builder,direct-codec +polyxml generate schema.xsd --lang csharp --backend source-gen --style record-struct +``` + +The same options work in both `[[generate]]` and `[codegen.]`: + +```toml +[codegen.java] +output = "generated/java" +backend = "jackson" +style = "pojo" +features = ["builder", "direct-codec"] +``` + +Legacy flags remain accepted but are hidden from help and emit a deprecation +warning. Replace `--zod` with `--backend zod`, `--source-gen` with +`--backend source-gen`, `--zero-copy` / `--rkyv` / `--builder` with their +`--feature` equivalents, `--codec direct` with `--feature direct-codec`, and +`--record-kind struct` with `--style record-struct`. Legacy manifest fields are +also retained and warned about. Explicit legacy `false` values are preserved; +combining one with an enabling feature is an error. For owned Rust strings, +`--zero-copy=false` (or `zero_copy = false` in the manifest) remains available. + +`polyxml generate` without schema paths delegates to the manifest. Put target +options in that manifest; command-line generation overrides are rejected rather +than silently ignored. + ### 2. `polyxml build` Declaratively compile complex, multi-schema enterprise projects using a workspace manifest (`polyxml.toml`): @@ -116,9 +160,8 @@ codecs = true [[generate]] target = "rust" output = "src/generated/rust" -zero_copy = true +features = ["zero-copy", "rkyv"] codecs = true -rkyv = true [[generate]] target = "java" @@ -126,8 +169,7 @@ output = "src/generated/java" package = "com.enterprise.banking.iso20022" backend = "jackson" # style = "pojo" # record (default) | pojo (JavaBeans, alias class) -# builder = true # fluent Java builders -# codec = "direct" # reflection-free StAX companion codecs +# features = ["builder", "direct-codec"] [[generate]] target = "typescript" @@ -150,8 +192,8 @@ backend = "sonic" target = "csharp" output = "src/generated/csharp" namespace = "Enterprise.Banking.Iso20022" -source_gen = true -record_kind = "struct" +backend = "source-gen" +style = "record-struct" ``` --- @@ -240,3 +282,32 @@ Explore full-scale enterprise examples demonstrating `polyxml.toml` manifests an Compiles the European **CEN SIRI v2.0** and **NeTEx** schemas and bridges live Google GTFS-Realtime feeds across all 7 languages. + +### Shell completion + +Generate and install a completion script for your shell: + +```bash +# Bash: add this to ~/.bashrc +source <(polyxml completions bash) + +# Zsh: save _polyxml into a directory already on $fpath, then run compinit +polyxml completions zsh > /your/completion/directory/_polyxml + +# Fish +mkdir -p ~/.config/fish/completions +polyxml completions fish > ~/.config/fish/completions/polyxml.fish +``` + +Backend, style, and feature suggestions use the CLI's validation rules and the +selected `--lang` (including aliases). With multiple languages, completion only +suggests values accepted by every selected target. Without `--lang`, it uses the +Python default. Hidden deprecated flags are not suggested. Bash also completes +comma-separated features; repeat `--feature` for portable completion across shells. + +### CLI startup benchmark + +Run `./benchmarks/cli/benchmark.sh` with `hyperfine` installed. It builds the +release CLI and measures help rendering plus argument validation and parsing of +an empty schema. See [the benchmark methodology](../../benchmarks/cli/README.md) +for measurement limits and output location. From 05466ba903f3139162619bf2d76560c8811399e3 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 02:42:00 -0500 Subject: [PATCH 02/10] feat(schema): preserve xs:pattern OR/AND semantics and enforce patterns in all 7 codegens (issue #54) Collapse same-restriction alternatives into one (a)|(b) group in the parser while keeping each derivation step a separate flat entry, so downstream ANDing yields W3C OR-within-restriction, AND-across-steps semantics without changing the IR shape. Add pattern enforcement where it was missing (Rust, Go, C++), and fix anchoring/escaping in Java (search not full-match), C# (unanchored regex, backslash escapes), and TypeScript TypeBox (duplicate pattern keys -> Type.Intersect). Patterned simple types resolve through primitive_base so validators operate on primitives. Add test_pattern_codegen.rs, which executes generated validators in all 7 languages, wired into scripts/test_codegen.sh; document the semantics in the codegen skill. --- .../skills/polyxml-codegen-workflow/SKILL.md | 55 +++ crates/polyxml-core/src/codegen/cpp/mod.rs | 71 +++- crates/polyxml-core/src/codegen/csharp/mod.rs | 10 +- crates/polyxml-core/src/codegen/go/mod.rs | 54 ++- crates/polyxml-core/src/codegen/java/codec.rs | 41 +- crates/polyxml-core/src/codegen/java/mod.rs | 12 +- crates/polyxml-core/src/codegen/mod.rs | 31 ++ crates/polyxml-core/src/codegen/rust/mod.rs | 81 +++- .../src/codegen/typescript/mod.rs | 22 +- crates/polyxml-core/src/ir/mod.rs | 3 + crates/polyxml-core/src/schema_parser/mod.rs | 17 + .../tests/test_pattern_codegen.rs | 360 ++++++++++++++++++ scripts/test_codegen.sh | 3 +- 13 files changed, 727 insertions(+), 33 deletions(-) create mode 100644 crates/polyxml-core/tests/test_pattern_codegen.rs diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 8e5eea30..2ae4feae 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -256,3 +256,58 @@ derived types inherit their base's patterns at parse time): - Color diagnostics respect `NO_COLOR`. When checking terminal color in a PTY, unset `NO_COLOR` in the test subprocess and use a color-capable `TERM`; test the opt-out separately. This development environment sets `NO_COLOR=1`. + +## 10. Pattern OR/AND Semantics & Enforcement (issue #54) + +W3C XSD combines `xs:pattern` two ways: multiple patterns **within one +`` are OR'd**; patterns inherited **across derivation steps are +AND'd**. The IR keeps `RestrictionFacets.patterns: Vec` flat — the +parser encodes each restriction's alternatives as ONE regex group: + +- `schema_parser` collapses `patterns.len() > 1` per restriction into + `(alt1)|(alt2)` (a singleton stays verbatim), then inheritance prepends/appends + each derived step's entry. Downstream code just ANDs the flat list — each + entry is already an OR-group. **Never** AND the pre-collapse alternatives. +- Round-trip tests must serialize `RestrictionFacets`, not the whole + `SchemaIR`: `SchemaIR.types` is a `BTreeMap` and serde_json rejects + struct map keys ("key must be a string"). This is pre-existing and unrelated + to facets. + +Shared helpers in `codegen/mod.rs` (use them; don't re-derive): + +- `primitive_base(ty, ir)` — resolves a simple alias chain to its primitive, + cycle-safe. Patterned simple types must emit their **primitive** base (e.g. + `std::string`, not the alias) so validators/codecs operate on real scalars. +- `patterned_simple(ty, ir)` — the patterned `SimpleTypeDef` behind a field + type, unwrapping `Boxed`/`List`. + +Per-language enforcement (all gated so unpatterned schemas stay byte-identical): + +- **Rust**: free `validate_{Type}_patterns(&str)` fns with `OnceLock` + per pattern; structs gain `validate_patterns()` called from `from_xml`/ + `to_xml`, attributes/text/empty-element paths call the free fn directly. +- **Go**: `Validate() error` on patterned aliases (+ `UnmarshalText`/ + `MarshalText` for string bases so `encoding/xml` enforces on read/write); + struct `Validate` recurses into fields, lists, and pointers. Imports + `regexp`/`fmt` only when patterns exist. +- **C++**: free `validate_{Type}_patterns(std::string_view)` with function-local + `static const std::regex`; struct `validate()` calls it. **Inline field + expressions into the call** — binding `const auto& value = ;` + self-references when the field is named `value` (range-for over a member named + `value` is fine). `#include ` is conditional on `ir`. +- **Java**: `Pattern.compile(p).matcher(v).find()` (search semantics — + `Pattern.matches` anchors and is wrong for XSD); direct codecs resolve + patterned aliases through `primitive_base`. +- **C#**: `Regex.IsMatch` **without** `^...$` anchors (XSD patterns are + unanchored searches); escape `\` before `"` in the pattern string. +- **TypeScript**: one pattern → `pattern:` option (zod/valibot) or single + `Type.String({pattern})`; multiple → `AfterValidator`-equivalent AND via + `_polyxml_patterns` (Python) / `Type.Intersect([...])` (TypeBox — duplicating + the `pattern:` key silently kept only one). Escape `/` inside regex literals. + +Execution tests live in `crates/polyxml-core/tests/test_pattern_codegen.rs` +(wired into `scripts/test_codegen.sh`): they **run** generated Go/C++/Rust/ +Python/Java/C# validators against values matching only one alternative, values +satisfying two restriction steps, and reject lists/optionals carrying a bad +element. The TS leg needs `POLYXML_TS_TEST_MODULES` pointing at a node_modules +with `zod@3 valibot@1 @sinclair/typebox@0.34 typescript@5` and skips otherwise. diff --git a/crates/polyxml-core/src/codegen/cpp/mod.rs b/crates/polyxml-core/src/codegen/cpp/mod.rs index a57f9ec1..c5b4e6da 100644 --- a/crates/polyxml-core/src/codegen/cpp/mod.rs +++ b/crates/polyxml-core/src/codegen/cpp/mod.rs @@ -274,7 +274,7 @@ impl CppCodegen { writeln!(out, "// Target: Modern C++20/C++23 (Header-Only)").unwrap(); writeln!(out, "#pragma once\n").unwrap(); - self.emit_includes(&mut out); + self.emit_includes(&mut out, ir); let ns = to_cpp_namespace(&self.options.namespace); writeln!(out, "namespace {} {{\n", ns).unwrap(); @@ -312,7 +312,7 @@ impl CppCodegen { writeln!(out, "// Target: Modern C++20/C++23 Module Interface Unit").unwrap(); writeln!(out, "module;\n").unwrap(); - self.emit_includes(&mut out); + self.emit_includes(&mut out, ir); let mod_id = if module_name.is_empty() { "polyxml.models" @@ -457,7 +457,19 @@ endif() ) } - fn emit_includes(&self, out: &mut String) { + fn emit_includes(&self, out: &mut String, ir: &SchemaIR) { + if self.options.validate_facets + && ir.types.values().any(|t| match t { + TypeDef::Simple(s) => !s.facets.patterns.is_empty(), + TypeDef::Struct(s) => s + .fields + .iter() + .any(|f| f.facets.as_ref().is_some_and(|f| !f.patterns.is_empty())), + _ => false, + }) + { + writeln!(out, "#include ").unwrap(); + } writeln!(out, "#include ").unwrap(); writeln!(out, "#include ").unwrap(); writeln!(out, "#include ").unwrap(); @@ -601,7 +613,7 @@ concept XmlModel = requires(T a) {{ // Emit simple type aliases first for type_def in ir.types.values() { if let TypeDef::Simple(simple) = type_def { - self.emit_simple_type(out, simple); + self.emit_simple_type(out, simple, ir); } } @@ -626,15 +638,27 @@ concept XmlModel = requires(T a) {{ } } - fn emit_simple_type(&self, out: &mut String, simple: &SimpleTypeDef) { + fn emit_simple_type(&self, out: &mut String, simple: &SimpleTypeDef, ir: &SchemaIR) { if let Some(ref doc) = simple.documentation { for line in doc.lines() { writeln!(out, "/// {}", line).unwrap(); } } let type_name = type_ident(&simple.qname); - let base_type = self.context.map_type_ref(&simple.base_type); + let base = if simple.facets.patterns.is_empty() { + &simple.base_type + } else { + super::primitive_base(&simple.base_type, ir) + }; + let base_type = self.context.map_type_ref(base); writeln!(out, "using {} = {};\n", type_name, base_type).unwrap(); + if self.options.validate_facets && !simple.facets.patterns.is_empty() { + writeln!(out, "[[nodiscard]] inline bool validate_{}_patterns(std::string_view value) noexcept {{\n try {{", type_name).unwrap(); + for (i, pattern) in simple.facets.patterns.iter().enumerate() { + writeln!(out, " static const std::regex pattern_{}({:?});\n if (!std::regex_search(value.begin(), value.end(), pattern_{})) return false;", i, pattern, i).unwrap(); + } + writeln!(out, " return true;\n }} catch (const std::regex_error&) {{ return false; }}\n}}\n").unwrap(); + } } fn emit_enum(&self, out: &mut String, enum_def: &EnumDef) { @@ -833,7 +857,7 @@ concept XmlModel = requires(T a) {{ } if self.options.validate_facets { - self.emit_struct_validator(out, s); + self.emit_struct_validator(out, s, ir); } writeln!(out, "}};\n").unwrap(); @@ -905,11 +929,39 @@ concept XmlModel = requires(T a) {{ } } - fn emit_struct_validator(&self, out: &mut String, s: &StructDef) { + fn emit_struct_validator(&self, out: &mut String, s: &StructDef, ir: &SchemaIR) { writeln!(out, "\n [[nodiscard]] bool validate() const noexcept {{").unwrap(); let mut has_checks = false; for f in &s.fields { + if let Some(simple) = super::patterned_simple(&f.type_ref, ir) { + let field = to_cpp_field_name(&f.name); + let name = type_ident(&simple.qname); + let is_string = self + .context + .map_type_ref(super::primitive_base(&simple.base_type, ir)) + == "std::string"; + let render = |target: String| { + if is_string { + target + } else { + format!("std::to_string({target})") + } + }; + if f.cardinality.is_list() || f.type_ref.is_list() { + writeln!(out, " for (const auto& value : {}) {{ if (!validate_{}_patterns({})) return false; }}", field, name, render("value".into())).unwrap(); + } else if f.cardinality.is_optional() || f.nillable { + writeln!(out, " if ({}.has_value()) {{ if (!validate_{}_patterns({})) return false; }}", field, name, render(format!("*{}", field))).unwrap(); + } else { + writeln!( + out, + " if (!validate_{}_patterns({})) return false;", + name, + render(field.clone()) + ) + .unwrap(); + } + } if let Some(ref facets) = f.facets { let field_name = to_cpp_field_name(&f.name); let is_opt = f.cardinality.is_optional() || f.nillable; @@ -943,6 +995,9 @@ concept XmlModel = requires(T a) {{ target: &str, indent: &str, ) { + for pattern in &facets.patterns { + writeln!(out, "{}try {{ static const std::regex pattern({:?}); if (!std::regex_search({}, pattern)) return false; }} catch (const std::regex_error&) {{ return false; }}", indent, pattern, target).unwrap(); + } if let Some(min_len) = facets.min_length { writeln!( out, diff --git a/crates/polyxml-core/src/codegen/csharp/mod.rs b/crates/polyxml-core/src/codegen/csharp/mod.rs index 74dba7f9..94abdaae 100644 --- a/crates/polyxml-core/src/codegen/csharp/mod.rs +++ b/crates/polyxml-core/src/codegen/csharp/mod.rs @@ -299,7 +299,11 @@ impl CSharpCodegen { if let TypeDef::Enum(e) = def { self.emit_enum(&mut out, e, indent); } else if let TypeDef::Simple(s) = def { - self.emit_simple(&mut out, s, indent); + let mut simple = s.clone(); + if !simple.facets.patterns.is_empty() { + simple.base_type = super::primitive_base(&simple.base_type, ir).clone(); + } + self.emit_simple(&mut out, &simple, indent); } } @@ -902,10 +906,10 @@ impl CSharpCodegen { .unwrap(); } for pattern in &facets.patterns { - let escaped = pattern.replace('"', "\\\""); + let escaped = pattern.replace('\\', "\\\\").replace('"', "\\\""); writeln!( out, - "{}if (!Regex.IsMatch({}.ToString() ?? \"\", \"^{}$\")) yield return new ValidationResult(\"{} does not match pattern {}\", [nameof({})]);", + "{}if (!Regex.IsMatch({}.ToString() ?? \"\", \"{}\")) yield return new ValidationResult(\"{} does not match pattern {}\", [nameof({})]);", indent, target, escaped, target, escaped, target ) .unwrap(); diff --git a/crates/polyxml-core/src/codegen/go/mod.rs b/crates/polyxml-core/src/codegen/go/mod.rs index 282b344a..f3719df0 100644 --- a/crates/polyxml-core/src/codegen/go/mod.rs +++ b/crates/polyxml-core/src/codegen/go/mod.rs @@ -252,7 +252,16 @@ impl GoCodegen { set_type_name_map(build_type_name_map(ir, to_go_type_name)); let mut body = String::new(); let mut has_time = false; - let mut has_fmt = false; + let has_patterns = self.options.validate_facets + && ir.types.values().any(|t| match t { + TypeDef::Simple(s) => !s.facets.patterns.is_empty(), + TypeDef::Struct(s) => s + .fields + .iter() + .any(|f| f.facets.as_ref().is_some_and(|f| !f.patterns.is_empty())), + _ => false, + }); + let mut has_fmt = has_patterns; let mut has_io = false; let mut has_xml = self.options.emit_xml_tags; @@ -329,6 +338,9 @@ impl GoCodegen { if has_io { imports.push("\"io\""); } + if has_patterns { + imports.push("\"regexp\""); + } if has_time { imports.push("\"time\""); } @@ -370,7 +382,7 @@ impl GoCodegen { // Emit simple types for type_def in ir.types.values() { if let TypeDef::Simple(simple) = type_def { - self.emit_simple_type(out, simple); + self.emit_simple_type(out, simple, ir); } } @@ -396,7 +408,7 @@ impl GoCodegen { } } - fn emit_simple_type(&self, out: &mut String, simple: &SimpleTypeDef) { + fn emit_simple_type(&self, out: &mut String, simple: &SimpleTypeDef, ir: &SchemaIR) { if let Some(ref doc) = simple.documentation { for line in doc.lines() { writeln!(out, "// {}", line).unwrap(); @@ -406,6 +418,21 @@ impl GoCodegen { let type_name = type_ident(&simple.qname); let base_type = self.context.map_type_ref(&simple.base_type); writeln!(out, "type {} {}\n", type_name, base_type).unwrap(); + if self.options.validate_facets && !simple.facets.patterns.is_empty() { + writeln!(out, "func (s {}) Validate() error {{", type_name).unwrap(); + for pattern in &simple.facets.patterns { + writeln!(out, " if matched, err := regexp.MatchString({:?}, fmt.Sprint(s)); err != nil || !matched {{ return fmt.Errorf(\"pattern constraint failed\") }}", pattern).unwrap(); + } + writeln!(out, " return nil\n}}\n").unwrap(); + if self + .context + .map_type_ref(super::primitive_base(&simple.base_type, ir)) + == "string" + { + writeln!(out, "func (s *{}) UnmarshalText(text []byte) error {{\n value := {}(text)\n if err := value.Validate(); err != nil {{ return err }}\n *s = value\n return nil\n}}\n", type_name, type_name).unwrap(); + writeln!(out, "func (s {}) MarshalText() ([]byte, error) {{\n if err := s.Validate(); err != nil {{ return nil, err }}\n return []byte(s), nil\n}}\n", type_name).unwrap(); + } + } } fn emit_enum(&self, out: &mut String, enum_def: &EnumDef) { @@ -678,7 +705,7 @@ impl GoCodegen { writeln!(out, "}}\n").unwrap(); if self.options.validate_facets { - self.emit_struct_validator(out, s); + self.emit_struct_validator(out, s, ir); } } @@ -752,7 +779,7 @@ impl GoCodegen { } } - fn emit_struct_validator(&self, out: &mut String, s: &StructDef) { + fn emit_struct_validator(&self, out: &mut String, s: &StructDef, ir: &SchemaIR) { let struct_name = type_ident(&s.qname); writeln!(out, "func (s {}) Validate() error {{", struct_name).unwrap(); @@ -761,6 +788,20 @@ impl GoCodegen { let field_name = to_go_field_name(&f.name); let is_opt = f.cardinality.is_optional() || f.nillable; + if super::patterned_simple(&f.type_ref, ir).is_some() { + if f.cardinality.is_list() || f.type_ref.is_list() { + writeln!(out, " for _, value := range s.{} {{ if err := value.Validate(); err != nil {{ return err }} }}", field_name).unwrap(); + } else if is_opt { + writeln!(out, " if s.{} != nil {{ if err := s.{}.Validate(); err != nil {{ return err }} }}", field_name, field_name).unwrap(); + } else { + writeln!( + out, + " if err := s.{}.Validate(); err != nil {{ return err }}", + field_name + ) + .unwrap(); + } + } if let Some(ref facets) = f.facets { if is_opt { writeln!(out, " if s.{} != nil {{", field_name).unwrap(); @@ -790,6 +831,9 @@ impl GoCodegen { target: &str, indent: &str, ) { + for pattern in &facets.patterns { + writeln!(out, "{}if matched, err := regexp.MatchString({:?}, fmt.Sprint({})); err != nil || !matched {{ return fmt.Errorf(\"pattern constraint failed\") }}", indent, pattern, target).unwrap(); + } if let Some(min_len) = facets.min_length { writeln!( out, diff --git a/crates/polyxml-core/src/codegen/java/codec.rs b/crates/polyxml-core/src/codegen/java/codec.rs index e5cd49b3..7bbc8a52 100644 --- a/crates/polyxml-core/src/codegen/java/codec.rs +++ b/crates/polyxml-core/src/codegen/java/codec.rs @@ -53,7 +53,14 @@ impl JavaCodegen { } } } - TypeDef::Simple(s) => check_type(&s.base_type, ir)?, + TypeDef::Simple(s) => check_type( + if s.facets.patterns.is_empty() { + &s.base_type + } else { + crate::codegen::primitive_base(&s.base_type, ir) + }, + ir, + )?, TypeDef::Union(u) => { for b in &u.branches { check_type(&b.type_ref, ir)?; @@ -151,7 +158,15 @@ impl JavaCodegen { match def { TypeDef::Struct(s) => self.emit_read_struct(&mut out, s, ir), TypeDef::Simple(s) => { - let expr = self.parse_scalar(&s.base_type, "reader.getElementText()", ir); + let expr = self.parse_scalar( + if s.facets.patterns.is_empty() { + &s.base_type + } else { + crate::codegen::primitive_base(&s.base_type, ir) + }, + "reader.getElementText()", + ir, + ); let _ = writeln!(out, " return new {name}({expr});"); } TypeDef::Enum(_) => { @@ -186,7 +201,15 @@ impl JavaCodegen { } else { "value.getValue()" }; - let expr = self.format_scalar(&s.base_type, access, ir); + let expr = self.format_scalar( + if s.facets.patterns.is_empty() { + &s.base_type + } else { + crate::codegen::primitive_base(&s.base_type, ir) + }, + access, + ir, + ); let _ = writeln!(out, " writer.writeCharacters({expr});"); } TypeDef::Union(u) => { @@ -432,7 +455,15 @@ impl JavaCodegen { Some(TypeDef::Simple(s)) => format!( "new {}({})", type_ident(q), - self.parse_scalar(&s.base_type, raw, ir) + self.parse_scalar( + if s.facets.patterns.is_empty() { + &s.base_type + } else { + crate::codegen::primitive_base(&s.base_type, ir) + }, + raw, + ir + ) ), _ => "null".into(), }, @@ -468,7 +499,7 @@ impl JavaCodegen { TypeRef::List(t)=>format!("{value}.stream().map(item -> {}).collect(java.util.stream.Collectors.joining(\" \"))",self.format_scalar(t,"item",ir)), TypeRef::Named(q)=>match ir.types.get(q) { Some(TypeDef::Enum(_))=>format!("{value}.getValue()"), - Some(TypeDef::Simple(s))=>self.format_scalar(&s.base_type,&format!("{value}.{}",if self.options.use_records {"value()"}else{"getValue()"}),ir), + Some(TypeDef::Simple(s))=>self.format_scalar(if s.facets.patterns.is_empty() { &s.base_type } else { crate::codegen::primitive_base(&s.base_type, ir) },&format!("{value}.{}",if self.options.use_records {"value()"}else{"getValue()"}),ir), _=>format!("String.valueOf({value})"), }, TypeRef::Primitive(PrimitiveType::Base64Binary)=>format!("java.util.Base64.getEncoder().encodeToString({value})"), diff --git a/crates/polyxml-core/src/codegen/java/mod.rs b/crates/polyxml-core/src/codegen/java/mod.rs index 7ad0a4c3..0ee4f806 100644 --- a/crates/polyxml-core/src/codegen/java/mod.rs +++ b/crates/polyxml-core/src/codegen/java/mod.rs @@ -394,7 +394,13 @@ impl JavaCodegen { } TypeDef::Simple(s) => { let name = type_ident(&s.qname); - self.emit_simple(&mut out, s, &name, indent); + { + let mut simple = s.clone(); + if !simple.facets.patterns.is_empty() { + simple.base_type = super::primitive_base(&simple.base_type, ir).clone(); + } + self.emit_simple(&mut out, &simple, &name, indent); + } name } }; @@ -829,7 +835,7 @@ impl JavaCodegen { } for pat in &facets.patterns { checks.push(format!( - "{}.ifPresent(v -> {{ if (!Pattern.matches({:?}, v)) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?}); }});", + "{}.ifPresent(v -> {{ if (!Pattern.compile({:?}).matcher(v).find()) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?}); }});", var_name, pat, var_name, pat )); } @@ -858,7 +864,7 @@ impl JavaCodegen { } for pat in &facets.patterns { checks.push(format!( - "if (!Pattern.matches({:?}, {})) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?});", + "if (!Pattern.compile({:?}).matcher({}).find()) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?});", pat, var_name, var_name, pat )); } diff --git a/crates/polyxml-core/src/codegen/mod.rs b/crates/polyxml-core/src/codegen/mod.rs index 635645d0..956bbf8e 100644 --- a/crates/polyxml-core/src/codegen/mod.rs +++ b/crates/polyxml-core/src/codegen/mod.rs @@ -598,3 +598,34 @@ fn generic_keywords() -> &'static HashSet<&'static str> { .collect() }) } + +/// Resolve a simple alias to its underlying primitive without looping on cycles. +pub(crate) fn primitive_base<'a>(ty: &'a TypeRef, ir: &'a SchemaIR) -> &'a TypeRef { + let mut current = ty; + let mut visited = HashSet::new(); + while let TypeRef::Named(q) = current { + if !visited.insert(q) { + break; + } + match ir.types.get(q) { + Some(crate::ir::TypeDef::Simple(s)) => current = &s.base_type, + _ => break, + } + } + current +} + +/// Patterned simple types referenced by fields, including list/boxed wrappers. +pub(crate) fn patterned_simple<'a>( + ty: &TypeRef, + ir: &'a SchemaIR, +) -> Option<&'a crate::ir::SimpleTypeDef> { + match ty { + TypeRef::Named(q) => match ir.types.get(q) { + Some(crate::ir::TypeDef::Simple(s)) if !s.facets.patterns.is_empty() => Some(s), + _ => None, + }, + TypeRef::Boxed(inner) | TypeRef::List(inner) => patterned_simple(inner, ir), + _ => None, + } +} diff --git a/crates/polyxml-core/src/codegen/rust/mod.rs b/crates/polyxml-core/src/codegen/rust/mod.rs index f712067d..651fc85b 100644 --- a/crates/polyxml-core/src/codegen/rust/mod.rs +++ b/crates/polyxml-core/src/codegen/rust/mod.rs @@ -437,6 +437,16 @@ impl RustCodegen { } else { let _ = writeln!(out, "pub type {} = {};", type_name, base_type); } + + if !s.facets.patterns.is_empty() { + let _ = writeln!(out, "pub fn validate_{}_patterns(value: &str) -> std::result::Result<(), &'static str> {{", type_name); + for (i, pattern) in s.facets.patterns.iter().enumerate() { + let _ = writeln!(out, " static PATTERN_{}: std::sync::OnceLock> = std::sync::OnceLock::new();", i); + let _ = writeln!(out, " let pattern = PATTERN_{}.get_or_init(|| regex::Regex::new({:?})).as_ref().map_err(|_| \"unsupported pattern syntax\")?;", i, pattern); + out.push_str(" if !pattern.is_match(value) { return Err(\"pattern constraint failed\"); }\n"); + } + out.push_str(" Ok(())\n}\n"); + } } fn emit_enum(&self, out: &mut String, e: &EnumDef) { @@ -998,6 +1008,10 @@ impl RustCodegen { types_with_lifetime: &HashSet, ir: &SchemaIR, ) { + let has_patterns = s + .fields + .iter() + .any(|f| super::patterned_simple(&f.type_ref, ir).is_some()); let struct_name = type_ident(&s.qname); let needs_lifetime = types_with_lifetime.contains(&s.qname); @@ -1204,7 +1218,11 @@ impl RustCodegen { out.push_str(" }\n"); } - out.push_str("\n Ok(Self {\n"); + if has_patterns { + out.push_str("\n let value = Self {\n"); + } else { + out.push_str("\n Ok(Self {\n"); + } for meta in &field_metas { let is_list = meta.field.cardinality.is_list() || meta.field.type_ref.is_list(); let is_optional = meta.field.cardinality.is_optional() || meta.field.nillable; @@ -1223,7 +1241,11 @@ impl RustCodegen { ); } } - out.push_str(" })\n"); + if has_patterns { + out.push_str(" };\n value.validate_patterns()?;\n Ok(value)\n"); + } else { + out.push_str(" })\n"); + } out.push_str(" }\n\n"); out.push_str(" pub fn decode_xml_empty(start: &BytesStart<'_>) -> Result {\n"); @@ -1250,7 +1272,11 @@ impl RustCodegen { out.push_str(" }\n"); } - out.push_str("\n Ok(Self {\n"); + if has_patterns { + out.push_str("\n let value = Self {\n"); + } else { + out.push_str("\n Ok(Self {\n"); + } for meta in &field_metas { let is_list = meta.field.cardinality.is_list() || meta.field.type_ref.is_list(); let is_optional = meta.field.cardinality.is_optional() || meta.field.nillable; @@ -1295,7 +1321,11 @@ impl RustCodegen { ); } } - out.push_str(" })\n"); + if has_patterns { + out.push_str(" };\n value.validate_patterns()?;\n Ok(value)\n"); + } else { + out.push_str(" })\n"); + } out.push_str(" }\n\n"); out.push_str(" pub fn to_xml(&self) -> Result> {\n"); @@ -1340,7 +1370,41 @@ impl RustCodegen { out.push_str(" }\n\n"); } + if has_patterns { + out.push_str(" pub fn validate_patterns(&self) -> Result<()> {\n"); + for meta in &field_metas { + let f = &meta.field; + if super::patterned_simple(&f.type_ref, ir).is_none() { + continue; + } + if f.cardinality.is_list() || f.type_ref.is_list() { + let _ = writeln!(out, " for value in &self.{} {{", meta.rust_name); + self.emit_pattern_check(out, f, "&value.to_string()", ir); + out.push_str(" }\n"); + } else if f.cardinality.is_optional() || f.nillable { + let _ = writeln!( + out, + " if let Some(value) = &self.{} {{", + meta.rust_name + ); + self.emit_pattern_check(out, f, "&value.to_string()", ir); + out.push_str(" }\n"); + } else { + self.emit_pattern_check( + out, + f, + &format!("&self.{}.to_string()", meta.rust_name), + ir, + ); + } + } + + out.push_str(" Ok(())\n }\n"); + } out.push_str(" pub fn encode_xml(&self, writer: &mut Writer, tag_name: Option<&str>) -> Result<()> {\n"); + if has_patterns { + out.push_str(" self.validate_patterns()?;\n"); + } let _ = writeln!( out, " let tag = tag_name.unwrap_or(\"{}\");", @@ -1365,6 +1429,12 @@ impl RustCodegen { out.push_str("}\n"); } + fn emit_pattern_check(&self, out: &mut String, field: &FieldDef, value: &str, ir: &SchemaIR) { + if let Some(simple) = super::patterned_simple(&field.type_ref, ir) { + let _ = writeln!(out, " validate_{}_patterns({}).map_err(|message| PolyXmlError::FacetViolation {{ field: {:?}.into(), expected: message.into(), actual: ({}).to_string() }})?;", type_ident(&simple.qname), value, field.name, value); + } + } + fn emit_attr_parse(&self, out: &mut String, field: &FieldDef, rust_name: &str, ir: &SchemaIR) { if self.field_is_string(&field.type_ref, ir) { out.push_str(" let s = attr.value.as_ref();\n"); @@ -1375,6 +1445,7 @@ impl RustCodegen { out.push_str(" Cow::Owned(s) => Cow::Owned(s),\n"); out.push_str(" };\n"); self.emit_facet_checks(out, field, "val"); + self.emit_pattern_check(out, field, "&val", ir); if self.options.zero_copy { let _ = writeln!(out, " var_{} = Some(val);", rust_name); } else { @@ -1426,6 +1497,7 @@ impl RustCodegen { field.xml_name ); self.emit_facet_checks(out, field, "text"); + self.emit_pattern_check(out, field, "&text", ir); if is_list { let _ = writeln!(out, " var_{}.push(text);", rust_name); } else { @@ -1532,6 +1604,7 @@ impl RustCodegen { let is_boxed = field.is_cycle_cut || field.type_ref.is_boxed(); if self.field_is_string(&field.type_ref, ir) { + self.emit_pattern_check(out, field, "\"\"", ir); if self.options.zero_copy { if is_list { let _ = writeln!( diff --git a/crates/polyxml-core/src/codegen/typescript/mod.rs b/crates/polyxml-core/src/codegen/typescript/mod.rs index 9ee30349..ce5f2e2f 100644 --- a/crates/polyxml-core/src/codegen/typescript/mod.rs +++ b/crates/polyxml-core/src/codegen/typescript/mod.rs @@ -219,7 +219,13 @@ impl TypeScriptCodegen { let ordered = self.order_types(ir); for type_def in ordered { match type_def { - TypeDef::Simple(s) => self.emit_simple_type(&mut out, s), + TypeDef::Simple(s) => { + let mut simple = s.clone(); + if !simple.facets.patterns.is_empty() { + simple.base_type = super::primitive_base(&simple.base_type, ir).clone(); + } + self.emit_simple_type(&mut out, &simple) + } TypeDef::Enum(e) => self.emit_enum(&mut out, e), TypeDef::Union(u) => self.emit_union(&mut out, u), TypeDef::Struct(s) => self.emit_struct(&mut out, s, ir, &recursive_types), @@ -851,7 +857,7 @@ impl TypeScriptCodegen { actions.push(format!("v.length({})", length)); } for pat in &facets.patterns { - actions.push(format!("v.regex(/{}/)", pat)); + actions.push(format!("v.regex(/{}/)", pat.replace('/', "\\/"))); } } if is_num { @@ -920,8 +926,8 @@ impl TypeScriptCodegen { if let Some(length) = facets.length { opts.push(format!("minLength: {}, maxLength: {}", length, length)); } - for pat in &facets.patterns { - opts.push(format!("pattern: {:?}", pat)); + if facets.patterns.len() == 1 { + opts.push(format!("pattern: {:?}", facets.patterns[0])); } } if is_num { @@ -948,6 +954,14 @@ impl TypeScriptCodegen { expr.push(')'); } } + if is_string && facets.patterns.len() > 1 { + let schemas = facets + .patterns + .iter() + .map(|p| format!("Type.String({{ pattern: {:?} }})", p)) + .collect::>(); + *expr = format!("Type.Intersect([{}, {}])", expr, schemas.join(", ")); + } } fn emit_docstring(&self, out: &mut String, doc: &str, indent: &str) { diff --git a/crates/polyxml-core/src/ir/mod.rs b/crates/polyxml-core/src/ir/mod.rs index 2ed25808..bd038a4c 100644 --- a/crates/polyxml-core/src/ir/mod.rs +++ b/crates/polyxml-core/src/ir/mod.rs @@ -249,6 +249,9 @@ pub struct RestrictionFacets { pub min_length: Option, pub max_length: Option, pub length: Option, + /// AND-combined derivation steps. Each entry is one regex, with alternatives + /// from the same restriction normalized to `(first)|(second)` (OR). + /// A singleton restriction retains its original regex verbatim. pub patterns: Vec, pub enumerations: Vec, pub white_space: Option, diff --git a/crates/polyxml-core/src/schema_parser/mod.rs b/crates/polyxml-core/src/schema_parser/mod.rs index d18df338..b93979c1 100644 --- a/crates/polyxml-core/src/schema_parser/mod.rs +++ b/crates/polyxml-core/src/schema_parser/mod.rs @@ -867,6 +867,11 @@ impl XsdParser { base_type = resolve_type_ref(&base, target_ns, prefixes); } } + "pattern" => { + if let Some(val) = get_attr_value(e, "value") { + facets.patterns.push(val); + } + } "enumeration" => { if let Some(val) = get_attr_value(e, "value") { enum_values.push(EnumValue { @@ -950,6 +955,18 @@ impl XsdParser { buf.clear(); } + // XSD same-restriction alternatives form ONE pattern facet. Preserve + // each derivation boundary as a separate entry during inheritance. + if facets.patterns.len() > 1 { + let alternatives = facets + .patterns + .iter() + .map(|p| format!("({p})")) + .collect::>() + .join("|"); + facets.patterns = vec![alternatives]; + } + // Issue #51 item 2: drop duplicate enumeration values (keep the first // occurrence) and disambiguate variant names that collide after // sanitization, so generated enums always compile. diff --git a/crates/polyxml-core/tests/test_pattern_codegen.rs b/crates/polyxml-core/tests/test_pattern_codegen.rs new file mode 100644 index 00000000..e065d3fc --- /dev/null +++ b/crates/polyxml-core/tests/test_pattern_codegen.rs @@ -0,0 +1,360 @@ +//! Issue #54: execute generated validators, not just regex source assertions. +use polyxml::codegen::*; +use polyxml::ir::{QName, RestrictionFacets, SchemaIR, TypeDef}; +use polyxml::schema_parser::XsdParser; +use std::{fs, path::Path, process::Command}; +use tempfile::tempdir; + +const SCHEMA: &str = r#" + + letters + + + + + + + + + + + + + + + +"#; + +fn schema() -> SchemaIR { + let dir = tempdir().unwrap(); + let path = dir.path().join("pattern.xsd"); + fs::write(&path, SCHEMA).unwrap(); + XsdParser::new().parse_file(&path).unwrap() +} +fn patterns<'a>(ir: &'a SchemaIR, name: &str) -> &'a [String] { + match &ir.types[&QName::new(None::, name)] { + TypeDef::Simple(s) => &s.facets.patterns, + _ => panic!("expected simple type"), + } +} +fn run(cmd: &mut Command) { + let output = cmd.output().unwrap(); + assert!( + output.status.success(), + "{cmd:?}\n{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} +fn available(program: &str) -> bool { + Command::new(program).arg("--version").output().is_ok() +} + +#[test] +fn pattern_restrictions_preserve_or_groups_and_inherited_and_steps() { + let ir = schema(); + assert_eq!(patterns(&ir, "ZBase"), ["([A-Z]+)|([0-9]+)"]); + assert_eq!( + patterns(&ir, "Derived"), + ["([A-Z]{3})|([0-9]{3})", "([A-Z]+)|([0-9]+)"] + ); + assert_eq!(patterns(&ir, "Mixed").len(), 3); + assert_eq!(patterns(&ir, "Single"), ["[A-Z]+"]); + // Facets (not the whole IR, whose QName map keys serde_json rejects) must + // survive serialization with their grouping intact. + let facets = match &ir.types[&QName::new(None::, "Mixed")] { + TypeDef::Simple(s) => &s.facets, + _ => panic!("expected simple type"), + }; + let serialized = serde_json::to_string(facets).unwrap(); + let restored: RestrictionFacets = serde_json::from_str(&serialized).unwrap(); + assert_eq!(&restored.patterns, &facets.patterns); +} + +#[test] +fn generated_go_pattern_validation_and_xml_roundtrip() { + if !available("go") { + return; + } + let dir = tempdir().unwrap(); + fs::write(dir.path().join("go.mod"), "module patterns\n\ngo 1.22\n").unwrap(); + fs::write( + dir.path().join("models.go"), + GoCodegen::new(GoOptions::default()).generate_module(&schema()), + ) + .unwrap(); + fs::write(dir.path().join("models_test.go"), r#"package models +import("testing"; "encoding/xml") +func TestPatterns(t *testing.T) { + for _, value := range []string{"ZZZ", "999", "xZZZy"} { + if err := Mixed(value).Validate(); err != nil { t.Fatal(value, err) } + var doc Document + if err := xml.Unmarshal([]byte(""+value+""), &doc); err != nil { t.Fatal(err) } + if err := doc.Validate(); err != nil { t.Fatal(err) } + if _, err := xml.Marshal(doc); err != nil { t.Fatal(err) } + } + for _, value := range []string{"ABC", "123", "Z", "9", "abc", ""} { + if Mixed(value).Validate() == nil { t.Fatal("accepted", value) } + var doc Document + if xml.Unmarshal([]byte(""+value+""), &doc) == nil { t.Fatal("read accepted", value) } + if _, err := xml.Marshal(Document{Value: Mixed(value)}); err == nil { t.Fatal("write accepted", value) } + } + if ZBase("ABC").Validate()!=nil || ZBase("123").Validate()!=nil { t.Fatal("OR") } + bad := Derived("x") + if (Document{Value: Mixed("ZZZ"), Optional: &bad}).Validate()==nil {t.Fatal("optional")} + if (Document{Value: Mixed("ZZZ"), Items: []Derived{bad}}).Validate()==nil {t.Fatal("list")} +} +"#).unwrap(); + run(Command::new("go").arg("test").current_dir(dir.path())); +} + +#[test] +fn generated_cpp_pattern_validation() { + if !available("g++") { + return; + } + let dir = tempdir().unwrap(); + fs::write( + dir.path().join("models.hpp"), + CppCodegen::new(CppOptions::default()).generate_header(&schema()), + ) + .unwrap(); + fs::write(dir.path().join("main.cpp"), r#"#include "models.hpp" +#include +using namespace polyxml::generated; +int main() { + for (auto value : {"ZZZ", "999", "xZZZy"}) { assert(validate_Mixed_patterns(value)); Document doc; doc.value=value; assert(doc.validate()); } + for (auto value : {"ABC", "123", "Z", "9", "abc", ""}) { assert(!validate_Mixed_patterns(value)); Document doc; doc.value=value; assert(!doc.validate()); } + assert(validate_ZBase_patterns("ABC")); assert(validate_ZBase_patterns("123")); + Document doc; doc.value="ZZZ"; doc.optional="x"; assert(!doc.validate()); + doc.optional.reset(); doc.items.push_back("x"); assert(!doc.validate()); +} +"#).unwrap(); + run(Command::new("g++") + .args(["-std=c++20", "main.cpp", "-o", "check"]) + .current_dir(dir.path())); + run(&mut Command::new(dir.path().join("check"))); +} + +#[test] +fn generated_rust_pattern_validation_and_xml_roundtrip() { + let dir = tempdir().unwrap(); + fs::create_dir(dir.path().join("src")).unwrap(); + let core = Path::new(env!("CARGO_MANIFEST_DIR")); + fs::write( + dir.path().join("Cargo.toml"), + format!( + r#"[package] +name="pattern-test" +version="0.0.0" +edition="2021" +[dependencies] +polyxml={{path={:?}}} +quick-xml={{version="0.42",features=["serialize"]}} +serde={{version="1",features=["derive"]}} +serde_json="1" +regex="1" +"#, + core + ), + ) + .unwrap(); + fs::write( + dir.path().join("src/models.rs"), + RustCodegen::new(RustOptions::default()).generate_module(&schema()), + ) + .unwrap(); + fs::write( + dir.path().join("src/main.rs"), + r#"mod models; +use models::*; +fn main() { + for value in ["ZZZ", "999", "xZZZy"] { + assert!(validate_Mixed_patterns(value).is_ok()); + let xml=format!("{value}"); + let doc=Document::from_xml(&xml).unwrap(); + assert!(doc.to_xml().is_ok()); + } + for value in ["ABC", "123", "Z", "9", "abc", ""] { + assert!(validate_Mixed_patterns(value).is_err()); + let xml=format!("{value}"); + assert!(Document::from_xml(&xml).is_err()); + let doc=Document { value: value.into(), ..Default::default() }; + assert!(doc.to_xml().is_err()); + } + assert!(Document::from_xml("").is_err()); + assert!(Document::from_xml("").is_err()); + assert!(validate_ZBase_patterns("ABC").is_ok()); + assert!(validate_ZBase_patterns("123").is_ok()); +} +"#, + ) + .unwrap(); + run(Command::new("cargo") + .args(["run", "--quiet", "--offline"]) + .env( + "CARGO_TARGET_DIR", + core.join("../../target/pattern-codegen-tests"), + ) + .current_dir(dir.path())); +} + +#[test] +fn generated_python_pattern_validation() { + let python = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.venv/bin/python"); + if !python.exists() { + return; + } + let dir = tempdir().unwrap(); + fs::write( + dir.path().join("models.py"), + PythonCodegen::new(PythonOptions { + backend: PythonBackend::Pydantic, + ..Default::default() + }) + .generate_module(&schema()), + ) + .unwrap(); + fs::write( + dir.path().join("check.py"), + r#"from models import Mixed, Derived, ZBase +from pydantic import TypeAdapter, ValidationError +mixed = TypeAdapter(Mixed) +for value in ('ZZZ', '999', 'xZZZy'): + assert mixed.validate_python(value) == value +for value in ('ABC', '123', 'Z', '9', 'abc', ''): + try: + mixed.validate_python(value) + except ValidationError: + pass + else: + raise AssertionError(value) +for value in ('ABC', '123'): + assert TypeAdapter(ZBase).validate_python(value) == value +"#, + ) + .unwrap(); + run(Command::new(python).arg("check.py").current_dir(dir.path())); +} + +#[test] +fn generated_java_pattern_validation_and_direct_codecs() { + if !available("javac") { + return; + } + let dir = tempdir().unwrap(); + let generator = JavaCodegen::new(JavaOptions { + package_name: "patterns".into(), + emit_direct_codec: true, + ..Default::default() + }); + let mut files = Vec::new(); + for (name, contents) in generator.generate_files(&schema()) { + fs::write(dir.path().join(&name), contents).unwrap(); + files.push(name); + } + fs::write(dir.path().join("Check.java"), r#"package patterns; +import java.io.*; +public class Check { + public static void main(String[] args) throws Exception { + for (String value : new String[]{"ZZZ", "999", "xZZZy"}) { + new Mixed(value); + var parsed = MixedCodec.readXml(new ByteArrayInputStream((""+value+"").getBytes())); + if (!parsed.value().equals(value)) throw new AssertionError(); + MixedCodec.writeXml(parsed, new ByteArrayOutputStream()); + } + for (String value : new String[]{"ABC", "123", "Z", "9", "abc", ""}) { + try { new Mixed(value); throw new AssertionError(value); } + catch (IllegalArgumentException expected) { } + } + new ZBase("ABC"); new ZBase("123"); + } +} +"#).unwrap(); + files.push("Check.java".into()); + run(Command::new("javac") + .args(["-d", "."]) + .args(&files) + .current_dir(dir.path())); + run(Command::new("java") + .args(["-cp", ".", "patterns.Check"]) + .current_dir(dir.path())); +} + +#[test] +fn generated_csharp_pattern_validation() { + if !available("dotnet") { + return; + } + let dir = tempdir().unwrap(); + fs::write(dir.path().join("Check.csproj"), r#"Exenet8.0enableenable"#).unwrap(); + fs::write( + dir.path().join("Models.cs"), + CSharpCodegen::new(CSharpOptions::default()).generate_module(&schema()), + ) + .unwrap(); + fs::write(dir.path().join("Program.cs"), r#"using Generated; +using System.ComponentModel.DataAnnotations; +static bool Valid(IValidatableObject value) => !value.Validate(new ValidationContext(value)).Any(); +foreach(var value in new[]{"ZZZ", "999", "xZZZy"}) if(!Valid(new Mixed(value))) throw new Exception(value); +foreach(var value in new[]{"ABC", "123", "Z", "9", "abc", ""}) if(Valid(new Mixed(value))) throw new Exception(value); +if(!Valid(new ZBase("ABC")) || !Valid(new ZBase("123"))) throw new Exception("OR"); +"#).unwrap(); + run(Command::new("dotnet") + .args(["run", "--project", "Check.csproj", "--verbosity", "quiet"]) + .current_dir(dir.path())); +} + +#[test] +fn generated_typescript_pattern_validation_all_backends() { + // Optional installed toolchain: npm install --prefix zod@3 valibot@1 + // @sinclair/typebox@0.34 typescript@5; set POLYXML_TS_TEST_MODULES=/node_modules. + let Ok(modules) = std::env::var("POLYXML_TS_TEST_MODULES") else { + return; + }; + let dir = tempdir().unwrap(); + #[cfg(unix)] + std::os::unix::fs::symlink(&modules, dir.path().join("node_modules")).unwrap(); + for (name, backend) in [ + ("zod", TypeScriptBackend::Zod), + ("valibot", TypeScriptBackend::Valibot), + ("typebox", TypeScriptBackend::TypeBox), + ] { + fs::write( + dir.path().join(format!("{name}.ts")), + TypeScriptCodegen::new(TypeScriptOptions { + backend, + ..Default::default() + }) + .generate_module(&schema()), + ) + .unwrap(); + } + fs::write(dir.path().join("check.ts"), r#"import { MixedSchema as z, ZBaseSchema as zb } from './zod'; +import { MixedSchema as v, ZBaseSchema as vb } from './valibot'; +import { MixedSchema as t, ZBaseSchema as tb } from './typebox'; +import * as valibot from 'valibot'; +import { Value } from '@sinclair/typebox/value'; +for(const value of ['ZZZ','999','xZZZy']) { + if(!z.safeParse(value).success || !valibot.safeParse(v,value).success || !Value.Check(t,value)) throw Error(value); +} +for(const value of ['ABC','123','Z','9','abc','']) { + if(z.safeParse(value).success || valibot.safeParse(v,value).success || Value.Check(t,value)) throw Error(value); +} +for(const value of ['ABC','123']) { + if(!zb.safeParse(value).success || !valibot.safeParse(vb,value).success || !Value.Check(tb,value)) throw Error('OR'); +} +"#).unwrap(); + run(Command::new(Path::new(&modules).join(".bin/tsc")) + .args([ + "check.ts", + "--target", + "es2020", + "--module", + "commonjs", + "--strict", + "--skipLibCheck", + ]) + .current_dir(dir.path())); + run(Command::new("node").arg("check.js").current_dir(dir.path())); +} diff --git a/scripts/test_codegen.sh b/scripts/test_codegen.sh index d26895c9..72a32d50 100755 --- a/scripts/test_codegen.sh +++ b/scripts/test_codegen.sh @@ -26,7 +26,8 @@ cargo test -p polyxml \ --test test_java_codegen \ --test test_ts_codegen \ --test test_go_codegen \ - --test test_csharp_codegen + --test test_csharp_codegen \ + --test test_pattern_codegen echo -e "${BLUE}==> [3/3] Running PolyXML CLI code generation & manifest integration tests...${NC}" cargo test -p polyxml-cli --test test_cli From f82abd7c4f8dc9841d26b68bf3b023015ef35594 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 03:15:00 -0500 Subject: [PATCH 03/10] refactor(cli): remove deprecated legacy flags and manifest keys Delete the 7 hidden legacy flags (--zod, --source-gen, --record-kind, --rkyv, --builder, --codec and the deprecation-warning machinery) plus their polyxml.toml counterparts; unknown manifest keys now fail loudly via deny_unknown_fields. --zero-copy stays a first-class visible option because --feature cannot express false (owned Rust output). Co-Authored-By: opencode@users.noreply.github.com --- .../skills/polyxml-codegen-workflow/SKILL.md | 16 +- benchmarks/java/generate.py | 4 +- crates/polyxml-cli/src/completions.rs | 4 +- crates/polyxml-cli/src/config.rs | 23 +-- crates/polyxml-cli/src/main.rs | 67 ++------ crates/polyxml-cli/src/options.rs | 116 +------------ crates/polyxml-cli/tests/test_cli.rs | 156 ++++++------------ docs/guides/compiler.md | 17 +- docs/guides/csharp.md | 19 ++- docs/guides/java.md | 14 +- docs/guides/rust.md | 4 +- docs/why-polyxml.md | 4 +- scripts/verify_codegen.sh | 10 +- 13 files changed, 123 insertions(+), 331 deletions(-) diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 2ae4feae..42829a69 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -132,8 +132,8 @@ If external compilers are installed on the development machine, run the E2E veri ## 6. Java/C# Model Styles and Direct Java Codecs - `--style record|pojo|class` is shared by Java and C#. Records remain the default; - `pojo`/`class` select mutable models. Java `--builder` and - `--codec annotation|direct` must also be forwarded in both manifest forms. + `pojo`/`class` select mutable models. Java's `--feature builder,direct-codec` + must also be forwarded as `features = [...]` in both manifest forms. - Java mutable models/builders live in `java/models.rs`; StAX companions live in `java/codec.rs`. Inheritance must share field-name allocation between accessors, builders, and codecs. A derived builder extends its base builder and overrides @@ -159,7 +159,7 @@ If external compilers are installed on the development machine, run the E2E veri all 7 targets from whichever local `target/{debug,release}/polyxml` is newer; a clean `git status` there proves byte-for-byte output parity after codegen changes. The same schema is the best large-schema smoke for - `--style pojo --builder --codec direct`: `javac` the output and round-trip + `--style pojo --feature builder,direct-codec`: `javac` the output and round-trip `data/pacs_008_customer_credit_transfer.xml` through the generated root codec. - `benchmarks/java -Ppanama` needs JDK 22+, but the host default can be JDK 21. Set `JAVA_HOME`/`PATH` to a downloaded JDK (Temurin 25 worked) and run @@ -234,8 +234,14 @@ derived types inherit their base's patterns at parse time): parsers can still return `None`; the CLI must reject invalid values before emitter defaults are applied. - New opt-ins belong in repeatable `--feature` and manifest `features` arrays. - Legacy booleans remain hidden with warnings; reject contradictions rather - than silently overriding an explicit false. Keep CLI and manifest parity. + The legacy hidden flags (`--zod`, `--source-gen`, `--record-kind`, `--rkyv`, + `--builder`, `--codec`) and their manifest keys were deleted outright: clap + rejects the flags as unexpected arguments, `#[serde(deny_unknown_fields)]` + rejects the keys, and the deprecation-warning loop no longer exists. Keep + CLI and manifest parity. +- `--zero-copy` (and manifest `zero_copy`) stays a first-class bool because + `--feature` cannot express `false`; it alone selects owned Rust output, and + `--zero-copy=false --feature zero-copy` is still rejected as a contradiction. - Defaults remain unchanged, including C# record classes, Rust zero-copy, and Python slots/kw-only. Expose only implemented styles/features. The issue's future examples (phf, aot, Python plain class, C# mutable struct) are not yet diff --git a/benchmarks/java/generate.py b/benchmarks/java/generate.py index 5dcb63dd..05b192c0 100644 --- a/benchmarks/java/generate.py +++ b/benchmarks/java/generate.py @@ -22,7 +22,7 @@ subprocess.run(["cargo", "build", "-p", "polyxml-cli"], cwd=repo, check=True) for style, package in [("pojo", "pojo"), ("record", "records")]: subprocess.run([str(binary), "generate", str(xsd), "--lang", "java", "--style", style, - "--builder", "--codec", "direct", "--backend", "jackson", + "--feature", "builder,direct-codec", "--backend", "jackson", "--package", f"io.polyxml.bench.{package}", "--out", str(target / "generated-sources/polyxml" / package)], check=True) helper = target / "generated-sources/polyxml/RecordMutation.java" args = ", ".join("status" if field == "status" else f"value.{field}()" for field in fields) @@ -30,5 +30,5 @@ 'public static io.polyxml.bench.records.Message withStatus(io.polyxml.bench.records.Message value, String status) {\n' f'return new io.polyxml.bench.records.Message({args});\n' + '}\n}\n') subprocess.run([str(binary), "generate", str(base / "cases.xsd"), "--lang", "java", "--style", "pojo", - "--builder", "--codec", "direct", "--backend", "jackson", + "--feature", "builder,direct-codec", "--backend", "jackson", "--package", "io.polyxml.bench.cases", "--out", str(target / "generated-sources/polyxml/cases")], check=True) diff --git a/crates/polyxml-cli/src/completions.rs b/crates/polyxml-cli/src/completions.rs index fe805777..48dad809 100644 --- a/crates/polyxml-cli/src/completions.rs +++ b/crates/polyxml-cli/src/completions.rs @@ -150,9 +150,7 @@ pub fn candidates(kind: &str, words: &[String]) -> Vec { features: if kind == "feature" { &features } else { &[] }, ..Default::default() }; - languages - .iter() - .all(|lang| opts.resolve(lang, false).is_ok()) + languages.iter().all(|lang| opts.resolve(lang).is_ok()) }) .map(|value| value.to_string()) .collect() diff --git a/crates/polyxml-cli/src/config.rs b/crates/polyxml-cli/src/config.rs index 4dcbf848..e26e7fac 100644 --- a/crates/polyxml-cli/src/config.rs +++ b/crates/polyxml-cli/src/config.rs @@ -44,7 +44,11 @@ pub struct WorkspaceSection { } /// Target configuration from either `[[generate]]` or `[codegen.]`. +/// Unknown keys are rejected so removed legacy fields (`zod`, `source_gen`, +/// `record_kind`, `builder`, `codec`, `rkyv`) fail loudly instead of being +/// silently ignored. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct TargetConfig { pub target: String, pub output: String, @@ -65,17 +69,12 @@ pub struct TargetConfig { pub modules: Option, pub mode: Option, pub serializer: Option, - pub zod: Option, - pub source_gen: Option, - pub record_kind: Option, pub style: Option, - pub builder: Option, - pub codec: Option, - pub rkyv: Option, pub custom_header: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct CodegenTargetConfig { pub enabled: Option, pub output: Option, @@ -95,13 +94,7 @@ pub struct CodegenTargetConfig { pub modules: Option, pub mode: Option, pub serializer: Option, - pub zod: Option, - pub source_gen: Option, - pub record_kind: Option, pub style: Option, - pub builder: Option, - pub codec: Option, - pub rkyv: Option, pub custom_header: Option, } @@ -169,13 +162,7 @@ impl WorkspaceManifest { modules: cfg.modules, mode: cfg.mode.clone(), serializer: cfg.serializer.clone(), - zod: cfg.zod, - source_gen: cfg.source_gen, - record_kind: cfg.record_kind.clone(), style: cfg.style.clone(), - builder: cfg.builder, - codec: cfg.codec.clone(), - rkyv: cfg.rkyv, custom_header: cfg.custom_header.clone().or_else(|| ws_header.clone()), }); } diff --git a/crates/polyxml-cli/src/main.rs b/crates/polyxml-cli/src/main.rs index 069f539a..d69e3ecb 100644 --- a/crates/polyxml-cli/src/main.rs +++ b/crates/polyxml-cli/src/main.rs @@ -86,42 +86,18 @@ pub struct GenerateArgs { #[arg(short = 'm', long = "mode", value_name = "MODE")] pub mode: Option, - /// Zero-copy mode for Rust models (borrow Cow<'a, str> instead of owned String) - #[arg(long = "zero-copy", hide = true, default_missing_value = "true", num_args = 0..=1)] + /// Zero-copy mode for Rust models (borrow Cow<'a, str>); pass 'false' for owned String fields (default: true) + #[arg(long = "zero-copy", default_missing_value = "true", num_args = 0..=1)] pub zero_copy: Option, /// Emit streaming serialization and deserialization codecs (default: true) #[arg(long = "codecs", default_missing_value = "true", num_args = 0..=1)] pub codecs: Option, - /// Emit runtime Zod validation schemas for TypeScript (default: false) - #[arg(long = "zod", hide = true, default_missing_value = "true", num_args = 0..=1)] - pub zod: Option, - - /// C# System.Text.Json compile-time source generation context (default: false) - #[arg(long = "source-gen", hide = true, default_missing_value = "true", num_args = 0..=1)] - pub source_gen: Option, - - /// C# record representation ('class' or 'struct', default: 'class') - #[arg(long = "record-kind", hide = true, value_name = "KIND")] - pub record_kind: Option, - /// Target model style (e.g. record, pojo, class, record-class, record-struct) #[arg(long, value_name = "STYLE")] pub style: Option, - /// Emit fluent Java builders - #[arg(long, hide = true, default_missing_value = "true", num_args = 0..=1)] - pub builder: Option, - - /// Java XML binding: annotation (default) or direct StAX companion codecs - #[arg(long, hide = true, value_parser = ["annotation", "direct"])] - pub codec: Option, - - /// Rust rkyv zero-copy binary wire format serialization (default: false) - #[arg(long = "rkyv", hide = true, default_missing_value = "true", num_args = 0..=1)] - pub rkyv: Option, - /// Output directory for generated source files #[arg(short = 'o', long = "out", alias = "out-dir", value_name = "DIR")] pub out: Option, @@ -250,12 +226,6 @@ fn run_generate(args: GenerateArgs) -> Result<(), Box> { || args.mode.is_some() || args.zero_copy.is_some() || args.codecs.is_some() - || args.zod.is_some() - || args.source_gen.is_some() - || args.record_kind.is_some() - || args.builder.is_some() - || args.codec.is_some() - || args.rkyv.is_some() || args.out.is_some() || args.custom_header.is_some() || args.strict_facets @@ -291,18 +261,15 @@ fn run_generate(args: GenerateArgs) -> Result<(), Box> { mode: args.mode.as_deref(), zero_copy: args.zero_copy, codecs: args.codecs, - zod: args.zod, - source_gen: args.source_gen, - record_kind: args.record_kind.as_deref(), style: args.style.as_deref(), - builder: args.builder, - codec: args.codec.as_deref(), - rkyv: args.rkyv, + builder: None, + codec: None, + rkyv: None, custom_header: args.custom_header.as_deref(), }; let resolved_options = languages .iter() - .map(|lang| emit_opts.resolve(lang, true)) + .map(|lang| emit_opts.resolve(lang)) .collect::>>()?; let mut parser = XsdParser::new(); @@ -359,7 +326,7 @@ fn run_build(args: BuildArgs) -> Result<(), Box> { let targets = manifest.resolved_targets(); for target in &targets { - target_options(target).resolve(&target.target, true)?; + target_options(target).resolve(&target.target)?; } let schema_files = manifest.expand_schemas(base_dir)?; @@ -414,7 +381,7 @@ fn run_build(args: BuildArgs) -> Result<(), Box> { target_dir.display() ); - let emit_opts = target_options(target).resolve(&target.target, false)?; + let emit_opts = target_options(target).resolve(&target.target)?; for (schema_path, ir) in &compiled_schemas { emit_target_code(&target.target, emit_opts, &target_dir, schema_path, ir)?; @@ -511,9 +478,6 @@ pub struct TargetEmitOptions<'a> { pub mode: Option<&'a str>, pub zero_copy: Option, pub codecs: Option, - pub zod: Option, - pub source_gen: Option, - pub record_kind: Option<&'a str>, pub style: Option<&'a str>, pub builder: Option, pub codec: Option<&'a str>, @@ -607,7 +571,9 @@ fn emit_target_code( let options = TypeScriptOptions { backend: ts_backend, - emit_zod: opts.zod.unwrap_or(false), + // Zod emission is selected via `--backend zod`; the removed + // legacy `--zod` switch was the only other way to set this. + emit_zod: false, use_interface: true, readonly_fields: false, emit_root_aliases: true, @@ -742,10 +708,11 @@ fn emit_target_code( .map(|s| s.to_string_lossy()) .unwrap_or_else(|| "Models".into()); - let record_kind = opts - .record_kind - .and_then(CSharpRecordKind::from_str_loose) - .unwrap_or(CSharpRecordKind::Class); + let record_kind = if matches!(opts.style, Some("record-struct")) { + CSharpRecordKind::Struct + } else { + CSharpRecordKind::Class + }; let options = CSharpOptions { namespace: ns.to_string(), @@ -756,7 +723,7 @@ fn emit_target_code( use_records, use_file_scoped_namespaces: true, emit_root_records: true, - emit_source_gen: opts.source_gen.unwrap_or(false), + emit_source_gen: opts.backend == Some("source-gen"), source_gen_context_name: format!("{}JsonContext", AsPascalCase(&file_stem)), custom_header: opts.custom_header.map(|s| s.to_string()), }; diff --git a/crates/polyxml-cli/src/options.rs b/crates/polyxml-cli/src/options.rs index 51c24974..2c05ef49 100644 --- a/crates/polyxml-cli/src/options.rs +++ b/crates/polyxml-cli/src/options.rs @@ -19,19 +19,16 @@ pub(crate) fn target_options(target: &config::TargetConfig) -> TargetEmitOptions .or_else(|| target.modules.filter(|v| *v).map(|_| "modules")), zero_copy: target.zero_copy, codecs: target.codecs, - zod: target.zod, - source_gen: target.source_gen, - record_kind: target.record_kind.as_deref(), style: target.style.as_deref(), - builder: target.builder, - codec: target.codec.as_deref(), - rkyv: target.rkyv, + builder: None, + codec: None, + rkyv: None, custom_header: target.custom_header.as_deref(), } } impl<'a> TargetEmitOptions<'a> { - pub(crate) fn resolve(mut self, lang: &str, warn: bool) -> Result { + pub(crate) fn resolve(mut self, lang: &str) -> Result { let language = lang.to_lowercase(); let target = match language.as_str() { "python" | "py" => "python", @@ -67,76 +64,6 @@ impl<'a> TargetEmitOptions<'a> { } } - // Reject even explicit false legacy flags on unrelated targets: they are - // almost always mistakes in scripts, and must not silently do nothing. - for (name, present, required, replacement) in [ - ("zod", self.zod.is_some(), "typescript", "--backend zod"), - ( - "source-gen", - self.source_gen.is_some(), - "csharp", - "--backend source-gen", - ), - ( - "zero-copy", - self.zero_copy.is_some(), - "rust", - "--feature zero-copy", - ), - ("rkyv", self.rkyv.is_some(), "rust", "--feature rkyv"), - ( - "builder", - self.builder.is_some(), - "java", - "--feature builder", - ), - ( - "codec", - self.codec.is_some(), - "java", - "--feature direct-codec (or omit for annotations)", - ), - ( - "record-kind", - self.record_kind.is_some(), - "csharp", - "--style record-class or --style record-struct", - ), - ] { - if present { - if target != required { - return Err(invalid(format!("option '{name}' is not supported for target '{target}'; use it with '{required}'."))); - } - if warn { - eprintln!("warning: option '--{name}' is deprecated. Use '{replacement}' instead (manifest: backend/style/features)."); - } - } - } - if self.zod == Some(true) { - if self.backend.is_some_and(|v| { - TypeScriptBackend::from_str_loose(v) != Some(TypeScriptBackend::Zod) - }) { - return Err(invalid( - "--zod conflicts with the selected backend; use --backend zod.", - )); - } - self.backend = Some("zod"); - } else if self.zod == Some(false) - && self.backend.is_some_and(|v| { - TypeScriptBackend::from_str_loose(v) == Some(TypeScriptBackend::Zod) - }) - { - return Err(invalid("--zod false conflicts with --backend zod.")); - } - if target == "csharp" { - if let Some(backend) = self.backend { - let enabled = backend == "source-gen"; - if self.source_gen.is_some_and(|value| value != enabled) { - return Err(invalid("source-gen conflicts with the selected backend.")); - } - self.source_gen = Some(enabled); - } - } let supported_styles: &[&str] = match target { "java" => &["record", "pojo", "class"], "csharp" => &["record", "pojo", "class", "record-class", "record-struct"], @@ -164,34 +91,6 @@ impl<'a> TargetEmitOptions<'a> { )); } } - if let Some(kind) = self.record_kind { - if CSharpRecordKind::from_str_loose(kind).is_none() { - return Err(invalid(format!( - "Unknown record-kind '{kind}'; use class or struct." - ))); - } - } - if target == "csharp" { - let kind = match self.style { - Some("record-class" | "class" | "pojo") => Some("class"), - Some("record-struct") => Some("struct"), - _ => None, - }; - if let Some(kind) = kind { - if self.record_kind.is_some_and(|v| { - CSharpRecordKind::from_str_loose(v) != CSharpRecordKind::from_str_loose(kind) - }) { - return Err(invalid("style conflicts with record-kind.")); - } - self.record_kind = Some(kind); - } - } - if self - .codec - .is_some_and(|v| !matches!(v, "annotation" | "direct")) - { - return Err(invalid("Unknown codec; use annotation or direct.")); - } if let Some(mode) = self.mode { if target != "cpp" || CppMode::from_str_loose(mode).is_none() { return Err(invalid(format!("mode '{mode}' is not supported for target '{target}'; C++ supports header or modules."))); @@ -217,11 +116,6 @@ impl<'a> TargetEmitOptions<'a> { "slots" => &mut self.slots, "kw-only" => &mut self.kw_only, "direct-codec" => { - if self.codec == Some("annotation") { - return Err(invalid( - "feature 'direct-codec' conflicts with codec 'annotation'.", - )); - } self.codec = Some("direct"); continue; } @@ -229,7 +123,7 @@ impl<'a> TargetEmitOptions<'a> { }; if *option == Some(false) { return Err(invalid(format!( - "feature '{feature}' conflicts with its explicitly disabled legacy option." + "feature '{feature}' conflicts with an explicitly disabled option." ))); } *option = Some(true); diff --git a/crates/polyxml-cli/tests/test_cli.rs b/crates/polyxml-cli/tests/test_cli.rs index 4aee46de..5ac219cb 100644 --- a/crates/polyxml-cli/tests/test_cli.rs +++ b/crates/polyxml-cli/tests/test_cli.rs @@ -432,7 +432,8 @@ fn test_cli_typescript_generation() { "generate", "--lang", "ts", - "--zod", + "--backend", + "zod", "--out", zod_out.to_str().unwrap(), schema_file.to_str().unwrap(), @@ -1250,9 +1251,10 @@ fn test_cli_csharp_source_gen_and_record_struct() { schema_file.to_str().unwrap(), "--lang", "csharp", - "--source-gen", - "--record-kind", - "struct", + "--backend", + "source-gen", + "--style", + "record-struct", "-o", out_dir.to_str().unwrap(), ]) @@ -1391,7 +1393,7 @@ fn test_cli_go_easyjson_and_sonic_backend() { } #[test] -fn test_cli_rust_rkyv_flag() { +fn test_cli_rust_rkyv_feature() { let dir = tempdir().unwrap(); let schema_file = dir.path().join("event.xsd"); fs::write( @@ -1414,7 +1416,8 @@ fn test_cli_rust_rkyv_flag() { schema_file.to_str().unwrap(), "--lang", "rust", - "--rkyv", + "--feature", + "rkyv", "-o", out_dir.to_str().unwrap(), ]) @@ -1455,8 +1458,8 @@ schemas = ["model.xsd"] [[generate]] target = "csharp" output = "cs" -source_gen = true -record_kind = "struct" +backend = "source-gen" +style = "record-struct" [[generate]] target = "ts" @@ -1471,7 +1474,7 @@ backend = "sonic" [[generate]] target = "rust" output = "rs" -rkyv = true +features = ["rkyv"] "#, ) .unwrap(); @@ -1628,9 +1631,8 @@ fn test_java_csharp_model_style_options_and_manifest() { "java", "--style", "pojo", - "--builder", - "--codec", - "direct", + "--feature", + "builder,direct-codec", "--out", java.to_str().unwrap(), ]) @@ -1647,16 +1649,9 @@ fn test_java_csharp_model_style_options_and_manifest() { assert!(java.join("ModelCodec.java").exists()); for args in [ vec!["--lang", "go", "--style", "pojo"], - vec!["--lang", "csharp", "--builder"], - vec![ - "--lang", - "csharp", - "--style", - "class", - "--record-kind", - "struct", - ], - vec!["--lang", "java", "--codec", "unknown"], + vec!["--lang", "csharp", "--feature", "builder"], + vec!["--lang", "cs", "--feature", "zero-copy"], + vec!["--lang", "java", "--backend", "unknown"], ] { let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) .args([ @@ -1679,8 +1674,7 @@ schemas = ["model.xsd"] target = "java" output = "generated-java" style = "class" -builder = true -codec = "direct" +features = ["builder", "direct-codec"] [codegen.csharp] output = "generated-csharp" style = "pojo" @@ -1702,70 +1696,43 @@ style = "pojo" assert!(cs.contains("get; set;")); let parsed: WorkspaceManifest = r#"[codegen.java] style="pojo" -builder=true -codec="direct" +features=["builder", "direct-codec"] "# .parse() .unwrap(); let target = &parsed.resolved_targets()[0]; assert_eq!(target.style.as_deref(), Some("pojo")); - assert_eq!(target.builder, Some(true)); - assert_eq!(target.codec.as_deref(), Some("direct")); + assert_eq!( + target.features, + vec!["builder".to_string(), "direct-codec".to_string()] + ); } #[test] -fn unified_options_match_legacy_output_and_warn() { +fn removed_legacy_flags_are_rejected() { let dir = tempdir().unwrap(); let schema = dir.path().join("model.xsd"); fs::write(&schema, r#""#).unwrap(); - for (lang, old, new) in [ - ("ts", vec!["--zod"], vec!["--backend", "zod"]), - ("cs", vec!["--source-gen"], vec!["--backend", "source-gen"]), - ( - "rs", - vec!["--zero-copy", "--rkyv"], - vec!["--feature", "zero-copy", "--feature", "rkyv"], - ), - ( - "java", - vec!["--builder", "--codec", "direct"], - vec!["--feature", "builder,direct-codec"], - ), - ( - "csharp", - vec!["--record-kind", "struct"], - vec!["--style", "record-struct"], - ), + for args in [ + vec!["--zod"], + vec!["--source-gen"], + vec!["--record-kind", "struct"], + vec!["--rkyv"], + vec!["--builder"], + vec!["--codec", "direct"], ] { - let legacy = dir.path().join(format!("{lang}-old")); - let unified = dir.path().join(format!("{lang}-new")); - for (flags, out, warning) in [(old, &legacy, true), (new, &unified, false)] { - let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) - .arg("generate") - .arg(&schema) - .args(["--lang", lang]) - .args(flags) - .arg("--out") - .arg(out) - .output() - .unwrap(); - let stderr = String::from_utf8_lossy(&result.stderr); - assert!(result.status.success(), "{lang}: {stderr}"); - assert_eq!(stderr.contains("deprecated"), warning, "{stderr}"); - } - let mut files = 0; - for entry in fs::read_dir(&legacy).unwrap() { - let entry = entry.unwrap(); - assert_eq!( - fs::read(entry.path()).unwrap(), - fs::read(unified.join(entry.file_name())).unwrap(), - "{lang}: {:?}", - entry.file_name() - ); - files += 1; - } - assert!(files > 0); - assert_eq!(files, fs::read_dir(&unified).unwrap().count()); + let result = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .arg("generate") + .arg(&schema) + .arg("--out") + .arg(dir.path().join("out")) + .args(&args) + .output() + .unwrap(); + assert!(!result.status.success(), "{args:?}"); + let stderr = String::from_utf8_lossy(&result.stderr); + assert!(stderr.contains("unexpected argument"), "{args:?}: {stderr}"); + assert!(!dir.path().join("out").exists()); } } @@ -1784,11 +1751,6 @@ fn unified_options_fail_before_schema_io_including_dry_run() { "feature 'rkyv'", ), (vec!["--lang", "rust", "--feature", "phf"], "feature 'phf'"), - (vec!["--lang", "python", "--zod"], "option 'zod'"), - ( - vec!["--lang", "ts", "--zod", "--backend", "valibot"], - "conflicts", - ), ( vec![ "--lang", @@ -1802,34 +1764,13 @@ fn unified_options_fail_before_schema_io_including_dry_run() { ( vec![ "--lang", - "java", - "--codec", - "annotation", - "--feature", - "direct-codec", - ], - "conflicts", - ), - ( - vec![ - "--lang", - "cs", - "--source-gen=false", + "python", "--backend", - "source-gen", - ], - "conflicts", - ), - ( - vec![ - "--lang", - "cs", + "pydantic", "--style", - "record-class", - "--record-kind", - "struct", + "dataclass", ], - "conflicts", + "requires the Python dataclass backend", ), ( vec!["--lang", "python", "--style", "class"], @@ -1929,13 +1870,12 @@ fn unified_help_hides_legacy_flags() { .output() .unwrap(); let help = String::from_utf8_lossy(&result.stdout); - for flag in ["--backend", "--style", "--feature"] { + for flag in ["--backend", "--style", "--feature", "--zero-copy"] { assert!(help.contains(flag)); } for flag in [ "--zod", "--source-gen", - "--zero-copy", "--rkyv", "--builder", "--record-kind", diff --git a/docs/guides/compiler.md b/docs/guides/compiler.md index 0c138c1b..b07db0d8 100644 --- a/docs/guides/compiler.md +++ b/docs/guides/compiler.md @@ -87,7 +87,7 @@ polyxml generate \ Options apply to every language selected in one invocation. Use `polyxml.toml` when targets need different options. Invalid values, unsupported combinations, -and conflicting legacy options fail before schema parsing or output creation, +and conflicting options fail before schema parsing or output creation, including with `--dry-run`. | Target | Backends (first is default) | Styles | Features | @@ -120,14 +120,13 @@ style = "pojo" features = ["builder", "direct-codec"] ``` -Legacy flags remain accepted but are hidden from help and emit a deprecation -warning. Replace `--zod` with `--backend zod`, `--source-gen` with -`--backend source-gen`, `--zero-copy` / `--rkyv` / `--builder` with their -`--feature` equivalents, `--codec direct` with `--feature direct-codec`, and -`--record-kind struct` with `--style record-struct`. Legacy manifest fields are -also retained and warned about. Explicit legacy `false` values are preserved; -combining one with an enabling feature is an error. For owned Rust strings, -`--zero-copy=false` (or `zero_copy = false` in the manifest) remains available. +The legacy hidden flags `--zod`, `--source-gen`, `--record-kind`, `--rkyv`, +`--builder`, and `--codec`, along with their `polyxml.toml` counterparts, have +been removed: clap rejects the flags outright and unknown manifest fields are +errors, so the unified options above are the only spellings. For owned Rust +strings, `--zero-copy=false` (or `zero_copy = false` in the manifest) remains +available as a first-class option; combining it with `--feature zero-copy` is +an error. `polyxml generate` without schema paths delegates to the manifest. Put target options in that manifest; command-line generation overrides are rejected rather diff --git a/docs/guides/csharp.md b/docs/guides/csharp.md index d213ebf9..06449e15 100644 --- a/docs/guides/csharp.md +++ b/docs/guides/csharp.md @@ -48,7 +48,7 @@ namespace = "Enterprise.Banking.Iso20022" ## Mutable classes ```bash -polyxml generate schema.xsd --lang csharp --style class --source-gen --out generated +polyxml generate schema.xsd --lang csharp --style class --backend source-gen --out generated ``` `--style pojo` is an alias for `class`; `--style record` retains the default. @@ -70,12 +70,13 @@ entity.Status = "PROCESSED"; output = "generated/csharp" namespace = "Enterprise.Models" style = "class" -source_gen = true +backend = "source-gen" ``` -These fields also work in `[[generate]]`. `--record-kind struct` cannot be -combined with mutable class style. `--builder` and `--codec` apply to Java; -C# uses object initializers and its standard XML/JSON serializers. +These fields also work in `[[generate]]`. `--style record-struct` and +`--style class` are mutually exclusive values of the same option. Java's +`--feature builder,direct-codec` options do not apply; C# uses object +initializers and its standard XML/JSON serializers. --- @@ -208,12 +209,12 @@ Customer restored = JsonSerializer.Deserialize(jsonString)!; assert(restored.Name == customer.Name); ``` -### Compile-Time Source Generation (`--source-gen`) +### Compile-Time Source Generation (`--backend source-gen`) For Native AOT, high-throughput microservices, and reflection-free environments, PolyXML can emit a compile-time `JsonSerializerContext`: ```bash -polyxml generate --lang csharp --source-gen --record-kind struct --out ./src/Generated schema.xsd +polyxml generate --lang csharp --backend source-gen --style record-struct --out ./src/Generated schema.xsd ``` This generates `[JsonSourceGenerationOptions]` and `[JsonSerializable(typeof(T))]` annotations: @@ -234,9 +235,9 @@ string json = JsonSerializer.Serialize(customer, CustomerJsonContext.Default.Cus Customer restored = JsonSerializer.Deserialize(json, CustomerJsonContext.Default.Customer); ``` -### Record Structs (`--record-kind struct`) +### Record Structs (`--style record-struct`) -By default, PolyXML emits reference `record class` types. For zero-allocation, cache-friendly scenarios where data contracts are small or short-lived, pass `--record-kind struct`: +By default, PolyXML emits reference `record class` types. For zero-allocation, cache-friendly scenarios where data contracts are small or short-lived, pass `--style record-struct`: ```csharp public readonly record struct Customer( diff --git a/docs/guides/java.md b/docs/guides/java.md index 4b8840d6..0d5a4fd4 100644 --- a/docs/guides/java.md +++ b/docs/guides/java.md @@ -10,9 +10,9 @@ description: Ultra-fast Java XML processing using Java 22 Foreign Function & Mem Records remain the default. Select mutable JavaBeans for setter-based frameworks: ```bash -polyxml generate schema.xsd --lang java --style pojo --builder --out generated -polyxml generate schema.xsd --lang java --style pojo --backend jackson --builder --out generated -polyxml generate schema.xsd --lang java --style record --builder --out generated +polyxml generate schema.xsd --lang java --style pojo --feature builder --out generated +polyxml generate schema.xsd --lang java --style pojo --backend jackson --feature builder --out generated +polyxml generate schema.xsd --lang java --style record --feature builder --out generated ``` `--style class` is an alias for `pojo`. Mutable complex types have a public no-argument @@ -49,10 +49,11 @@ is introduced. ## Direct streaming XML codecs ```bash -polyxml generate schema.xsd --lang java --style pojo --builder --codec direct --out generated +polyxml generate schema.xsd --lang java --style pojo --feature builder,direct-codec --out generated ``` -`--codec annotation` is the default and emits only models. `direct` adds a companion +Annotation-based codecs are the default and emit only models. +`--feature direct-codec` adds a companion `TypeNameCodec.java` for every generated type, with statically linked getters/setters, constructors, and nested codecs. It works with either record or POJO models and can be combined with Jackson annotations. It uses StAX from the JDK, without reflection @@ -97,9 +98,8 @@ target = "java" output = "generated/java" package = "com.enterprise.models" style = "pojo" -builder = true +features = ["builder", "direct-codec"] backend = "jackson" -codec = "direct" ``` The same options work under `[codegen.java]`. See the diff --git a/docs/guides/rust.md b/docs/guides/rust.md index 7be8f6b5..b13a3b51 100644 --- a/docs/guides/rust.md +++ b/docs/guides/rust.md @@ -358,12 +358,12 @@ let restored_xml = json_to_xml( --- -## 9. Zero-Copy Binary Wire Format Serialization via rkyv (`--rkyv`) +## 9. Zero-Copy Binary Wire Format Serialization via rkyv (`--feature rkyv`) For inter-process communication (IPC), shared memory, network wire protocols, and high-frequency messaging where even JSON and binary serialization overhead is unacceptable, PolyXML supports zero-copy deserialization via **`rkyv`**: ```bash -polyxml generate --lang rust --rkyv --out ./src/generated schema.xsd +polyxml generate --lang rust --feature rkyv --out ./src/generated schema.xsd ``` This generates `#[cfg_attr(feature = "rkyv", ...)]` attributes on structs, enums, and choices: diff --git a/docs/why-polyxml.md b/docs/why-polyxml.md index b445738b..b5bbd44a 100644 --- a/docs/why-polyxml.md +++ b/docs/why-polyxml.md @@ -51,7 +51,7 @@ PolyXML eliminates intermediate DOM allocations entirely: ### 3. Modern Language Idioms (2024–2026) vs. 20-Year-Old Code Generation Most legacy compilers were architected during the Java 5 / C++98 era. They generate sprawling boilerplate: -- **Immutable by default, mutable on request**: PolyXML generates immutable Java 21+ `record` types and `sealed interface` choice models that support compiler-enforced pattern matching without default branches. When a legacy framework requires JavaBeans, `--style pojo --builder` emits no-arg classes with getters/setters and fluent builders instead — same facets, same codecs. +- **Immutable by default, mutable on request**: PolyXML generates immutable Java 21+ `record` types and `sealed interface` choice models that support compiler-enforced pattern matching without default branches. When a legacy framework requires JavaBeans, `--style pojo --feature builder` emits no-arg classes with getters/setters and fluent builders instead — same facets, same codecs. - **No more raw pointers or Xerces**: PolyXML generates clean C++20 value types, `std::variant`, and C++20 concepts with zero external runtime dependencies. - **No more untyped Python bags**: PolyXML generates `@dataclass(slots=True, kw_only=True)` and Pydantic v2 models leveraging Python 3.12 PEP 695 type aliases (`type Sku = ...`) and PEP 604 union syntax (`TypeA | TypeB`). @@ -168,7 +168,7 @@ Across more than 600 official test groups from Sun Microsystems, Microsoft, and | If you are using... | PolyXML gives you... | | :--- | :--- | -| **JAXB / `xjc` in Java** | Immutable Java 21+ records, sealed interface choices, zero reflection overhead, and Project Panama FFI — or drop-in JavaBeans with fluent builders and zero-reflection StAX codecs (`--style pojo --builder --codec direct`) for existing codebases. | +| **JAXB / `xjc` in Java** | Immutable Java 21+ records, sealed interface choices, zero reflection overhead, and Project Panama FFI — or drop-in JavaBeans with fluent builders and zero-reflection StAX codecs (`--style pojo --feature builder,direct-codec`) for existing codebases. | | **CodeSynthesis in C++** | Modern C++20 value types, `std::variant`, zero Apache Xerces dependency, zero UTF-16 transcoding overhead, and a permissive MIT license. | | **`xsdata` in Python** | **10x faster** XML parsing, **23.5x faster** XML serialization, **9.5x faster** native JSON, 100% drop-in replacement (`JsonSerializer`, `JsonParser`), and direct C/Rust transcoding (`xml_to_json`, `json_to_xml`). | | **`xsd-parser` in Rust** | A battle-tested compiler that doesn't panic on complex schemas, with automatic Tarjan `Box` cycle breaks, inherent streaming XML codecs, and native `.to_json_string()` codecs. | diff --git a/scripts/verify_codegen.sh b/scripts/verify_codegen.sh index 503da0aa..4dec66f7 100755 --- a/scripts/verify_codegen.sh +++ b/scripts/verify_codegen.sh @@ -58,8 +58,8 @@ EOF echo -e "${BLUE}==> Testing code generation across all 7 target ecosystems...${NC}" # 1. Rust (zero-copy + rkyv) -echo -n " • Rust (--zero-copy --rkyv)... " -"${POLYXML_BIN}" generate --lang rust --zero-copy --rkyv --out "${TMP_DIR}/rs" "${SCHEMA_FILE}" >/dev/null +echo -n " • Rust (--feature zero-copy,rkyv)... " +"${POLYXML_BIN}" generate --lang rust --feature zero-copy,rkyv --out "${TMP_DIR}/rs" "${SCHEMA_FILE}" >/dev/null test -f "${TMP_DIR}/rs/test_schema.rs" echo -e "${GREEN}OK${NC}" @@ -98,7 +98,7 @@ echo -e "${GREEN}OK${NC}" # Mutable Java models and direct codecs. echo -n " • Java (POJO + builder + direct codec)... " -"${POLYXML_BIN}" generate --lang java --style pojo --builder --codec direct --out "${TMP_DIR}/java_pojo" "${SCHEMA_FILE}" >/dev/null +"${POLYXML_BIN}" generate --lang java --style pojo --feature builder,direct-codec --out "${TMP_DIR}/java_pojo" "${SCHEMA_FILE}" >/dev/null test -f "${TMP_DIR}/java_pojo/TreeNodeCodec.java" if command -v javac >/dev/null 2>&1; then javac -d "${TMP_DIR}/java_classes" "${TMP_DIR}/java_pojo/"*.java @@ -144,13 +144,13 @@ test -f "${TMP_DIR}/cs_class/TestSchema.cs" echo -e "${GREEN}OK${NC}" echo -n " • C# (record struct + source-gen)... " -"${POLYXML_BIN}" generate --lang csharp --record-kind struct --source-gen --namespace Smoke --out "${TMP_DIR}/cs_struct" "${SCHEMA_FILE}" >/dev/null +"${POLYXML_BIN}" generate --lang csharp --style record-struct --backend source-gen --namespace Smoke --out "${TMP_DIR}/cs_struct" "${SCHEMA_FILE}" >/dev/null test -f "${TMP_DIR}/cs_struct/TestSchema.cs" echo -e "${GREEN}OK${NC}" echo -n " • C# (mutable class + source-gen)... " -"${POLYXML_BIN}" generate --lang csharp --style class --source-gen --out "${TMP_DIR}/cs_class" "${SCHEMA_FILE}" >/dev/null +"${POLYXML_BIN}" generate --lang csharp --style class --backend source-gen --out "${TMP_DIR}/cs_class" "${SCHEMA_FILE}" >/dev/null test -f "${TMP_DIR}/cs_class/TestSchema.cs" echo -e "${GREEN}OK${NC}" echo -e "${GREEN}✨ Multi-target smoke verification passed completely across all 7 ecosystems!${NC}" From 2c6e6119d703d1c0c5d03a2652938ff938b6ffb0 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 03:50:00 -0500 Subject: [PATCH 04/10] feat(schema): xsi:type polymorphic dispatch for abstract complexTypes (issue #53) - ModelSchema gains is_abstract plus a variants registry (OnceLock, keyed by type QName local part), populated from XSD extension chains in from_ir and from Python subclass hierarchies in the PyO3 layer. - from_ir flattens base-chain fields into derived runtime schemas (mirroring the Java codegen), so dispatched records keep inherited fields. - Parser dispatches xsi:type at every frame site (root, nested, list, empty, iterparse); unknown selectors on abstract types fail loudly listing known derivations, plain content attributes named "type" never misdispatch. - Serializer re-emits xsi:type for variant records and declares xmlns:xsi whenever dispatch is possible; xml_to_json keeps variant-only fields. - Python codegen emits Meta.abstract; polyxml.serialize grows target_type for base-declared root round trips. - New docs/guides/polymorphism.md documents the strategy, limitations, and escape hatches; core-engine and codegen skills updated. Tests: 7 Rust (test_issue53_xsi_type.rs), 11 Python (test_xsi_type.py at 100% coverage), test_python_abstract_meta_emission, audit item 7 re-pointed. --- .../skills/polyxml-codegen-workflow/SKILL.md | 32 ++ .agents/skills/polyxml-core-engine/SKILL.md | 56 +++ crates/polyxml-core/src/codegen/python/mod.rs | 6 + crates/polyxml-core/src/json.rs | 8 + crates/polyxml-core/src/parser.rs | 88 ++++- crates/polyxml-core/src/schema.rs | 173 ++++++++-- crates/polyxml-core/src/serializer.rs | 56 +++ .../polyxml-core/tests/test_issue51_audit.rs | 4 +- .../tests/test_issue53_xsi_type.rs | 319 ++++++++++++++++++ .../polyxml-core/tests/test_python_codegen.rs | 47 +++ .../polyxml-python/python/polyxml/__init__.py | 15 +- crates/polyxml-python/src/lib.rs | 93 ++++- crates/polyxml-python/tests/test_xsi_type.py | 237 +++++++++++++ docs/guides/polymorphism.md | 145 ++++++++ docs/index.md | 1 + 15 files changed, 1245 insertions(+), 35 deletions(-) create mode 100644 crates/polyxml-core/tests/test_issue53_xsi_type.rs create mode 100644 crates/polyxml-python/tests/test_xsi_type.py create mode 100644 docs/guides/polymorphism.md diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 42829a69..52caeb9a 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -317,3 +317,35 @@ Python/Java/C# validators against values matching only one alternative, values satisfying two restriction steps, and reject lists/optionals carrying a bad element. The TS leg needs `POLYXML_TS_TEST_MODULES` pointing at a node_modules with `zod@3 valibot@1 @sinclair/typebox@0.34 typescript@5` and skips otherwise. + +## 11. Python Abstract Meta & Runtime Type Discovery (issue #53) + +`xsi:type` dispatch is a **dynamic-runtime feature** (see +`polyxml-core-engine` skill §5 and `docs/guides/polymorphism.md`); the +codegen only has to expose two facts to the PyO3 layer: + +- **`Meta.abstract = True`** is emitted for `is_abstract` structs in + `codegen/python/mod.rs` (inside the `emit_meta` block, after + `namespace`). PyO3 reads it in `extract_schema_from_class` → + `builder.is_abstract(true)`, which turns unknown `xsi:type` values into + loud errors instead of silent truncation. Only abstract types may emit + it — locked by `test_python_abstract_meta_emission`. +- **Class inheritance must stay intact**: `emit_struct` already emits + `class Derived(Base):`, and PyO3 builds a derived class's schema from + `__dataclass_fields__`/`model_fields`, which include inherited fields — + do not flatten inheritance away or variant schemas will miss base fields. + +Key gotchas when touching this area: + +- Variant discovery (`discover_variants`) must run **after** both global + cache inserts in `get_or_create_schema_meta`, otherwise a subclass field + typed as the base class re-enters extraction and recurses infinitely. +- Variant keys come from `Meta.name` (the XML type name), not the class + name — codegen may mangle class identifiers (`type_ident`) while + `xsi:type` always carries the wire QName local part. +- `polyxml.serialize` takes an optional **`target_type`** (pyo3 + wrapper): + given the declared base, a concrete instance is converted against the + base schema and the variant's record triggers `xsi:type` re-emission at + the root; omitted, the instance's concrete schema is used (no selector). + Both branches are covered in `tests/test_xsi_type.py` (11 tests, kept at + 100% statement/branch coverage). diff --git a/.agents/skills/polyxml-core-engine/SKILL.md b/.agents/skills/polyxml-core-engine/SKILL.md index 8dab3f92..4e3c5402 100644 --- a/.agents/skills/polyxml-core-engine/SKILL.md +++ b/.agents/skills/polyxml-core-engine/SKILL.md @@ -73,3 +73,59 @@ Key mechanisms (regression-locked in `crates/polyxml-core/tests/test_issue51_aud per-includer via `rekey_to_namespace`/`rekey_new_state`. 7. Parser reuse across `parse_file` calls is supported; groups partially replay from `file_groups` on cache hit. + +## 5. `xsi:type` Polymorphic Dispatch (issue #53) + +The runtime dispatches polymorphic elements through a **type registry on +`ModelSchema`** — never through generated code. Documented for users in +`docs/guides/polymorphism.md`; regression-locked in +`crates/polyxml-core/tests/test_issue53_xsi_type.rs`. + +- **Registry**: `ModelSchema` carries `is_abstract: bool` plus a + `variants: OnceLock>>` (methods: `set_variants`, + `variants`, `has_variants`, `find_variant(local_bytes)`, + `matches_variant(candidate)`). `OnceLock` keeps `ModelSchema`'s `Clone` + derive intact and guarantees each schema registers once. Variants are + keyed by the type QName local part (`xml_name`), which is `Meta.name` on + the Python side and the type local on the XSD side. +- **Population (`schema.rs::from_ir`)**: `build_struct` (a) flattens the + `xs:extension` content model by collecting the base-chain structs + (root-base first, cycle-guarded) and prepending their fields — mirroring + Java codegen's `collect`, so derived runtime schemas carry inherited + fields — and (b) scans the `BTreeMap` of types for transitive + derivations (`derives_from`), building each inside a `visited` + insert/remove guard before `set_variants`. **Python-side population + (pyo3 `lib.rs`)**: after both global caches are inserted in + `get_or_create_schema_meta`, `discover_variants` walks + `__subclasses__()` transitively (ptr-seen set, extracts only + dataclass/Pydantic classes) — running *after* cache insertion so a + subclass field typed as the base resolves from cache instead of + re-entering extraction. +- **Parse (`parser.rs`)**: `resolve_record_schema(declared, start)` runs at + *every* frame creation site — root `Start`/`Empty` in + `deserialize_with_limit`, the root frame in `parse_sub_tree`, both + nested `Start`/`Empty` arms (single and list), and the `XmlItemStream` + `Empty` arm. `xsi_type_value` matches the attribute **local name** + `type` (namespace-blind, like `is_nil_element`). Policy: registered + variant → switch schema; unknown value on `is_abstract` → + `PolyXmlError::SchemaError` listing known variants, or an + "escape hatch" error when none are registered; everything else falls + back to the declared schema. Schemas that are neither abstract nor have + variants return immediately without scanning attributes (zero-overhead + fast path — this is also why a content attribute named `type` can never + be misdispatched on plain types). +- **Serialize (`serializer.rs`)**: `write_model` checks + `schema.matches_variant(rec)` on `Record` values, shadows the effective + schema to the variant, and pushes `xsi:type` (qualified QName via + `qualify_element`, falling back to literal `xsi:type` when namespaces + are disabled) after content attributes. `NamespaceContext::collect_namespaces` + recurses `variants` and injects the XSI URI so `xmlns:xsi` is always + declared at the root whenever dispatch is possible. +- **JSON (`json.rs`)**: `poly_value_to_json_value` prefers the record's + own schema when its name differs from the declared one, so variant-only + fields survive `xml_to_json` (JSON carries no selector marker). +- **Escape hatches**: generated static codecs (Rust serde, Java) do *not* + dispatch — Java's codec errors loudly; deserialize the concrete type or + use the dynamic runtime. `polyxml-js` bindings are scalar-flat today, so + dispatch is unreachable from JS until nested schemas land (shared core + means no future binding work). diff --git a/crates/polyxml-core/src/codegen/python/mod.rs b/crates/polyxml-core/src/codegen/python/mod.rs index 23a7dc8f..f53f04cb 100644 --- a/crates/polyxml-core/src/codegen/python/mod.rs +++ b/crates/polyxml-core/src/codegen/python/mod.rs @@ -568,6 +568,12 @@ impl PythonCodegen { if let Some(ref ns) = s.qname.namespace { let _ = writeln!(out, " namespace = \"{}\"", ns); } + // Marks the type as a dispatch base: the runtime raises a clear + // error when xsi:type targets an abstract type with no registered + // derivations (issue #53). + if s.is_abstract { + out.push_str(" abstract = True\n"); + } has_body = true; } diff --git a/crates/polyxml-core/src/json.rs b/crates/polyxml-core/src/json.rs index 8773eed9..687a75b1 100644 --- a/crates/polyxml-core/src/json.rs +++ b/crates/polyxml-core/src/json.rs @@ -76,6 +76,14 @@ pub fn poly_value_to_json_value( schema: &ModelSchema, by_alias: bool, ) -> JsonValue { + // xsi:type dispatch (issue #53): a Record parsed as a concrete + // derivation iterates its own schema so variant-only fields survive + // transcoding (JSON carries no xsi:type marker). + let schema = match val { + PolyValue::Record { schema: rec, .. } if rec.name != schema.name => rec.as_ref(), + _ => schema, + }; + let get_field = |idx: usize, name: &str| -> Option<&PolyValue> { match val { PolyValue::Record { values, .. } => values.get(idx).and_then(|v| v.as_ref()), diff --git a/crates/polyxml-core/src/parser.rs b/crates/polyxml-core/src/parser.rs index 24e1e5b2..18615ae9 100644 --- a/crates/polyxml-core/src/parser.rs +++ b/crates/polyxml-core/src/parser.rs @@ -101,6 +101,73 @@ fn is_nil_element(e: &BytesStart) -> bool { false } +/// Raw unescaped value of the `xsi:type` attribute on `e`, if present. +/// +/// Matching follows this parser's namespace-blind convention (like +/// `is_nil_element`): the attribute's local name must be `type`, so +/// `xsi:type`, any other prefixed `*:type`, or a bare `type` all match. +fn xsi_type_value(e: &BytesStart) -> Result>> { + for attr in e.attributes().flatten() { + let raw_key = attr.key.as_ref(); + if raw_key == "xmlns" || raw_key.starts_with("xmlns:") { + continue; + } + if attr.key.local_name().as_ref() == "type" { + let unescaped = quick_xml::escape::unescape(attr.value.as_ref())?; + return Ok(Some(unescaped.into_owned().into_bytes())); + } + } + Ok(None) +} + +/// Resolve the concrete schema an element should be parsed with (issue #53). +/// +/// When the declared type has `xsi:type` derivations registered (or is +/// abstract), the wire attribute selects one; unknown types on an abstract +/// base error loudly instead of silently dropping concrete fields. Types +/// without dispatch information keep today's behavior of parsing as +/// declared, so a plain content attribute named `type` is never mistaken +/// for dispatch on a non-abstract type. +fn resolve_record_schema(declared: &Arc, e: &BytesStart) -> Result> { + if !declared.is_abstract && !declared.has_variants() { + return Ok(Arc::clone(declared)); + } + let raw = match xsi_type_value(e)? { + Some(raw) => raw, + None => return Ok(Arc::clone(declared)), + }; + let local: &[u8] = match raw.iter().rposition(|&b| b == b':') { + Some(i) => &raw[i + 1..], + None => &raw[..], + }; + if let Some(variant) = declared.find_variant(local) { + return Ok(variant); + } + if declared.is_abstract { + let value = String::from_utf8_lossy(&raw); + return Err(PolyXmlError::SchemaError(if declared.has_variants() { + let known: Vec = declared + .variants() + .iter() + .map(|v| String::from_utf8_lossy(&v.xml_name).into_owned()) + .collect(); + format!( + "xsi:type=\"{}\" does not match any known derivation of '{}' (known: {})", + value, + declared.name, + known.join(", ") + ) + } else { + format!( + "xsi:type=\"{}\" targets abstract type '{}', which has no registered \ + derivations; deserialize the concrete type directly (escape hatch)", + value, declared.name + ) + })); + } + Ok(Arc::clone(declared)) +} + fn append_general_ref( e: &quick_xml::events::BytesRef, active_scalar: bool, @@ -149,7 +216,8 @@ impl XmlDeserializer { return Self::parse_sub_tree(&mut reader, root_schema, e, max_depth); } Ok(Event::Empty(ref e)) => { - let mut frame = StackFrame::new(Arc::clone(&root_schema)); + let schema = resolve_record_schema(&root_schema, e)?; + let mut frame = StackFrame::new(schema); Self::parse_attributes(e, &mut frame)?; return frame.finish(); } @@ -177,7 +245,8 @@ impl XmlDeserializer { max_depth: usize, ) -> Result { let mut stack: Vec = Vec::with_capacity(16); - let mut root_frame = StackFrame::new(Arc::clone(&root_schema)); + let root_schema = resolve_record_schema(&root_schema, root_start)?; + let mut root_frame = StackFrame::new(root_schema); Self::parse_attributes(root_start, &mut root_frame)?; stack.push(root_frame); @@ -235,14 +304,16 @@ impl XmlDeserializer { } } ValueType::Nested(sub_schema) => { - let mut frame = StackFrame::new(Arc::clone(sub_schema)); + let frame_schema = resolve_record_schema(sub_schema, e)?; + let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; stack.push(frame); } _ => {} }, ValueType::Nested(sub_schema) => { - let mut frame = StackFrame::new(Arc::clone(sub_schema)); + let frame_schema = resolve_record_schema(sub_schema, e)?; + let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; stack.push(frame); } @@ -283,7 +354,8 @@ impl XmlDeserializer { stack.last_mut().unwrap().push_list_item(field_idx, val); } ValueType::Nested(sub_schema) => { - let mut frame = StackFrame::new(Arc::clone(sub_schema)); + let frame_schema = resolve_record_schema(sub_schema, e)?; + let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; let instance = frame.finish()?; stack @@ -294,7 +366,8 @@ impl XmlDeserializer { _ => {} }, ValueType::Nested(sub_schema) => { - let mut frame = StackFrame::new(Arc::clone(sub_schema)); + let frame_schema = resolve_record_schema(sub_schema, e)?; + let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; let instance = frame.finish()?; stack.last_mut().unwrap().values[field_idx] = Some(instance); @@ -475,7 +548,8 @@ impl XmlItemStream { if local.as_ref().as_bytes() == target_local || e.name().as_ref().as_bytes() == self.target_tag.as_slice() { - let mut frame = StackFrame::new(Arc::clone(&self.schema)); + let schema = resolve_record_schema(&self.schema, e)?; + let mut frame = StackFrame::new(schema); XmlDeserializer::parse_attributes(e, &mut frame)?; return Ok(Some(frame.finish()?)); } diff --git a/crates/polyxml-core/src/schema.rs b/crates/polyxml-core/src/schema.rs index cebf0fa5..7b632572 100644 --- a/crates/polyxml-core/src/schema.rs +++ b/crates/polyxml-core/src/schema.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum FieldKind { @@ -80,6 +80,13 @@ pub struct ModelSchema { pub element_map: HashMap, usize>, pub attribute_map: HashMap, usize>, pub text_field: Option, + /// Declared `abstract="true"` in the source schema (issue #53). + pub is_abstract: bool, + /// Concrete derivations eligible for `xsi:type` dispatch, each keyed by + /// its type QName local part (stored in `xml_name`). Set once after the + /// schema is created: from `SchemaIR` extension chains in `from_ir`, or + /// from Python subclass hierarchies in the PyO3 layer. + variants: OnceLock>>, } impl ModelSchema { @@ -87,6 +94,38 @@ impl ModelSchema { ModelSchemaBuilder::new(name) } + /// Register the concrete derivations eligible for `xsi:type` dispatch. + /// Ignored on a second call (`OnceLock` semantics). + pub fn set_variants(&self, variants: Vec>) { + let _ = self.variants.set(variants); + } + + /// Registered derivations for `xsi:type` dispatch (empty when none). + pub fn variants(&self) -> &[Arc] { + self.variants.get().map(Vec::as_slice).unwrap_or(&[]) + } + + /// Whether any `xsi:type` derivations are registered for this type. + pub fn has_variants(&self) -> bool { + !self.variants().is_empty() + } + + /// Look up a derivation by the QName local part of an `xsi:type` value. + pub fn find_variant(&self, local: &[u8]) -> Option> { + self.variants() + .iter() + .find(|v| v.xml_name.as_slice() == local) + .cloned() + } + + /// Whether `candidate` is a registered derivation of `self`. + pub fn matches_variant(&self, candidate: &ModelSchema) -> bool { + self.variants().iter().any(|v| { + std::ptr::eq(v.as_ref(), candidate) + || (v.xml_name == candidate.xml_name && v.namespace == candidate.namespace) + }) + } + /// Construct a runtime `ModelSchema` from a compiled `SchemaIR`. pub fn from_ir( ir: &crate::ir::SchemaIR, @@ -207,6 +246,56 @@ impl ModelSchema { } } + fn build_field( + f: &crate::ir::FieldDef, + ir: &crate::ir::SchemaIR, + visited: &mut HashSet, + ) -> FieldSchema { + let kind = match f.kind { + crate::ir::FieldKind::Attribute => FieldKind::Attribute, + crate::ir::FieldKind::Element => FieldKind::Element, + crate::ir::FieldKind::Text => FieldKind::Text, + _ => FieldKind::Element, + }; + + let mut val_type = build_type(&f.type_ref, ir, visited); + if f.cardinality.is_list() && !matches!(val_type, ValueType::List(_)) { + val_type = ValueType::List(Box::new(val_type)); + } + + let mut field_schema = FieldSchema::new(&f.name, f.xml_name.as_bytes(), kind, val_type); + if let Some(ref ns) = f.namespace { + field_schema = field_schema.namespace(ns); + } + if f.cardinality.min_occurs > 0 && !f.cardinality.is_optional() { + field_schema = field_schema.required(); + } + field_schema + } + + /// Whether `d` transitively extends `base` via `xs:extension` (issue #53). + fn derives_from( + ir: &crate::ir::SchemaIR, + d: &crate::ir::StructDef, + base: &crate::ir::QName, + ) -> bool { + let mut seen: HashSet = HashSet::new(); + let mut cur = d.base_type.as_ref(); + while let Some(q) = cur { + if q == base { + return true; + } + if !seen.insert(q.clone()) { + break; + } + cur = match ir.types.get(q) { + Some(TypeDef::Struct(b)) => b.base_type.as_ref(), + _ => None, + }; + } + false + } + fn build_struct( s: &crate::ir::StructDef, ir: &crate::ir::SchemaIR, @@ -216,33 +305,65 @@ impl ModelSchema { if let Some(ref ns) = s.qname.namespace { builder = builder.namespace(ns); } - + builder = builder.is_abstract(s.is_abstract); + + // Flatten the xs:extension content model: base-chain fields + // precede the struct's own fields (mirrors the Java codegen). + let mut seen_bases: HashSet = HashSet::new(); + seen_bases.insert(s.qname.clone()); + let mut chain: Vec<&crate::ir::StructDef> = Vec::new(); + let mut cur = s.base_type.as_ref(); + while let Some(base_q) = cur { + if !seen_bases.insert(base_q.clone()) { + break; + } + match ir.types.get(base_q) { + Some(TypeDef::Struct(base_s)) => { + chain.push(base_s); + cur = base_s.base_type.as_ref(); + } + _ => break, + } + } + for base_s in chain.into_iter().rev() { + for f in &base_s.fields { + builder = builder.field(build_field(f, ir, visited)); + } + } for f in &s.fields { - let kind = match f.kind { - crate::ir::FieldKind::Attribute => FieldKind::Attribute, - crate::ir::FieldKind::Element => FieldKind::Element, - crate::ir::FieldKind::Text => FieldKind::Text, - _ => FieldKind::Element, - }; + builder = builder.field(build_field(f, ir, visited)); + } - let mut val_type = build_type(&f.type_ref, ir, visited); - if f.cardinality.is_list() && !matches!(val_type, ValueType::List(_)) { - val_type = ValueType::List(Box::new(val_type)); - } + let schema = builder.build(); - let mut field_schema = - FieldSchema::new(&f.name, f.xml_name.as_bytes(), kind, val_type); - if let Some(ref ns) = f.namespace { - field_schema = field_schema.namespace(ns); + // xsi:type dispatch registry (issue #53): every transitive + // derivation of this type, keyed by QName local part. + let derived: Vec = ir + .types + .iter() + .filter_map(|(q, td)| match td { + TypeDef::Struct(d) if d.qname != s.qname && derives_from(ir, d, &s.qname) => { + Some(q.clone()) + } + _ => None, + }) + .collect(); + let mut variants = Vec::with_capacity(derived.len()); + for dq in derived { + if visited.contains(&dq) { + continue; } - if f.cardinality.min_occurs > 0 && !f.cardinality.is_optional() { - field_schema = field_schema.required(); + visited.insert(dq.clone()); + if let Some(TypeDef::Struct(d)) = ir.types.get(&dq) { + variants.push(build_struct(d, ir, visited)); } - - builder = builder.field(field_schema); + visited.remove(&dq); + } + if !variants.is_empty() { + schema.set_variants(variants); } - builder.build() + schema } let mut visited = HashSet::new(); @@ -284,6 +405,7 @@ pub struct ModelSchemaBuilder { xml_name: Option>, namespace: Option, fields: Vec, + is_abstract: bool, } impl ModelSchemaBuilder { @@ -293,9 +415,16 @@ impl ModelSchemaBuilder { xml_name: None, namespace: None, fields: Vec::new(), + is_abstract: false, } } + /// Mark the type as `abstract="true"` (issue #53). + pub fn is_abstract(mut self, is_abstract: bool) -> Self { + self.is_abstract = is_abstract; + self + } + pub fn xml_name(mut self, xml_name: &[u8]) -> Self { self.xml_name = Some(xml_name.to_vec()); self @@ -353,6 +482,8 @@ impl ModelSchemaBuilder { element_map, attribute_map, text_field, + is_abstract: self.is_abstract, + variants: OnceLock::new(), }) } } diff --git a/crates/polyxml-core/src/serializer.rs b/crates/polyxml-core/src/serializer.rs index 494286c4..bf4565fa 100644 --- a/crates/polyxml-core/src/serializer.rs +++ b/crates/polyxml-core/src/serializer.rs @@ -9,6 +9,9 @@ use crate::error::{PolyXmlError, Result}; use crate::schema::{FieldKind, ModelSchema, ValueType}; use crate::value::PolyValue; +/// XML Schema Instance namespace, needed to write `xsi:type` selectors. +const XSI_NS: &str = "http://www.w3.org/2001/XMLSchema-instance"; + #[derive(Debug, Clone)] pub struct NamespaceContext { pub uri_to_prefix: HashMap, @@ -103,6 +106,19 @@ impl NamespaceContext { _ => {} } } + + // xsi:type dispatch (issue #53): variant namespaces must be in + // scope, and the XML Schema Instance namespace is required to + // write the xsi:type selector itself. + let variants = schema.variants(); + if !variants.is_empty() { + if !uris.iter().any(|u| u == XSI_NS) { + uris.push(XSI_NS.to_string()); + } + for variant in variants { + Self::collect_namespaces(variant, uris); + } + } } pub fn qualify_element<'a>(&'a self, local_name: &'a str, ns: Option<&str>) -> Cow<'a, str> { @@ -221,6 +237,30 @@ impl XmlSerializer { } } + // xsi:type dispatch (issue #53): when the value is a Record built + // from a registered derivation of the declared schema, write the + // concrete variant's fields and re-emit the xsi:type selector. + let mut xsi_type: Option = None; + let schema = if let PolyValue::Record { schema: rec, .. } = value { + if schema.matches_variant(rec) { + xsi_type = match ns_ctx { + Some(ctx) => { + let name = std::str::from_utf8(&rec.xml_name)?; + Some( + ctx.qualify_element(name, rec.namespace.as_deref()) + .into_owned(), + ) + } + None => Some(String::from_utf8_lossy(&rec.xml_name).into_owned()), + }; + rec.as_ref() + } else { + schema + } + } else { + schema + }; + let get_field = |idx: usize, name: &str| -> Option<&PolyValue> { match value { PolyValue::Record { values, .. } => values.get(idx).and_then(|v| v.as_ref()), @@ -272,6 +312,22 @@ impl XmlSerializer { } } + // Re-emit the xsi:type selector after content attributes (issue #53). + if let Some(ref xsi_val) = xsi_type { + let attr_key = match ns_ctx { + Some(ctx) => { + let key = ctx.qualify_attribute("type", Some(XSI_NS)); + if key.as_ref() == "type" { + "xsi:type".to_string() + } else { + key.into_owned() + } + } + None => "xsi:type".to_string(), + }; + elem.push_attribute((attr_key.as_str(), xsi_val.as_str())); + } + writer .write_event(Event::Start(elem)) .map_err(|e| PolyXmlError::SerializationError(e.to_string()))?; diff --git a/crates/polyxml-core/tests/test_issue51_audit.rs b/crates/polyxml-core/tests/test_issue51_audit.rs index 3a910084..664a905c 100644 --- a/crates/polyxml-core/tests/test_issue51_audit.rs +++ b/crates/polyxml-core/tests/test_issue51_audit.rs @@ -394,8 +394,8 @@ fn issue51_item6_quick_xml_serde_skips_unknown_elements() { } // --------------------------------------------------------------------------- -// Item 7: xsi:type polymorphic dispatch — intentionally untested here -// (scoped as a separate enhancement issue; unsupported today). +// Item 7: xsi:type polymorphic dispatch — implemented as issue #53; +// see `tests/test_issue53_xsi_type.rs`. // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- diff --git a/crates/polyxml-core/tests/test_issue53_xsi_type.rs b/crates/polyxml-core/tests/test_issue53_xsi_type.rs new file mode 100644 index 00000000..238a83a7 --- /dev/null +++ b/crates/polyxml-core/tests/test_issue53_xsi_type.rs @@ -0,0 +1,319 @@ +//! Issue #53: `xsi:type` polymorphic dispatch for abstract complexTypes. +//! +//! Locks in the runtime registry strategy: `ModelSchema` carries the +//! concrete derivations of a type (keyed by QName local part), the parser +//! selects one from the wire `xsi:type` attribute, and the serializer +//! re-emits the selector so round trips preserve the concrete type. + +use std::io::Cursor; +use std::sync::Arc; + +use polyxml::schema::{ModelSchema, ValueType}; +use polyxml::schema_parser::XsdParser; +use polyxml::value::PolyValue; +use polyxml::XmlItemStream; + +const VEHICLE_XSD: &str = r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +"#; + +fn schema_for(root: Option<&str>) -> Arc { + let ir = XsdParser::new().parse_str(VEHICLE_XSD).expect("parse XSD"); + ModelSchema::from_ir(&ir, root).expect("build runtime schema") +} + +/// Declared schema of the `Root` document's `vehicle` field. +fn declared_vehicle(root: &Arc) -> Arc { + let idx = root + .fields + .iter() + .position(|f| f.name == "vehicle") + .expect("vehicle field"); + match &root.fields[idx].val_type { + ValueType::Nested(s) => Arc::clone(s), + other => panic!("expected nested vehicle schema, got {other:?}"), + } +} + +fn rec_field<'a>(value: &'a PolyValue, field_name: &str) -> Option<&'a PolyValue> { + match value { + PolyValue::Record { schema, values } => { + let idx = schema.fields.iter().position(|f| f.name == field_name)?; + values.get(idx).and_then(|v| v.as_ref()) + } + _ => None, + } +} + +fn rec_schema(value: &PolyValue) -> &ModelSchema { + match value { + PolyValue::Record { schema, .. } => schema, + other => panic!("expected Record, got {other:?}"), + } +} + +fn assert_str(value: Option<&PolyValue>, expected: &str, ctx: &str) { + match value { + Some(PolyValue::String(s)) => assert_eq!(s, expected, "wrong value for {ctx}"), + other => panic!("expected string {expected:?} for {ctx}, got {other:?}"), + } +} + +fn schema_local(schema: &ModelSchema) -> String { + String::from_utf8_lossy(&schema.xml_name).into_owned() +} + +#[test] +fn issue53_from_ir_flattens_base_fields_and_registers_variants() { + let root = schema_for(Some("Root")); + let declared = declared_vehicle(&root); + + assert!(declared.is_abstract, "Vehicle must be marked abstract"); + assert!( + declared.has_variants(), + "Vehicle must register its derivations" + ); + let keys: Vec = declared + .variants() + .iter() + .map(|v| schema_local(v)) + .collect(); + assert_eq!(keys, ["Car", "Truck"], "variant keys by QName local part"); + + // The declared (abstract) type keeps only its own content model. + let base_fields: Vec<&str> = declared.fields.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(base_fields, ["id"], "abstract base keeps only base fields"); + + // Derived runtime schemas flatten the extension chain (base first). + let car = declared.find_variant(b"Car").expect("Car variant"); + let car_fields: Vec<&str> = car.fields.iter().map(|f| f.name.as_str()).collect(); + assert_eq!( + car_fields, + ["id", "doors"], + "Car must inherit id before its own doors" + ); + assert!(!car.is_abstract, "concrete derivation is not abstract"); + + let truck = declared.find_variant(b"Truck").expect("Truck variant"); + let truck_fields: Vec<&str> = truck.fields.iter().map(|f| f.name.as_str()).collect(); + assert_eq!(truck_fields, ["id", "payload"]); +} + +#[test] +fn issue53_nested_xsi_type_dispatch_and_round_trip() { + let root = schema_for(Some("Root")); + let xml = br#" + V14 + S1ore + + "#; + + let value = polyxml::deserialize(xml, Arc::clone(&root)).expect("dispatch parse"); + + let vehicle = rec_field(&value, "vehicle").expect("vehicle value"); + assert_eq!(schema_local(rec_schema(vehicle)), "Car"); + assert_str(rec_field(vehicle, "id"), "V1", "vehicle.id (inherited)"); + match rec_field(vehicle, "doors") { + Some(PolyValue::Int(i)) => assert_eq!(*i, 4, "vehicle.doors"), + other => panic!("expected int doors, got {other:?}"), + } + + let spares = rec_field(&value, "spare").expect("spare list"); + let items = match spares { + PolyValue::List(items) => items, + other => panic!("expected spare list, got {other:?}"), + }; + assert_eq!(items.len(), 2); + assert_eq!(schema_local(rec_schema(&items[0])), "Truck"); + assert_str( + rec_field(&items[0], "payload"), + "ore", + "spare.payload (inherited nesting)", + ); + assert_eq!(schema_local(rec_schema(&items[1])), "Car"); + + // Round trip: xsi:type selectors are re-emitted for every dispatch. + let out = polyxml::serialize_with_options("Root", &value, &root, None, Some(true), None) + .expect("serialize"); + let out_str = String::from_utf8_lossy(&out); + assert!(out_str.contains("xsi:type="), "missing selector: {out_str}"); + assert!(out_str.contains("Car"), "missing Car selector: {out_str}"); + assert!( + out_str.contains("Truck"), + "missing Truck selector: {out_str}" + ); + assert!( + out_str.contains("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""), + "missing xsi declaration: {out_str}" + ); + + let reparsed = polyxml::deserialize(&out, Arc::clone(&root)).expect("re-parse"); + assert_eq!( + reparsed, value, + "round trip must preserve dispatched records" + ); +} + +#[test] +fn issue53_root_xsi_type_dispatch_including_empty_element() { + let root = schema_for(Some("Vehicle")); + assert!(root.is_abstract); + assert!(root.has_variants()); + + let xml = br#"T9ore"#; + let value = polyxml::deserialize(xml, Arc::clone(&root)).expect("root dispatch"); + assert_eq!(schema_local(rec_schema(&value)), "Truck"); + assert_str(rec_field(&value, "id"), "T9", "root id"); + assert_str(rec_field(&value, "payload"), "ore", "root payload"); + + let out = polyxml::serialize_with_options("Vehicle", &value, &root, None, Some(true), None) + .expect("serialize root"); + let out_str = String::from_utf8_lossy(&out); + assert!( + out_str.starts_with(""#; + let value = polyxml::deserialize(empty, root).expect("empty dispatch"); + assert_eq!(schema_local(rec_schema(&value)), "Car"); +} + +#[test] +fn issue53_unknown_xsi_type_on_abstract_type_errors() { + let root = schema_for(Some("Root")); + let xml = br#"X"#; + let err = polyxml::deserialize(xml, root).expect_err("unknown derivation must fail"); + let msg = err.to_string(); + assert!( + msg.contains("xsi:type=\"t:Plane\"") && msg.contains("known derivation"), + "error must name the offending selector: {msg}" + ); + assert!( + msg.contains("'Vehicle'") && msg.contains("Car, Truck"), + "error must name the base and known derivations: {msg}" + ); +} + +#[test] +fn issue53_abstract_type_without_derivations_errors() { + let xsd = r#" + + + + + + + "#; + let ir = XsdParser::new().parse_str(xsd).expect("parse XSD"); + let root = ModelSchema::from_ir(&ir, Some("Solo")).expect("build runtime schema"); + assert!(root.is_abstract); + assert!(!root.has_variants()); + + let xml = br#"X"#; + let err = polyxml::deserialize(xml, root).expect_err("must fail loudly"); + let msg = err.to_string(); + assert!( + msg.contains("no registered derivations") && msg.contains("escape hatch"), + "error must explain the escape hatch: {msg}" + ); +} + +#[test] +fn issue53_plain_type_attribute_is_not_mistaken_for_dispatch() { + // A content attribute literally named "type" on a non-abstract type + // must never be treated as an xsi:type selector. + let xsd = r#" + + + + + + + + "#; + let ir = XsdParser::new().parse_str(xsd).expect("parse XSD"); + let root = ModelSchema::from_ir(&ir, Some("Tagged")).expect("build runtime schema"); + + let xml = br#""#; + let value = polyxml::deserialize(xml, Arc::clone(&root)).expect("parse as declared"); + assert_str(rec_field(&value, "type"), "gear", "content attribute"); + assert_str(rec_field(&value, "label"), "L", "label"); + + let out = polyxml::serialize_with_options("Tagged", &value, &root, None, None, None) + .expect("serialize"); + let out_str = String::from_utf8_lossy(&out); + assert!( + !out_str.contains("xsi:type="), + "no selector may be invented for plain types: {out_str}" + ); +} + +#[test] +fn issue53_iterparse_dispatches_each_streamed_item() { + let root = schema_for(Some("Root")); + let declared = declared_vehicle(&root); + let xml = br#" + A2 + Bp + + "#; + + let mut stream = XmlItemStream::new(Cursor::new(&xml[..]), declared, b"vehicle"); + let first = stream.next_item().expect("first item").expect("item 1"); + assert_eq!(schema_local(rec_schema(&first)), "Car"); + match rec_field(&first, "doors") { + Some(PolyValue::Int(i)) => assert_eq!(*i, 2, "streamed doors"), + other => panic!("expected int doors, got {other:?}"), + } + + let second = stream.next_item().expect("second item").expect("item 2"); + assert_eq!(schema_local(rec_schema(&second)), "Truck"); + + let third = stream.next_item().expect("third item").expect("item 3"); + assert_eq!(schema_local(rec_schema(&third)), "Car"); + + assert!(stream.next_item().expect("eof").is_none()); +} diff --git a/crates/polyxml-core/tests/test_python_codegen.rs b/crates/polyxml-core/tests/test_python_codegen.rs index 1878d6fa..c13871d8 100644 --- a/crates/polyxml-core/tests/test_python_codegen.rs +++ b/crates/polyxml-core/tests/test_python_codegen.rs @@ -434,3 +434,50 @@ fn test_python_generated_tag_and_custom_header() { assert!(custom_code .starts_with("# ruff: noqa\n# mypy: ignore-errors\n\n# @generated by PolyXML Compiler")); } + +#[test] +fn test_python_abstract_meta_emission() { + // Issue #53: abstract complexTypes mark Meta.abstract so the runtime + // can raise a clear error for xsi:type values with no derivations. + let mut ir = SchemaIR::new().with_target_namespace("urn:veh"); + ir.add_type(TypeDef::Struct(StructDef { + qname: QName::new(Some("urn:veh"), "Vehicle"), + base_type: None, + is_abstract: true, + fields: vec![FieldDef::new( + "id", + "id", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::String), + )], + documentation: None, + })); + ir.add_type(TypeDef::Struct(StructDef { + qname: QName::new(Some("urn:veh"), "Car"), + base_type: Some(QName::new(Some("urn:veh"), "Vehicle")), + is_abstract: false, + fields: vec![FieldDef::new( + "doors", + "doors", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::Int), + )], + documentation: None, + })); + + let code = PythonCodegen::new(PythonOptions::default()).generate_module(&ir); + + assert!( + code.contains("class Vehicle:"), + "abstract base must still emit:\n{code}" + ); + assert_eq!( + code.matches("abstract = True").count(), + 1, + "only the abstract type may set Meta.abstract:\n{code}" + ); + assert!( + code.contains("class Car(Vehicle):"), + "concrete derivation must inherit the base:\n{code}" + ); +} diff --git a/crates/polyxml-python/python/polyxml/__init__.py b/crates/polyxml-python/python/polyxml/__init__.py index ee4aad53..1c3c5af7 100644 --- a/crates/polyxml-python/python/polyxml/__init__.py +++ b/crates/polyxml-python/python/polyxml/__init__.py @@ -142,6 +142,7 @@ def iterparse[T]( def serialize( obj: object, *, + target_type: type[object] | None = None, indent: int | None = None, namespaces: bool | None = None, ns_map: dict[str, str] | None = None, @@ -150,6 +151,12 @@ def serialize( Args: obj: Python dataclass or Pydantic model instance. + target_type: Optional declared base type for ``xsi:type`` polymorphic + dispatch. When given, a concrete subclass instance is serialized + using the base type's element name and re-emits an ``xsi:type`` + selector naming the concrete type, so polymorphic round trips + preserve the wire form. When None (default), the instance's own + concrete type is used. indent: Optional indentation size in spaces for pretty-printing. namespaces: Optional boolean toggle to enable or disable XML namespace prefix resolution and root xmlns attribute generation. When None (default), @@ -160,7 +167,13 @@ def serialize( Returns: UTF-8 encoded XML bytes representing the model instance. """ - return _serialize(obj, indent=indent, namespaces=namespaces, ns_map=ns_map) + return _serialize( + obj, + target_type=target_type, + indent=indent, + namespaces=namespaces, + ns_map=ns_map, + ) def serialize_json( diff --git a/crates/polyxml-python/src/lib.rs b/crates/polyxml-python/src/lib.rs index bdd6b798..9d2137a7 100644 --- a/crates/polyxml-python/src/lib.rs +++ b/crates/polyxml-python/src/lib.rs @@ -6,7 +6,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyTuple, PyType}; use pyo3::IntoPyObjectExt; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType}; @@ -228,6 +228,13 @@ fn extract_schema_from_class<'py>( } } } + // Issue #53: abstract types raise a clear error when xsi:type names + // a derivation the runtime does not know. + if let Ok(abstract_val) = meta_cls.getattr("abstract") { + if abstract_val.extract::().unwrap_or(false) { + builder = builder.is_abstract(true); + } + } } let mut cached_fields = Vec::new(); @@ -432,9 +439,56 @@ fn get_or_create_schema_meta<'py>(cls: &Bound<'py, PyType>) -> PyResult) -> Vec> { + let mut out = Vec::new(); + let mut seen: HashSet = HashSet::new(); + collect_subclass_schemas(cls, &mut out, &mut seen); + out +} + +fn collect_subclass_schemas( + cls: &Bound<'_, PyType>, + out: &mut Vec>, + seen: &mut HashSet, +) { + let Ok(subclasses) = cls.call_method0("__subclasses__") else { + return; + }; + let Ok(items) = subclasses.try_iter() else { + return; + }; + for item in items.flatten() { + let Ok(sub) = item.cast::() else { + continue; + }; + if !seen.insert(sub.as_ptr() as usize) { + continue; + } + let usable = sub.hasattr("__dataclass_fields__").unwrap_or(false) + || sub.hasattr("model_fields").unwrap_or(false); + if usable { + if let Ok(meta) = get_or_create_schema_meta(sub) { + out.push(Arc::clone(&meta.schema)); + } + } + collect_subclass_schemas(sub, out, seen); + } +} + fn get_or_create_schema<'py>(cls: &Bound<'py, PyType>) -> PyResult<(Arc, PyObject)> { let meta = get_or_create_schema_meta(cls)?; Ok((Arc::clone(&meta.schema), meta.py_cls.clone_ref(cls.py()))) @@ -582,6 +636,19 @@ fn poly_value_to_py<'py>( schema: rec_schema, values, } => { + // xsi:type dispatch (issue #53): the record was parsed as a + // concrete derivation of the declared type, so construct it with + // the derivation's Python class. + if let ValueType::Nested(ref declared) = val_type { + if declared.matches_variant(rec_schema) { + if let Some(variant_meta) = lookup_cached_meta(&rec_schema.name) { + let variant_cls = variant_meta.py_cls.bind(py).clone(); + let variant_vt = ValueType::Nested(Arc::clone(rec_schema)); + return poly_value_to_py(py, val, &variant_vt, Some(&variant_cls), None); + } + } + } + let meta_opt = if let ValueType::Nested(ref s) = val_type { lookup_cached_meta(&s.name) } else { @@ -856,6 +923,18 @@ fn py_to_poly_value<'py>( meta_opt: Option<&CachedSchemaMeta>, ) -> PyResult { if let Some(meta) = meta_opt { + // xsi:type dispatch (issue #53): a concrete subclass instance in a + // field declared as the base type builds the derivation's record so + // the serializer re-emits xsi:type and keeps every concrete field. + let obj_type = obj.get_type(); + if obj_type.as_ptr() != meta.py_cls.as_ptr() { + if let Ok(actual_meta) = get_or_create_schema_meta(&obj_type) { + if meta.schema.matches_variant(&actual_meta.schema) { + return py_to_poly_value(py, obj, &actual_meta.schema, Some(&actual_meta)); + } + } + } + let mut values: Vec> = vec![None; schema.fields.len()]; for (i, field) in schema.fields.iter().enumerate() { @@ -1101,16 +1180,22 @@ impl XmlIterator { } #[pyfunction] -#[pyo3(signature = (obj, indent=None, namespaces=None, ns_map=None))] +#[pyo3(signature = (obj, target_type=None, indent=None, namespaces=None, ns_map=None))] fn serialize<'py>( py: Python<'py>, obj: Bound<'py, PyAny>, + target_type: Option>, indent: Option, namespaces: Option, ns_map: Option>, ) -> PyResult> { - let cls = obj.get_type(); - let meta = get_or_create_schema_meta(&cls)?; + // With `target_type`, serialize against the declared base type so + // xsi:type dispatch round-trips the element name (issue #53); without + // it, the instance's own concrete type is used. + let meta = match &target_type { + Some(t) => get_or_create_schema_meta(t)?, + None => get_or_create_schema_meta(&obj.get_type())?, + }; let poly_val = py_to_poly_value(py, &obj, &meta.schema, Some(&meta))?; let root_name = std::str::from_utf8(&meta.schema.xml_name).unwrap_or(meta.schema.name.as_str()); diff --git a/crates/polyxml-python/tests/test_xsi_type.py b/crates/polyxml-python/tests/test_xsi_type.py new file mode 100644 index 00000000..70df2f18 --- /dev/null +++ b/crates/polyxml-python/tests/test_xsi_type.py @@ -0,0 +1,237 @@ +"""Issue #53: xsi:type polymorphic dispatch through the Python runtime. + +Covers both directions: parsing wire xsi:type selectors into concrete +subclasses (discovered from the Python class hierarchy), and re-emitting +those selectors on serialize so round trips preserve the concrete type. +""" + +import json +from dataclasses import dataclass, field + +import pytest +from pydantic import BaseModel, Field + +import polyxml + +# --------------------------------------------------------------- +# Dataclass fixtures: abstract Vehicle base with Car/Truck derivations +# --------------------------------------------------------------- + + +@dataclass +class Vehicle: + class Meta: + name = "Vehicle" + abstract = True + + id: str = field(metadata={"type": "Element", "name": "id"}) + + +@dataclass +class Car(Vehicle): + class Meta: + name = "Car" + + doors: int = field(metadata={"type": "Element", "name": "doors"}) + + +@dataclass +class Truck(Vehicle): + class Meta: + name = "Truck" + + payload: str = field(metadata={"type": "Element", "name": "payload"}) + + +@dataclass +class Fleet: + class Meta: + name = "Fleet" + + vehicle: Vehicle = field( + metadata={"type": "Element", "name": "vehicle"}, + default_factory=Vehicle, + ) + spares: list[Vehicle] = field( + metadata={"type": "Element", "name": "spare"}, + default_factory=list, + ) + + +@dataclass +class LonelyBase: + class Meta: + name = "LonelyBase" + abstract = True + + id: str = field(metadata={"type": "Element", "name": "id"}) + + +@dataclass +class Tagged: + class Meta: + name = "Tagged" + + label: str = field(metadata={"type": "Element", "name": "label"}) + type: str = field(metadata={"type": "Attribute", "name": "type"}, default="") + + +FLEET_XML = ( + b"" + b'V14' + b'S1ore' + b'S22' + b"" +) + + +def make_fleet() -> Fleet: + return Fleet( + vehicle=Car(id="V1", doors=4), + spares=[ + Truck(id="S1", payload="ore"), + Car(id="S2", doors=2), + ], + ) + + +def test_deserialize_nested_xsi_type_dispatch(): + fleet = polyxml.deserialize(FLEET_XML, Fleet) + + assert type(fleet.vehicle) is Car + assert fleet.vehicle.id == "V1" # inherited base field + assert fleet.vehicle.doors == 4 + + assert type(fleet.spares) is list + assert type(fleet.spares[0]) is Truck + assert fleet.spares[0].payload == "ore" + assert type(fleet.spares[1]) is Car + assert fleet.spares[1].doors == 2 + + +def test_nested_xsi_type_round_trip(): + fleet = make_fleet() + + out = polyxml.serialize(fleet) + assert b'xsi:type="Car"' in out, out + assert b'xsi:type="Truck"' in out, out + assert b'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' in out, out + + reparsed = polyxml.deserialize(out, Fleet) + assert reparsed == fleet + + +def test_root_xsi_type_dispatch(): + xml = b'T9ore' + + vehicle = polyxml.deserialize(xml, Vehicle) + assert type(vehicle) is Truck + assert vehicle.id == "T9" + assert vehicle.payload == "ore" + + +def test_root_round_trip_with_target_type(): + truck = Truck(id="T9", payload="ore") + + out = polyxml.serialize(truck, target_type=Vehicle) + assert out.startswith(b"X' + + with pytest.raises(ValueError, match="known derivation"): + polyxml.deserialize(xml, Fleet) + + +def test_abstract_without_derivations_raises(): + xml = b'X' + + with pytest.raises(ValueError, match="no registered derivations"): + polyxml.deserialize(xml, LonelyBase) + + with pytest.raises(ValueError, match="escape hatch"): + polyxml.deserialize(xml, LonelyBase) + + +def test_plain_type_attribute_not_dispatched(): + # A content attribute literally named "type" on a non-abstract, + # derivation-free model must never be treated as an xsi:type selector. + res = polyxml.deserialize(b'', Tagged) + assert res.type == "gear" + assert res.label == "L" + + out = polyxml.serialize(res) + assert b"xsi:type=" not in out, out + assert b'type="gear"' in out, out + + +# --------------------------------------------------------------- +# Pydantic fixtures: discovery also walks BaseModel subclass trees +# --------------------------------------------------------------- + + +class PVehicle(BaseModel): + id: str = Field(..., json_schema_extra={"type": "Element"}) + + +class PCar(PVehicle): + doors: int = Field(..., json_schema_extra={"type": "Element"}) + + +class PTruck(PVehicle): + payload: str = Field(..., json_schema_extra={"type": "Element"}) + + +class PFleet(BaseModel): + vehicle: PVehicle = Field(..., json_schema_extra={"type": "Element"}) + + +def test_pydantic_xsi_type_dispatch_and_round_trip(): + xml = b'V92' + + fleet = polyxml.deserialize(xml, PFleet) + assert type(fleet.vehicle) is PCar + assert fleet.vehicle.id == "V9" + assert fleet.vehicle.doors == 2 + + out = polyxml.serialize(fleet) + assert b'xsi:type="PCar"' in out, out + + reparsed = polyxml.deserialize(out, PFleet) + assert reparsed == fleet + + +def test_iterparse_xsi_type_dispatch(): + xml = ( + b"" + b'A2' + b'Bp' + b"" + ) + + items = list(polyxml.iterparse(xml, Vehicle, tag="vehicle")) + assert [type(v) for v in items] == [Car, Truck] + assert items[0].doors == 2 + assert items[1].payload == "p" + + +def test_xml_to_json_keeps_variant_fields(): + xml = b'V14' + + out = polyxml.xml_to_json(xml, target_type=Fleet) + data = json.loads(out) + assert data["vehicle"]["id"] == "V1" + assert data["vehicle"]["doors"] == 4 diff --git a/docs/guides/polymorphism.md b/docs/guides/polymorphism.md new file mode 100644 index 00000000..d4ffa8f0 --- /dev/null +++ b/docs/guides/polymorphism.md @@ -0,0 +1,145 @@ +# Polymorphic Types & `xsi:type` Dispatch + +XSD type hierarchies built from `` (and +`xsi:type` wire selectors) are dispatched by PolyXML's **dynamic runtime** — +the shared Rust engine behind the native API, `polyxml-python`, and (as it +gains nested schemas) `polyxml-js`. This page documents the strategy, its +round-trip behavior, and the known limitations of the static codegen paths. + +--- + +## 🧭 Strategy: a type registry on `ModelSchema` + +Every runtime `ModelSchema` carries: + +- `is_abstract` — the source schema marked the type `abstract="true"`. +- a **`variants` registry** — the concrete derivations eligible for + dispatch, each keyed by its **QName local part** (e.g. `Car` for + `urn:veh:Car`). + +The registry is populated from whichever schema source built the model: + +| Source | How variants are discovered | +|---|---| +| **XSD** (`ModelSchema::from_ir`) | All *transitive* `xs:extension` derivations of the type are built recursively from the `SchemaIR`. Derived schemas **flatten the full base-chain content model**: inherited fields precede the type's own fields. | +| **Python classes** (PyO3) | A `__subclasses__()` walk of the dataclass/Pydantic hierarchy, keyed by `Meta.name` (falling back to the class name). Generated Python already emits real class inheritance (`class Car(Vehicle):`), so inherited fields and metadata flow automatically. | + +**Matching is namespace-blind**, consistent with the rest of the runtime: +the parser matches elements and attributes by local name, so an `xsi:type` +QName value is reduced to its local part before lookup. Both prefixed +(`xsi:type="t:Car"` / `xsi:type="p:Car"`) and bare (`type="Car"`) wire +forms are accepted — the attribute's local name must simply be `type`. + +--- + +## 📥 Parse behavior + +Dispatch is applied at **every frame creation point**: the document root +(`Start` and `Empty`), nested elements, list items, and streamed +`iterparse` records. + +- `xsi:type` names a registered derivation → the element is parsed with the + concrete schema, so **base fields and concrete fields both survive**. +- `xsi:type` names an unknown type on an **abstract** base → a loud + `ValueError` listing the known derivations: + ``` + xsi:type="t:Plane" does not match any known derivation of 'Vehicle' (known: Car, Truck) + ``` +- An abstract type with **no registered derivations** fails with an + explicit escape-hatch message telling you to deserialize the concrete + type directly. +- Everything else is **tolerant**: non-abstract types without derivations + never scan for `xsi:type` at all (zero-overhead fast path), so a plain + content attribute literally named `type` is never mistaken for a + selector; unknown selectors on non-abstract types parse as declared. + +--- + +## 📤 Serialize behavior + +When a value is a record built from a **registered variant** of the +declared schema, the serializer writes the concrete type's fields and +re-emits the selector (`xsi:type="t:Car"`), declaring the XML Schema +Instance namespace at the root automatically. + +Python has two entry points: + +```python +# Serialize against the concrete type (default): the element is . +polyxml.serialize(car) + +# Serialize against the declared base: the element stays +# and xsi:type="Car" is re-emitted — a faithful wire round trip. +polyxml.serialize(car, target_type=Vehicle) +``` + +### Round trip + +```python +from dataclasses import dataclass, field +import polyxml + +@dataclass +class Vehicle: + class Meta: + name = "Vehicle" + abstract = True + id: str = field(metadata={"type": "Element", "name": "id"}) + +@dataclass +class Car(Vehicle): + class Meta: + name = "Car" + doors: int = field(metadata={"type": "Element", "name": "doors"}) + +@dataclass +class Fleet: + class Meta: + name = "Fleet" + vehicle: Vehicle = field(metadata={"type": "Element", "name": "vehicle"}) + +xml = b'V14' + +fleet = polyxml.deserialize(xml, Fleet) # fleet.vehicle is a Car +assert fleet.vehicle.doors == 4 # concrete field preserved +out = polyxml.serialize(fleet) # b'... xsi:type="Car" ...' +assert polyxml.deserialize(out, Fleet) == fleet +``` + +The same registry is reachable from pure Rust: + +```rust +let ir = polyxml::schema_parser::XsdParser::new().parse_str(xsd)?; +let schema = polyxml::schema::ModelSchema::from_ir(&ir, Some("Root"))?; +let value = polyxml::deserialize(xml, schema.clone())?; // dispatched Record +let out = polyxml::serialize_with_options("Root", &value, &schema, None, Some(true), None)?; +``` + +--- + +## ⚠️ Known limitations & escape hatches + +1. **Generated static codecs do not dispatch.** The compiled serde/quick-xml + codecs (Rust, Java, …) bind each element to one declared type; the Java + codec rejects `xsi:type` outright. *Escape hatch:* deserialize the + concrete type directly, or route polymorphic payloads through the + dynamic runtime (`polyxml::deserialize`, `polyxml.deserialize`). +2. **`polyxml-js` is scalar-flat today.** Its schema binding exposes only + scalar fields, so complex-typed (and therefore polymorphic) fields are + unreachable from JS yet — the dispatch itself lives in the shared core, + so no binding change will be needed when nested schemas land. +3. **JSON transcoding drops the selector.** `xml_to_json` keeps every + concrete field (it iterates the record's own schema), but JSON carries + no `xsi:type` marker, so JSON → XML cannot recover the wire selector. +4. **Discovery timing (Python).** Subclass discovery runs when the base + class's schema is first created. Define/import concrete subclasses + before the first `deserialize` call — ordinary module import order + already guarantees this. +5. **Unknown derivations fail loudly.** An `xsi:type` naming a type the + registry doesn't hold is an error (never silent truncation). *Escape + hatch:* import the module that defines the missing subclass, or + deserialize with the concrete type. + +Regression coverage lives in +`crates/polyxml-core/tests/test_issue53_xsi_type.rs` (Rust, 7 cases) and +`crates/polyxml-python/tests/test_xsi_type.py` (Python, 11 cases). diff --git a/docs/index.md b/docs/index.md index 84874225..6def2bb9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -127,5 +127,6 @@ Explore complete, production-ready example repositories showcasing PolyXML in mi - Check out the [5-Minute Multi-Language Quickstart](quickstart.md) to see PolyXML in action. - Read about our [XSD-to-Code Generator & CLI Toolchain](guides/compiler.md). +- Learn how [Polymorphic Types & `xsi:type` Dispatch](guides/polymorphism.md) work at runtime. - Read about our [Architecture & Streaming Design](architecture.md). - Explore [Performance & Benchmarks](benchmarks.md). From 93713b353a41a50e02b9686072e896badb19bb68 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 04:30:00 -0500 Subject: [PATCH 05/10] fix(rust-codegen): inline xsd:extension base fields in generated structs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add flatten_fields(s, ir): walks the base chain root-first with a cycle cut, then appends own fields with the most-derived declaration of a schema name shadowing inherited duplicates (keeps the simpleContent `value` field singular instead of emitting a `value_2` duplicate). - Use it at all four field-iteration sites — compute_types_with_lifetime, emit_struct, the has_patterns probe, and the decode field_metas build — so the struct body, `<'a>` propagation, validate_patterns, and both codec directions agree on one list. Previously Rust was the only backend that dropped inherited fields: a derived InternationalAddress could not store or round-trip its base's street/city/country, and a derived type whose own fields were all numeric would have emitted Cow<'a, str> in a struct declared without the lifetime parameter. - Regression tests cover two- and three-level chains, lifetime propagation from inherited string fields, codec decode/encode of inherited members, the simpleContent shadow rule, and circular bases; the codegen skill now records the invariant plus two related unfixed gaps found while verifying (Java record mode bypasses the model_fields base walk, and generated Rust codecs never consume FieldKind::Text). Tests: 4 new cases in test_rust_codegen.rs (13 green), cargo test --workspace, test_codegen.sh (TS modules enabled), verify_codegen.sh across all 7 ecosystems, Python gate at 100% coverage; generated output additionally compiled and round-tripped in a scratch cargo project (inherited decode, required-field enforcement, encode parity). --- .../skills/polyxml-codegen-workflow/SKILL.md | 22 ++ crates/polyxml-core/src/codegen/rust/mod.rs | 67 ++++- .../polyxml-core/tests/test_rust_codegen.rs | 265 ++++++++++++++++++ 3 files changed, 346 insertions(+), 8 deletions(-) diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 52caeb9a..29be77ca 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -128,6 +128,28 @@ If external compilers are installed on the development machine, run the E2E veri 5. **Never Derive Type Identifiers from `qname.local` Directly**: all named-type identifiers must flow through the language's `type_ident` helper (see section 7) so cross-namespace collisions stay disambiguated. +6. **`xsd:extension` Base-Field Flattening (Rust)**: Rust has no struct + inheritance, so `codegen/rust/mod.rs::flatten_fields(s, ir)` must inline the + whole base chain (root first, cycle-cut on revisit, most-derived declaration + of a schema name shadows inherited duplicates so the simpleContent `value` + field stays singular). It is required at EVERY field-iteration site: + `compute_types_with_lifetime`, `emit_struct`, the `has_patterns` probe, and + the decode `field_metas` build — `encode_xml`/`validate_patterns` follow + `field_metas` automatically. Missing the lifetime site emits `Cow<'a, str>` + in a struct without `<'a>` (generated code fails to compile); missing codec + sites silently drops inherited data. Backends with real inheritance (TS + `extends`, Go embed, C++/C# `: Base`, Python `class Derived(Base)`) must + NOT flatten — Python leans on `__dataclass_fields__` (see §11). +7. **Known Extension/Text Gaps (found 2026-09-22, unfixed)**: Java record + mode (the default `--style record`) bypasses the `model_fields` base walk + via the `use_records && !emit_builder && !emit_direct_codec` guard in + `java/models.rs`, so derived records drop inherited fields (records cannot + `extends`, so they need flattening like Rust). Separately, generated Rust + codecs never consume `FieldKind::Text` — only a `#[polyxml(text)]` label + exists — so simpleContent structs always fail `from_xml` with + `Missing required field 'value'` and `encode_xml` omits the text; Go, Java, + C++, and C# codecs do handle Text. When touching either area, verify with a + two-level extension XSD plus a simpleContent XSD across all 7 backends. ## 6. Java/C# Model Styles and Direct Java Codecs diff --git a/crates/polyxml-core/src/codegen/rust/mod.rs b/crates/polyxml-core/src/codegen/rust/mod.rs index 651fc85b..282e791b 100644 --- a/crates/polyxml-core/src/codegen/rust/mod.rs +++ b/crates/polyxml-core/src/codegen/rust/mod.rs @@ -201,6 +201,59 @@ fn type_ident(q: &QName) -> String { lookup_type_name(q, || AsPascalCase(&q.local).to_string()) } +/// Flattened field list for a struct: every field inherited through its +/// `xsd:extension` base chain (root first), followed by the struct's own +/// fields. Rust structs have no inheritance, so a derived type must inline +/// the base fields instead of silently dropping them — struct declarations, +/// lifetime analysis, pattern detection, and both codec directions must all +/// agree on this exact list. +/// +/// When a derived type re-declares a field whose schema name matches an +/// inherited one (the `simpleContent` value field, for instance), the +/// most-derived declaration wins and the inherited duplicate is skipped, so +/// flattening never introduces `value_2`-style duplicates. A base chain that +/// revisits a type is cut at the revisit. +fn flatten_fields<'a>(s: &'a StructDef, ir: &'a SchemaIR) -> Vec<&'a FieldDef> { + fn collect_bases<'a>( + s: &'a StructDef, + ir: &'a SchemaIR, + seen: &mut HashSet, + out: &mut Vec<&'a StructDef>, + ) { + if !seen.insert(s.qname.clone()) { + return; + } + if let Some(TypeDef::Struct(base)) = s.base_type.as_ref().and_then(|q| ir.types.get(q)) { + collect_bases(base, ir, seen, out); + out.push(base); + } + } + + let mut bases = Vec::new(); + collect_bases(s, ir, &mut HashSet::new(), &mut bases); + if bases.is_empty() { + return s.fields.iter().collect(); + } + + // Claim schema names leaf-to-root so the most-derived declaration of a + // name shadows any inherited duplicate. Names are claimed per struct, not + // per field, so an attribute and an element sharing a name inside one + // struct are both preserved. + let mut claimed: HashSet<&str> = HashSet::new(); + let mut groups: Vec> = Vec::new(); + for st in std::iter::once(s).chain(bases.iter().rev().copied()) { + let group: Vec<&FieldDef> = st + .fields + .iter() + .filter(|f| !claimed.contains(f.name.as_str())) + .collect(); + claimed.extend(group.iter().map(|f| f.name.as_str())); + groups.push(group); + } + groups.reverse(); + groups.into_iter().flatten().collect() +} + impl RustCodegen { pub fn new(options: RustOptions) -> Self { let zero_copy = options.zero_copy; @@ -325,9 +378,8 @@ impl RustCodegen { .branches .iter() .any(|b| typeref_has_lifetime(&b.type_ref, &requires_lifetime)), - TypeDef::Struct(s) => s - .fields - .iter() + TypeDef::Struct(s) => flatten_fields(s, ir) + .into_iter() .any(|f| typeref_has_lifetime(&f.type_ref, &requires_lifetime)), }; @@ -653,7 +705,7 @@ impl RustCodegen { let _ = writeln!(out, "{} {{", struct_decl); let mut seen_fields = HashSet::new(); - for field in &s.fields { + for field in flatten_fields(s, ir) { let rust_name = self.unique_rust_field_name(&field.name, &mut seen_fields); self.emit_struct_field(out, field, &rust_name, types_with_lifetime); } @@ -1008,9 +1060,8 @@ impl RustCodegen { types_with_lifetime: &HashSet, ir: &SchemaIR, ) { - let has_patterns = s - .fields - .iter() + let has_patterns = flatten_fields(s, ir) + .into_iter() .any(|f| super::patterned_simple(&f.type_ref, ir).is_some()); let struct_name = type_ident(&s.qname); let needs_lifetime = types_with_lifetime.contains(&s.qname); @@ -1074,7 +1125,7 @@ impl RustCodegen { rust_name: String, } let mut field_metas = Vec::new(); - for field in &s.fields { + for field in flatten_fields(s, ir) { let rust_name = self.unique_rust_field_name(&field.name, &mut seen_fields); field_metas.push(FieldMeta { field: field.clone(), diff --git a/crates/polyxml-core/tests/test_rust_codegen.rs b/crates/polyxml-core/tests/test_rust_codegen.rs index d04d35e8..7364794c 100644 --- a/crates/polyxml-core/tests/test_rust_codegen.rs +++ b/crates/polyxml-core/tests/test_rust_codegen.rs @@ -5,6 +5,21 @@ use polyxml::ir::{ Cardinality, EnumDef, EnumValue, FieldDef, FieldKind, PrimitiveType, QName, SchemaIR, StructDef, TypeDef, TypeRef, UnionBranch, UnionDef, }; +use polyxml::schema_parser::XsdParser; + +/// Slice a generated struct or `impl` body from `start` up to the first +/// closing brace at column 0, panicking when `start` is absent. Scoping +/// assertions to one body keeps them from passing against a sibling type. +fn section<'a>(code: &'a str, start: &str) -> &'a str { + let from = code + .find(start) + .unwrap_or_else(|| panic!("expected {start:?} in generated code:\n{code}")); + let rest = &code[from..]; + match rest.find("\n}\n") { + Some(i) => &rest[..i + 3], + None => rest, + } +} #[test] fn test_rust_identifier_sanitization() { @@ -577,3 +592,253 @@ fn test_rust_rkyv_derives() { assert!(code.contains("#[cfg_attr(feature = \"rkyv\", derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize))]")); assert!(code.contains("#[cfg_attr(feature = \"rkyv\", rkyv(check_bytes))]")); } + +// --------------------------------------------------------------------------- +// xsd:extension base-field flattening: Rust structs have no inheritance, so a +// derived type must inline the base chain's fields everywhere — struct body, +// lifetime analysis, and both codec directions. +// --------------------------------------------------------------------------- + +/// Lifetime edge case: `Middle` owns no string fields, so it only earns its +/// `<'a>` if inherited fields participate in `compute_types_with_lifetime`. +fn extension_xsd() -> &'static str { + r#" + + + + + + + + + + + + + + + + + + + + + + + + + + "# +} + +#[test] +fn test_rust_extension_inlines_base_fields() { + let ir = XsdParser::new() + .parse_str(extension_xsd()) + .expect("parse extension schema"); + let code = RustCodegen::new(RustOptions::default()).generate_module(&ir); + + let base = section(&code, "pub struct Base"); + assert!(base.contains("pub label: Cow<'a, str>")); + assert!(base.contains("pub kind: Option>")); + + let middle = section(&code, "pub struct Middle"); + assert!( + middle.contains("pub struct Middle<'a> {"), + "inherited string field must propagate the lifetime parameter:\n{middle}" + ); + for inherited in [ + "pub label: Cow<'a, str>,", + "pub kind: Option>,", + ] { + assert!( + middle.contains(inherited), + "Middle dropped inherited field {inherited:?}:\n{middle}" + ); + } + assert!(middle.contains("pub rank: i32,")); + assert!(middle.contains("pub active: Option,")); + // Root-first ordering: base fields precede derived ones. + let label_at = middle.find("pub label:").expect("label"); + let rank_at = middle.find("pub rank:").expect("rank"); + assert!(label_at < rank_at, "base fields must come first:\n{middle}"); + + // Three-level chain: Leaf sees Base + Middle + its own field. + let leaf = section(&code, "pub struct Leaf"); + for expected in [ + "pub label: Cow<'a, str>,", + "pub kind: Option>,", + "pub rank: i32,", + "pub active: Option,", + "pub note: Option>,", + ] { + assert!( + leaf.contains(expected), + "Leaf missing {expected:?}:\n{leaf}" + ); + } +} + +#[test] +fn test_rust_extension_codecs_decode_and_encode_base_fields() { + let ir = XsdParser::new() + .parse_str(extension_xsd()) + .expect("parse extension schema"); + let code = RustCodegen::new(RustOptions::default()).generate_module(&ir); + + let middle_impl = section(&code, "impl<'a> Middle"); + // Decode: variable slots, element dispatch, and attribute dispatch for + // every inherited member, plus enforcement of required base fields. + for expected in [ + "let mut var_label = None;", + "let mut var_kind = None;", + "let mut var_rank = None;", + "\"label\" => {", + "\"kind\" => {", + "\"rank\" => {", + "\"active\" => {", + "Missing required field 'label'", + ] { + assert!( + middle_impl.contains(expected), + "Middle codec missing {expected:?}:\n{middle_impl}" + ); + } + // Encode: inherited element and attribute both written back out. + for expected in [ + "BytesStart::new(\"label\")", + "BytesText::new(self.label.as_ref())", + "push_attribute((\"kind\"", + "BytesStart::new(\"rank\")", + ] { + assert!( + middle_impl.contains(expected), + "Middle encode missing {expected:?}:\n{middle_impl}" + ); + } + + let leaf_impl = section(&code, "impl<'a> Leaf"); + for expected in [ + "let mut var_label = None;", + "let mut var_rank = None;", + "\"label\" => {", + "\"rank\" => {", + "Missing required field 'label'", + "BytesStart::new(\"label\")", + "push_attribute((\"kind\"", + ] { + assert!( + leaf_impl.contains(expected), + "Leaf codec missing {expected:?}:\n{leaf_impl}" + ); + } +} + +#[test] +fn test_rust_simple_content_extension_shadow_rule() { + // simpleContent extension re-declares the base `value` field; flattening + // must inherit the base attributes without emitting a `value_2` duplicate. + let xsd = r#" + + + + + + + + + + + + + + + "#; + + let ir = XsdParser::new() + .parse_str(xsd) + .expect("parse simpleContent schema"); + let code = RustCodegen::new(RustOptions::default()).generate_module(&ir); + + let derived = section(&code, "pub struct PreciseMeasurement"); + assert!( + derived.contains("pub unit: Option>,"), + "inherited unit attribute missing:\n{derived}" + ); + assert!( + derived.contains("pub precision: Option,"), + "own precision attribute missing:\n{derived}" + ); + assert_eq!( + derived.matches("pub value").count(), + 1, + "value must be shadowed by the derived declaration, not duplicated:\n{derived}" + ); + assert!( + !derived.contains("value_2"), + "shadow rule must prevent suffixed duplicates:\n{derived}" + ); + + let derived_impl = section(&code, "impl<'a> PreciseMeasurement"); + assert!(derived_impl.contains("\"unit\" => {")); + assert!(derived_impl.contains("\"precision\" => {")); + assert!(derived_impl.contains("push_attribute((\"unit\"")); + assert!(derived_impl.contains("push_attribute((\"precision\"")); +} + +#[test] +fn test_rust_extension_base_cycle_cut() { + // A circular extension is invalid XSD but must not hang or duplicate + // fields when handed to the generator directly. + let mut ir = SchemaIR::new().with_target_namespace("urn:cycle"); + let qa = QName::new(Some("urn:cycle"), "Alpha"); + let qb = QName::new(Some("urn:cycle"), "Beta"); + + ir.add_type(TypeDef::Struct(StructDef { + qname: qa.clone(), + base_type: Some(qb.clone()), + is_abstract: false, + fields: vec![FieldDef::new( + "alphaField", + "alphaField", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::String), + )], + documentation: None, + })); + ir.add_type(TypeDef::Struct(StructDef { + qname: qb.clone(), + base_type: Some(qa.clone()), + is_abstract: false, + fields: vec![FieldDef::new( + "betaField", + "betaField", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::Int), + )], + documentation: None, + })); + + let codegen = RustCodegen::new(RustOptions::default()); + let code = codegen.generate_module(&ir); + + let alpha = section(&code, "pub struct Alpha"); + assert_eq!( + alpha.matches("pub beta_field").count(), + 1, + "cycle-merged field must appear exactly once:\n{alpha}" + ); + assert_eq!(alpha.matches("pub alpha_field").count(), 1); + assert!(alpha.contains("pub struct Alpha<'a> {")); + + let beta = section(&code, "pub struct Beta"); + assert_eq!( + beta.matches("pub alpha_field").count(), + 1, + "cycle-merged field must appear exactly once:\n{beta}" + ); + assert_eq!(beta.matches("pub beta_field").count(), 1); +} From 5e559ee8d11bbe831ec5baa4651dec91993d938b Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 05:30:00 -0500 Subject: [PATCH 06/10] fix(java-codegen): inline base fields in plain record mode - model_fields always runs the base walk now: the `use_records && !emit_builder && !emit_direct_codec` guard made the DEFAULT record output declare only a derived type's own components, so an InternationalAddress record could not even store its base's street/city/country. Builder and direct-codec modes already collected; only the plain record path dropped inherited data. - The local collect walker is replaced by the shared flatten_fields helper (moved from rust/mod.rs to codegen/mod.rs), which also brings Java its first shadow rule: a derived declaration of the same schema name wins, so simpleContent chains emit one `value` component instead of a suffixed duplicate in records, classes, and codecs alike. - POJO/class emission is unchanged: it re-slices the own-fields tail for declarations and keeps `extends Base`; builders keep their inherited boundary arithmetic. Regression tests: plain-record component assertions (root-first order, inherited id/unit present, exactly one value) plus a javac+java E2E that constructs derived records positionally across complexContent and simpleContent chains; codegen skill records the fix. Tests: 2 new cases in test_java_codegen.rs (15 green), cargo test --workspace (26 suites), test_codegen.sh (TS modules enabled), verify_codegen.sh across all 7 ecosystems, Python gate at 100% coverage; fixture records additionally compiled and executed via javac (three-level chain + simpleContent unit inheritance). --- .../skills/polyxml-codegen-workflow/SKILL.md | 39 ++--- .../polyxml-core/src/codegen/java/models.rs | 28 +-- crates/polyxml-core/src/codegen/mod.rs | 57 ++++++- crates/polyxml-core/src/codegen/rust/mod.rs | 56 +----- .../polyxml-core/tests/test_java_codegen.rs | 160 ++++++++++++++++++ 5 files changed, 244 insertions(+), 96 deletions(-) diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 29be77ca..7862ad07 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -128,28 +128,29 @@ If external compilers are installed on the development machine, run the E2E veri 5. **Never Derive Type Identifiers from `qname.local` Directly**: all named-type identifiers must flow through the language's `type_ident` helper (see section 7) so cross-namespace collisions stay disambiguated. -6. **`xsd:extension` Base-Field Flattening (Rust)**: Rust has no struct - inheritance, so `codegen/rust/mod.rs::flatten_fields(s, ir)` must inline the - whole base chain (root first, cycle-cut on revisit, most-derived declaration - of a schema name shadows inherited duplicates so the simpleContent `value` - field stays singular). It is required at EVERY field-iteration site: - `compute_types_with_lifetime`, `emit_struct`, the `has_patterns` probe, and - the decode `field_metas` build — `encode_xml`/`validate_patterns` follow - `field_metas` automatically. Missing the lifetime site emits `Cow<'a, str>` - in a struct without `<'a>` (generated code fails to compile); missing codec - sites silently drops inherited data. Backends with real inheritance (TS +6. **`xsd:extension` Base-Field Flattening (Rust + Java records)**: targets + without inheritance must inline the base chain via the shared + `codegen/mod.rs::flatten_fields(s, ir)` (root first, cycle-cut on revisit, + most-derived declaration of a schema name shadows inherited duplicates so + the simpleContent `value` field stays singular). For Rust it is required at + EVERY field-iteration site: `compute_types_with_lifetime`, `emit_struct`, + the `has_patterns` probe, and the decode `field_metas` build — + `encode_xml`/`validate_patterns` follow `field_metas` automatically. + Missing the lifetime site emits `Cow<'a, str>` in a struct without `<'a>` + (generated code fails to compile); missing codec sites silently drops + inherited data. Java's `model_fields` returns the flattened list for every + mode — records declare it as components, classes re-slice the own-fields + tail because they `extends` instead. Backends with real inheritance (TS `extends`, Go embed, C++/C# `: Base`, Python `class Derived(Base)`) must NOT flatten — Python leans on `__dataclass_fields__` (see §11). -7. **Known Extension/Text Gaps (found 2026-09-22, unfixed)**: Java record - mode (the default `--style record`) bypasses the `model_fields` base walk - via the `use_records && !emit_builder && !emit_direct_codec` guard in - `java/models.rs`, so derived records drop inherited fields (records cannot - `extends`, so they need flattening like Rust). Separately, generated Rust - codecs never consume `FieldKind::Text` — only a `#[polyxml(text)]` label - exists — so simpleContent structs always fail `from_xml` with +7. **Known Gap (found 2026-09-22, unfixed)**: generated Rust codecs never + consume `FieldKind::Text` — only a `#[polyxml(text)]` label exists — so + simpleContent structs always fail `from_xml` with `Missing required field 'value'` and `encode_xml` omits the text; Go, Java, - C++, and C# codecs do handle Text. When touching either area, verify with a - two-level extension XSD plus a simpleContent XSD across all 7 backends. + C++, and C# codecs do handle Text. When touching this area, verify with a + simpleContent XSD across all 7 backends. (The companion Java gap — plain + record mode dropping inherited fields — was fixed the same day: records + cannot `extends`, so `model_fields` now always inlines `flatten_fields`.) ## 6. Java/C# Model Styles and Direct Java Codecs diff --git a/crates/polyxml-core/src/codegen/java/models.rs b/crates/polyxml-core/src/codegen/java/models.rs index bce6a35a..b8861f8a 100644 --- a/crates/polyxml-core/src/codegen/java/models.rs +++ b/crates/polyxml-core/src/codegen/java/models.rs @@ -1,4 +1,5 @@ use super::*; +use crate::codegen::flatten_fields; use crate::ir::{FieldDef, FieldKind}; impl JavaCodegen { @@ -38,28 +39,11 @@ impl JavaCodegen { s: &'a StructDef, ir: &'a SchemaIR, ) -> Vec<(&'a FieldDef, String)> { - fn collect<'a>( - s: &'a StructDef, - ir: &'a SchemaIR, - seen: &mut HashSet, - fields: &mut Vec<&'a FieldDef>, - ) { - if !seen.insert(s.qname.clone()) { - return; - } - if let Some(TypeDef::Struct(base)) = s.base_type.as_ref().and_then(|q| ir.types.get(q)) - { - collect(base, ir, seen, fields); - } - fields.extend(&s.fields); - } - let mut fields = Vec::new(); - if self.options.use_records && !self.options.emit_builder && !self.options.emit_direct_codec - { - fields.extend(&s.fields); - } else { - collect(s, ir, &mut HashSet::new(), &mut fields); - } + // Records cannot `extends`, so every record mode must inline the base + // chain — a plain record previously declared only its own components + // and silently dropped inherited fields. Classes re-slice back to + // their own tail for field declarations because they inherit instead. + let fields = flatten_fields(s, ir); let mut seen = HashSet::new(); fields .into_iter() diff --git a/crates/polyxml-core/src/codegen/mod.rs b/crates/polyxml-core/src/codegen/mod.rs index 956bbf8e..18525182 100644 --- a/crates/polyxml-core/src/codegen/mod.rs +++ b/crates/polyxml-core/src/codegen/mod.rs @@ -22,7 +22,7 @@ pub use python::{PythonBackend, PythonCodegen, PythonOptions}; pub use rust::{RustCodegen, RustOptions}; pub use typescript::{TypeScriptBackend, TypeScriptCodegen, TypeScriptOptions}; -use crate::ir::{PrimitiveType, QName, SchemaIR, TypeRef}; +use crate::ir::{FieldDef, PrimitiveType, QName, SchemaIR, StructDef, TypeRef}; #[derive(Debug, Error)] pub enum CodegenError { @@ -629,3 +629,58 @@ pub(crate) fn patterned_simple<'a>( _ => None, } } + +/// Flattened field list for a struct: every field inherited through its +/// `xsd:extension` base chain (root first), followed by the struct's own +/// fields. Backends whose targets cannot represent the base chain directly — +/// Rust structs and Java records have no inheritance — must emit this exact +/// list; declaration, name allocation, and both codec directions must agree +/// on it or derived types silently drop inherited data. +/// +/// When a derived type re-declares a field whose schema name matches an +/// inherited one (the `simpleContent` value field, for instance), the +/// most-derived declaration wins and the inherited duplicate is skipped, so +/// flattening never introduces suffixed `value2`-style duplicates. A base +/// chain that revisits a type is cut at the revisit. +pub(crate) fn flatten_fields<'a>(s: &'a StructDef, ir: &'a SchemaIR) -> Vec<&'a FieldDef> { + fn collect_bases<'a>( + s: &'a StructDef, + ir: &'a SchemaIR, + seen: &mut HashSet, + out: &mut Vec<&'a StructDef>, + ) { + if !seen.insert(s.qname.clone()) { + return; + } + if let Some(crate::ir::TypeDef::Struct(base)) = + s.base_type.as_ref().and_then(|q| ir.types.get(q)) + { + collect_bases(base, ir, seen, out); + out.push(base); + } + } + + let mut bases = Vec::new(); + collect_bases(s, ir, &mut HashSet::new(), &mut bases); + if bases.is_empty() { + return s.fields.iter().collect(); + } + + // Claim schema names leaf-to-root so the most-derived declaration of a + // name shadows any inherited duplicate. Names are claimed per struct, not + // per field, so an attribute and an element sharing a name inside one + // struct are both preserved. + let mut claimed: HashSet<&str> = HashSet::new(); + let mut groups: Vec> = Vec::new(); + for st in std::iter::once(s).chain(bases.iter().rev().copied()) { + let group: Vec<&FieldDef> = st + .fields + .iter() + .filter(|f| !claimed.contains(f.name.as_str())) + .collect(); + claimed.extend(group.iter().map(|f| f.name.as_str())); + groups.push(group); + } + groups.reverse(); + groups.into_iter().flatten().collect() +} diff --git a/crates/polyxml-core/src/codegen/rust/mod.rs b/crates/polyxml-core/src/codegen/rust/mod.rs index 282e791b..0a7be519 100644 --- a/crates/polyxml-core/src/codegen/rust/mod.rs +++ b/crates/polyxml-core/src/codegen/rust/mod.rs @@ -5,7 +5,8 @@ use heck::{AsPascalCase, AsSnakeCase}; use serde::{Deserialize, Serialize}; use crate::codegen::{ - build_type_name_map, lookup_type_name, sanitize_keyword, set_type_name_map, LanguageContext, + build_type_name_map, flatten_fields, lookup_type_name, sanitize_keyword, set_type_name_map, + LanguageContext, }; use crate::ir::{ EnumDef, FieldDef, FieldKind, PrimitiveType, QName, SchemaIR, SimpleTypeDef, StructDef, @@ -201,59 +202,6 @@ fn type_ident(q: &QName) -> String { lookup_type_name(q, || AsPascalCase(&q.local).to_string()) } -/// Flattened field list for a struct: every field inherited through its -/// `xsd:extension` base chain (root first), followed by the struct's own -/// fields. Rust structs have no inheritance, so a derived type must inline -/// the base fields instead of silently dropping them — struct declarations, -/// lifetime analysis, pattern detection, and both codec directions must all -/// agree on this exact list. -/// -/// When a derived type re-declares a field whose schema name matches an -/// inherited one (the `simpleContent` value field, for instance), the -/// most-derived declaration wins and the inherited duplicate is skipped, so -/// flattening never introduces `value_2`-style duplicates. A base chain that -/// revisits a type is cut at the revisit. -fn flatten_fields<'a>(s: &'a StructDef, ir: &'a SchemaIR) -> Vec<&'a FieldDef> { - fn collect_bases<'a>( - s: &'a StructDef, - ir: &'a SchemaIR, - seen: &mut HashSet, - out: &mut Vec<&'a StructDef>, - ) { - if !seen.insert(s.qname.clone()) { - return; - } - if let Some(TypeDef::Struct(base)) = s.base_type.as_ref().and_then(|q| ir.types.get(q)) { - collect_bases(base, ir, seen, out); - out.push(base); - } - } - - let mut bases = Vec::new(); - collect_bases(s, ir, &mut HashSet::new(), &mut bases); - if bases.is_empty() { - return s.fields.iter().collect(); - } - - // Claim schema names leaf-to-root so the most-derived declaration of a - // name shadows any inherited duplicate. Names are claimed per struct, not - // per field, so an attribute and an element sharing a name inside one - // struct are both preserved. - let mut claimed: HashSet<&str> = HashSet::new(); - let mut groups: Vec> = Vec::new(); - for st in std::iter::once(s).chain(bases.iter().rev().copied()) { - let group: Vec<&FieldDef> = st - .fields - .iter() - .filter(|f| !claimed.contains(f.name.as_str())) - .collect(); - claimed.extend(group.iter().map(|f| f.name.as_str())); - groups.push(group); - } - groups.reverse(); - groups.into_iter().flatten().collect() -} - impl RustCodegen { pub fn new(options: RustOptions) -> Self { let zero_copy = options.zero_copy; diff --git a/crates/polyxml-core/tests/test_java_codegen.rs b/crates/polyxml-core/tests/test_java_codegen.rs index 992d2477..fbeebcf8 100644 --- a/crates/polyxml-core/tests/test_java_codegen.rs +++ b/crates/polyxml-core/tests/test_java_codegen.rs @@ -883,3 +883,163 @@ public class Main {{ public static void main(String[] args) throws Exception {{ ); } } + +// --------------------------------------------------------------------------- +// xsd:extension in plain record mode (the default): records cannot `extends`, +// so inherited fields must be inlined as components. Previously only the +// builder/direct-codec modes ran the base walk, silently dropping inherited +// fields from the default record output. +// --------------------------------------------------------------------------- + +/// Slice one record's component header from `record Name(` through `) {`. +fn record_header<'a>(code: &'a str, name: &str) -> &'a str { + let marker = format!("record {name}("); + let start = code + .find(&marker) + .unwrap_or_else(|| panic!("expected {marker:?} in:\n{code}")); + let rest = &code[start..]; + let end = rest + .find(") {") + .unwrap_or_else(|| panic!("unterminated record {name}:\n{code}")); + &rest[..end + 3] +} + +fn extension_schema() -> &'static str { + r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + "# +} + +#[test] +fn test_java_plain_record_inlines_base_fields() { + let ir = polyxml::schema_parser::XsdParser::new() + .parse_str(extension_schema()) + .expect("parse extension schema"); + // Defaults: use_records=true with no builder and no direct codec — the + // mode that used to bypass the base walk entirely. + let generator = JavaCodegen::new(JavaOptions { + package_name: String::new(), + ..Default::default() + }); + let models = generator.generate_module(&ir, "Models"); + + let message = record_header(&models, "Message"); + assert!( + message.contains("int id"), + "record Message dropped the inherited id attribute:\n{message}" + ); + assert!( + message.contains("String text"), + "own text missing:\n{message}" + ); + assert!(message.contains("count"), "own count missing:\n{message}"); + let id_at = message.find("int id").expect("id"); + let text_at = message.find("String text").expect("text"); + assert!( + id_at < text_at, + "base components must precede derived ones:\n{message}" + ); + + // simpleContent chain: `unit` inherited, `value` present exactly once — + // a suffixed duplicate would still match the " value" prefix below. + let derived = record_header(&models, "PreciseMeasurement"); + assert!( + derived.contains("unit"), + "record PreciseMeasurement dropped the inherited unit attribute:\n{derived}" + ); + assert!( + derived.contains("precision"), + "own precision missing:\n{derived}" + ); + assert_eq!( + derived.matches(" value").count(), + 1, + "`value` must be shadowed by the derived declaration, not duplicated:\n{derived}" + ); +} + +#[test] +fn test_java_plain_record_extension_javac_execute() { + let ir = polyxml::schema_parser::XsdParser::new() + .parse_str(extension_schema()) + .expect("parse extension schema"); + let generator = JavaCodegen::new(JavaOptions { + package_name: String::new(), + ..Default::default() + }); + let dir = tempdir().unwrap(); + fs::write( + dir.path().join("Models.java"), + generator.generate_module(&ir, "Models"), + ) + .unwrap(); + fs::write( + dir.path().join("Main.java"), + r#"import java.util.Optional; +public class Main { + public static void main(String[] args) { + var m = new Models.Message(5, "hello", Optional.of(3)); + if (m.id() != 5) throw new AssertionError("inherited id attribute lost"); + if (!m.text().equals("hello")) throw new AssertionError("own text lost"); + if (m.count().orElse(-1) != 3) throw new AssertionError("own count lost"); + var p = new Models.PreciseMeasurement( + Optional.of("km"), + new Models.Measurement(new java.math.BigDecimal("2.5"), Optional.empty()), + Optional.of(3)); + if (!p.unit().orElse("").equals("km")) throw new AssertionError("inherited unit lost"); + if (p.precision().orElse(-1) != 3) throw new AssertionError("own precision lost"); + } +}"#, + ) + .unwrap(); + if Command::new("javac").arg("-version").output().is_err() { + return; + } + let result = Command::new("javac") + .current_dir(dir.path()) + .args(["Models.java", "Main.java"]) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); + let result = Command::new("java") + .current_dir(dir.path()) + .args(["-cp", ".", "Main"]) + .output() + .unwrap(); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); +} From c16e57d30a1501a154e6d02c7dfc921b4b16be8e Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 07:00:00 -0500 Subject: [PATCH 07/10] fix: simpleContent text codecs, split-text accumulation, and transcoder entity refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - emit FieldKind::Text codecs (emit_text_content_parse/_serialize): decode reads the element content through the scalar/facet path and encode writes it back; only scalar-backed values are emitted (struct-typed value stays a documented cross-backend gap pending issue #51 item 4 IR modeling) - read_element_text (both zero_copy variants) now appends segments and resolves GeneralRef/CData like parser.rs::append_general_ref: quick-xml splits 'a & b' into Text/GeneralRef/Text, so the old last-wins loop silently dropped every entity-bearing or CDATA text value - transcoder: resolve general refs instead of dropping them via _ => {}, unescape attribute values, and drop trim_text(true) — per-segment trimming ate spaces adjacent to refs (x & y -> x&y); the End arm trims the assembled buffer instead - add unused_assignments to the generated module allow-header (the text path assigns its slot unconditionally) - regression tests: test_rust_simple_content_text_codec, test_rust_read_element_text_accumulates_refs_and_cdata, test_schemaless_preserves_refs_cdata_and_attr_entities; skills updated --- .../skills/polyxml-codegen-workflow/SKILL.md | 36 +++- .agents/skills/polyxml-core-engine/SKILL.md | 11 +- crates/polyxml-core/src/codegen/rust/mod.rs | 162 +++++++++++++++++- crates/polyxml-core/src/transcoder.rs | 34 +++- .../polyxml-core/tests/test_rust_codegen.rs | 114 ++++++++++++ crates/polyxml-core/tests/test_transcoder.rs | 30 ++++ 6 files changed, 371 insertions(+), 16 deletions(-) diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 7862ad07..8802428c 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -143,14 +143,34 @@ If external compilers are installed on the development machine, run the E2E veri tail because they `extends` instead. Backends with real inheritance (TS `extends`, Go embed, C++/C# `: Base`, Python `class Derived(Base)`) must NOT flatten — Python leans on `__dataclass_fields__` (see §11). -7. **Known Gap (found 2026-09-22, unfixed)**: generated Rust codecs never - consume `FieldKind::Text` — only a `#[polyxml(text)]` label exists — so - simpleContent structs always fail `from_xml` with - `Missing required field 'value'` and `encode_xml` omits the text; Go, Java, - C++, and C# codecs do handle Text. When touching this area, verify with a - simpleContent XSD across all 7 backends. (The companion Java gap — plain - record mode dropping inherited fields — was fixed the same day: records - cannot `extends`, so `model_fields` now always inlines `flatten_fields`.) +7. **simpleContent `FieldKind::Text` codecs (fixed 2026-09-22)**: generated + Rust codecs now consume `FieldKind::Text` — `emit_text_content_parse` + reads the element content via `read_element_text` and runs it through the + same scalar/facet path as elements (`var_value = Some(...)`; patterns from + the extension base are enforced via `validate_patterns`), and + `emit_text_content_serialize` writes the value between Start/End. Only + scalar-backed Text is emitted: a struct-typed value (`value: Measurement`) + stays a documented cross-backend gap pending issue #51 item 4 IR modeling. + The module allow-header includes `unused_assignments` because the text path + assigns its slot unconditionally. The companion Java gap — plain record + mode dropping inherited fields — was fixed the same day: records cannot + `extends`, so `model_fields` now always inlines `flatten_fields`. + Regression-locked by `test_rust_simple_content_text_codec` plus runtime + round-trips (numeric, patterned, string, bad-number rejection). +8. **Text events arrive split — readers must accumulate**: quick-xml emits + `a & b` as `Text("a ")` + `GeneralRef("amp")` + `Text(" b")` and CDATA + as its own event, so `read_element_text` (both `zero_copy` variants) + appends every segment and resolves refs exactly like + `parser.rs::append_general_ref` (char refs → `resolve_char_ref()`, + predefined → `escape::resolve_xml_entity()`, otherwise the raw name — + lenient, never an error). Never overwrite the accumulator per event (the + pre-fix last-wins helper silently dropped data) and never pair split-text + assembly with `trim_text(true)` — per-segment trimming eats spaces adjacent + to refs (`x & y` → `x&y`); trim once on the assembled buffer (see + `transcoder.rs`, which now also unescapes attribute values and resolves + general refs instead of dropping them via `_ => {}`). Regression-locked by + `test_rust_read_element_text_accumulates_refs_and_cdata` and + `test_schemaless_preserves_refs_cdata_and_attr_entities`. ## 6. Java/C# Model Styles and Direct Java Codecs diff --git a/.agents/skills/polyxml-core-engine/SKILL.md b/.agents/skills/polyxml-core-engine/SKILL.md index 4e3c5402..6b7c8eab 100644 --- a/.agents/skills/polyxml-core-engine/SKILL.md +++ b/.agents/skills/polyxml-core-engine/SKILL.md @@ -17,10 +17,15 @@ This skill documents the high-performance design patterns and strict constraints - Never import `pyo3`, Python runtime types, or Python C-API dependencies in this crate. 2. **Streaming Event Model**: - Avoid building DOM trees in memory during parsing. - - Stream tokens using `quick-xml::events::Event` (`Event::Start`, `Event::End`, `Event::Text`, `Event::Empty`). + - Stream tokens using `quick-xml::events::Event` (`Event::Start`, `Event::End`, `Event::Text`, `Event::Empty`, `Event::CData`, `Event::GeneralRef`). 3. **Zero Intermediate Allocations**: - Parse integers, floats, and booleans directly from raw byte slices `&[u8]` using `lexical_core::parse` and custom byte parsers. - Use `smallvec::SmallVec` for small, bounded collections of attributes or namespace declarations on the stack. +4. **Complete Event Coverage in Read Loops** (found 2026-09-22): + - quick-xml SPLITS character data: `a & b` arrives as `Text` + `GeneralRef` + `Text`, and CDATA is its own event. Any accumulating loop (`parser.rs`, `transcoder.rs`, generated codecs) must handle `Text`/`CData`/`GeneralRef` or content silently vanishes. + - Resolve refs leniently like `parser.rs::append_general_ref`: `is_char_ref()`/`resolve_char_ref()` for `&#NN;`/`&#xNN;`, `escape::resolve_xml_entity()` for `amp`/`lt`/`gt`/`quot`/`apos`, else push the raw name (never error — a document the core accepts must not fail in generated code). + - Never enable `trim_text` on a reader that assembles split text: per-segment trimming loses spaces around refs (`x & y` → `x&y`). Trim the assembled buffer once at element end (the transcoder `End` arm). + - Attribute values arrive raw too — always `escape::unescape` them (`parse_attributes`, the transcoder's Start/Empty attr arms). ## 2. Testing & Verification @@ -63,7 +68,9 @@ Key mechanisms (regression-locked in `crates/polyxml-core/tests/test_issue51_aud `complexType`/`simpleType` is extracted as a uniquely named top-level type (`unique_type_name`) — its fields must never leak into the parent struct. 4. **simpleContent**: `` emits a `Text` field named `value` - carrying the extension base type. + carrying the extension base type (decoded/encoded by generated Rust codecs + via `emit_text_content_parse`/`emit_text_content_serialize` since + 2026-09-22). 5. **Pattern inheritance**: derived simple types append their base type's patterns (AND semantics) via `inherit_pattern_facets`; enum facet inheritance is intentionally skipped (`EnumDef` has no facets). diff --git a/crates/polyxml-core/src/codegen/rust/mod.rs b/crates/polyxml-core/src/codegen/rust/mod.rs index 0a7be519..51fe6341 100644 --- a/crates/polyxml-core/src/codegen/rust/mod.rs +++ b/crates/polyxml-core/src/codegen/rust/mod.rs @@ -227,7 +227,7 @@ impl RustCodegen { out.push_str("// @generated by PolyXML Compiler (https://github.com/nth-bailey/PolyXML)\n"); out.push_str( - "#![allow(dead_code, unused_imports, unused_mut, unused_variables, non_camel_case_types, non_snake_case)]\n\n", + "#![allow(dead_code, unused_imports, unused_mut, unused_variables, unused_assignments, non_camel_case_types, non_snake_case)]\n\n", ); let types_with_lifetime = if self.options.zero_copy { @@ -845,7 +845,27 @@ impl RustCodegen { out.push_str(" },\n"); out.push_str(" Cow::Owned(s) => Cow::Owned(quick_xml::escape::unescape(&s)?.into_owned()),\n"); out.push_str(" };\n"); - out.push_str(" text = raw;\n"); + out.push_str(" if text.is_empty() {\n"); + out.push_str(" text = raw;\n"); + out.push_str(" } else {\n"); + out.push_str(" text.to_mut().push_str(&raw);\n"); + out.push_str(" }\n"); + out.push_str(" }\n"); + out.push_str(" Event::CData(c) => {\n"); + out.push_str(" text.to_mut().push_str(c.as_ref());\n"); + out.push_str(" }\n"); + out.push_str(" Event::GeneralRef(r) => {\n"); + out.push_str(" if r.is_char_ref() {\n"); + out.push_str(" if let Some(ch) = r.resolve_char_ref()? {\n"); + out.push_str(" text.to_mut().push(ch);\n"); + out.push_str(" }\n"); + out.push_str( + " } else if let Some(val) = quick_xml::escape::resolve_xml_entity(r.as_ref()) {\n", + ); + out.push_str(" text.to_mut().push_str(val);\n"); + out.push_str(" } else {\n"); + out.push_str(" text.to_mut().push_str(r.as_ref());\n"); + out.push_str(" }\n"); out.push_str(" }\n"); out.push_str( " Event::End(e) if e.local_name().as_ref() == tag_name => break,\n", @@ -866,7 +886,23 @@ impl RustCodegen { out.push_str( " let unescaped = quick_xml::escape::unescape(t.as_ref())?;\n", ); - out.push_str(" text = unescaped.into_owned();\n"); + out.push_str(" text.push_str(&unescaped);\n"); + out.push_str(" }\n"); + out.push_str(" Event::CData(c) => {\n"); + out.push_str(" text.push_str(c.as_ref());\n"); + out.push_str(" }\n"); + out.push_str(" Event::GeneralRef(r) => {\n"); + out.push_str(" if r.is_char_ref() {\n"); + out.push_str(" if let Some(ch) = r.resolve_char_ref()? {\n"); + out.push_str(" text.push(ch);\n"); + out.push_str(" }\n"); + out.push_str( + " } else if let Some(val) = quick_xml::escape::resolve_xml_entity(r.as_ref()) {\n", + ); + out.push_str(" text.push_str(val);\n"); + out.push_str(" } else {\n"); + out.push_str(" text.push_str(r.as_ref());\n"); + out.push_str(" }\n"); out.push_str(" }\n"); out.push_str( " Event::End(e) if e.local_name().as_ref() == tag_name => break,\n", @@ -1090,6 +1126,8 @@ impl RustCodegen { } } + let text_meta = field_metas.iter().find(|m| m.field.kind == FieldKind::Text); + let attr_fields: Vec<_> = field_metas .iter() .filter(|m| m.field.kind == FieldKind::Attribute) @@ -1217,6 +1255,15 @@ impl RustCodegen { out.push_str(" }\n"); } + // simpleContent value field: consume the element's own text content. + // Only reachable when there are no child elements — the element loop + // above already reads through End(start). + if elem_fields.is_empty() { + if let Some(meta) = &text_meta { + self.emit_text_content_parse(out, &meta.field, &meta.rust_name, ir); + } + } + if has_patterns { out.push_str("\n let value = Self {\n"); } else { @@ -1417,6 +1464,15 @@ impl RustCodegen { out.push_str(" writer.write_event(Event::Start(start))?;\n"); + // simpleContent value: write the element's own text content between + // Start and End. Text and child content are mutually exclusive in the + // IR, so this never overlaps the element loop below. + if elem_fields.is_empty() { + if let Some(meta) = &text_meta { + self.emit_text_content_serialize(out, &meta.field, &meta.rust_name, ir); + } + } + for meta in &elem_fields { self.emit_element_serialize(out, &meta.field, &meta.rust_name, ir); } @@ -1661,6 +1717,106 @@ impl RustCodegen { } } + /// Decode the simpleContent value field from the element's own text + /// content. Emitted only when the struct has no child elements (the + /// element loop already consumes through `End(start)`), and only for + /// scalar-backed text: a simpleContent chain whose value is typed as its + /// base struct has no scalar form here and keeps its previous behavior. + fn emit_text_content_parse( + &self, + out: &mut String, + field: &FieldDef, + rust_name: &str, + ir: &SchemaIR, + ) { + let read = " let text = read_element_text(reader, start.local_name().as_ref())?;\n"; + if self.field_is_string(&field.type_ref, ir) { + out.push_str(read); + self.emit_facet_checks(out, field, "text"); + self.emit_pattern_check(out, field, "&text", ir); + let _ = writeln!(out, " var_{} = Some(text);", rust_name); + } else if let Some(num) = self.field_numeric_type(&field.type_ref, ir) { + out.push_str(read); + out.push_str(" let s = text.trim();\n"); + let _ = writeln!( + out, + " let val = s.parse::<{}>().map_err(|_| PolyXmlError::ScalarParseError {{ field: \"{}\".into(), expected: \"{}\", value: s.into() }})?;", + num, field.name, num + ); + self.emit_facet_checks(out, field, "val"); + let _ = writeln!(out, " var_{} = Some(val);", rust_name); + } else if self.field_is_bool(&field.type_ref, ir) { + out.push_str(read); + out.push_str(" let s = text.trim();\n"); + out.push_str(" let val = s == \"true\" || s == \"1\";\n"); + let _ = writeln!(out, " var_{} = Some(val);", rust_name); + } else if let Some(enum_name) = self.field_is_enum(&field.type_ref, ir) { + out.push_str(read); + out.push_str(" let s = text.trim();\n"); + let _ = writeln!( + out, + " let val = {}::from_str(s).map_err(|_| PolyXmlError::ScalarParseError {{ field: \"{}\".into(), expected: \"{}\", value: s.into() }})?;", + enum_name, field.name, enum_name + ); + let _ = writeln!(out, " var_{} = Some(val);", rust_name); + } + } + + /// Serialize the simpleContent value field as the element's own text + /// content; mirrors [`Self::emit_text_content_parse`] and skips the same + /// struct-typed values. + fn emit_text_content_serialize( + &self, + out: &mut String, + field: &FieldDef, + rust_name: &str, + ir: &SchemaIR, + ) { + let is_optional = field.cardinality.is_optional() || field.nillable; + if self.field_is_string(&field.type_ref, ir) { + if is_optional { + let _ = writeln!(out, " if let Some(ref val) = self.{} {{", rust_name); + out.push_str( + " writer.write_event(Event::Text(BytesText::new(val.as_ref())))?;\n }\n", + ); + } else { + let _ = writeln!( + out, + " writer.write_event(Event::Text(BytesText::new(self.{}.as_ref())))?;", + rust_name + ); + } + } else if self.field_numeric_type(&field.type_ref, ir).is_some() + || self.field_is_bool(&field.type_ref, ir) + { + if is_optional { + let _ = writeln!(out, " if let Some(ref val) = self.{} {{", rust_name); + out.push_str( + " writer.write_event(Event::Text(BytesText::new(&val.to_string())))?;\n }\n", + ); + } else { + let _ = writeln!( + out, + " writer.write_event(Event::Text(BytesText::new(&self.{}.to_string())))?;", + rust_name + ); + } + } else if self.field_is_enum(&field.type_ref, ir).is_some() { + if is_optional { + let _ = writeln!(out, " if let Some(ref val) = self.{} {{", rust_name); + out.push_str( + " writer.write_event(Event::Text(BytesText::new(val.as_str())))?;\n }\n", + ); + } else { + let _ = writeln!( + out, + " writer.write_event(Event::Text(BytesText::new(self.{}.as_str())))?;", + rust_name + ); + } + } + } + fn emit_facet_checks(&self, out: &mut String, field: &FieldDef, val_var: &str) { if let Some(ref facets) = field.facets { if let Some(min_len) = facets.min_length { diff --git a/crates/polyxml-core/src/transcoder.rs b/crates/polyxml-core/src/transcoder.rs index 9e60e069..211dead7 100644 --- a/crates/polyxml-core/src/transcoder.rs +++ b/crates/polyxml-core/src/transcoder.rs @@ -92,7 +92,10 @@ fn parse_dynamic_scalar(s: &str) -> Value { /// Schema-less dynamic XML -> JSON transcoder. pub fn xml_to_json_dynamic(xml: &[u8], indent: Option) -> Result> { let mut reader = Reader::from_reader(Cursor::new(xml)); - reader.config_mut().trim_text(true); + // Do NOT enable `trim_text`: quick-xml trims *each* Text event, and text is + // split at entity references, so `x & y` would become `x&y` — losing + // interior spaces around the reference. `End` trims the assembled buffer + // instead, which is the only correct place to normalize whitespace. let mut buf = Vec::new(); let mut stack: Vec = Vec::new(); @@ -110,7 +113,13 @@ pub fn xml_to_json_dynamic(xml: &[u8], indent: Option) -> Result> })?; let key = attr.key.local_name().as_ref().to_string(); let raw_val = attr.value.as_ref(); - attrs.insert(format!("@{}", key), parse_dynamic_scalar(raw_val)); + let unescaped = quick_xml::escape::unescape(raw_val).map_err(|err| { + PolyXmlError::XmlSyntaxError { + position: reader.buffer_position(), + source: quick_xml::Error::Escape(err), + } + })?; + attrs.insert(format!("@{}", key), parse_dynamic_scalar(&unescaped)); } stack.push(DynamicFrame { tag_name: tag, @@ -135,6 +144,19 @@ pub fn xml_to_json_dynamic(xml: &[u8], indent: Option) -> Result> frame.text_buf.push_str(e.as_ref()); } } + Ok(Event::GeneralRef(ref e)) => { + if let Some(frame) = stack.last_mut() { + if e.is_char_ref() { + if let Some(ch) = e.resolve_char_ref().map_err(PolyXmlError::XmlError)? { + frame.text_buf.push(ch); + } + } else if let Some(val) = quick_xml::escape::resolve_xml_entity(e.as_ref()) { + frame.text_buf.push_str(val); + } else { + frame.text_buf.push_str(e.as_ref()); + } + } + } Ok(Event::Empty(ref e)) => { let tag = e.local_name().as_ref().to_string(); let mut attrs = Map::new(); @@ -145,7 +167,13 @@ pub fn xml_to_json_dynamic(xml: &[u8], indent: Option) -> Result> })?; let key = attr.key.local_name().as_ref().to_string(); let raw_val = attr.value.as_ref(); - attrs.insert(format!("@{}", key), parse_dynamic_scalar(raw_val)); + let unescaped = quick_xml::escape::unescape(raw_val).map_err(|err| { + PolyXmlError::XmlSyntaxError { + position: reader.buffer_position(), + source: quick_xml::Error::Escape(err), + } + })?; + attrs.insert(format!("@{}", key), parse_dynamic_scalar(&unescaped)); } let val = if attrs.is_empty() { Value::Null diff --git a/crates/polyxml-core/tests/test_rust_codegen.rs b/crates/polyxml-core/tests/test_rust_codegen.rs index 7364794c..57b55b03 100644 --- a/crates/polyxml-core/tests/test_rust_codegen.rs +++ b/crates/polyxml-core/tests/test_rust_codegen.rs @@ -842,3 +842,117 @@ fn test_rust_extension_base_cycle_cut() { ); assert_eq!(beta.matches("pub beta_field").count(), 1); } + +#[test] +fn test_rust_simple_content_text_codec() { + let xsd = r#" + + + + + + + + + + + + + + + + + + + + + + +"#; + + let ir = XsdParser::new() + .parse_str(xsd) + .expect("parse simpleContent schema"); + let code = RustCodegen::new(RustOptions::default()).generate_module(&ir); + + // Numeric base: decode parses the element's text content into `value` and + // encode writes it back out. Before the fix, `value` was never assigned + // during decode, so every simpleContent instance failed with + // "Missing required field 'value'". + let measurement = section(&code, "impl<'a> Measurement"); + for expected in [ + "read_element_text(reader, start.local_name().as_ref())", + "var_value = Some(val)", + "BytesText::new(&self.value.to_string())", + ] { + assert!( + measurement.contains(expected), + "Measurement text codec missing {expected:?}:\n{measurement}" + ); + } + + // Patterned string base: the decoded text flows through the inherited + // facets (`validate_patterns`) before it is accepted. + let ticket = section(&code, "impl<'a> Ticket"); + for expected in [ + "read_element_text(reader, start.local_name().as_ref())", + "var_value = Some(text)", + "validate_patterns", + ] { + assert!( + ticket.contains(expected), + "Ticket text codec missing {expected:?}:\n{ticket}" + ); + } +} + +/// quick-xml splits text at entity references and CDATA (`a & b` arrives as +/// Text/GeneralRef/Text), so the reader must *accumulate* segments: the old +/// last-wins helper kept only the final one, silently corrupting any element or +/// simpleContent value containing an entity, char ref, or CDATA section. +#[test] +fn test_rust_read_element_text_accumulates_refs_and_cdata() { + let ir = XsdParser::new() + .parse_str(extension_xsd()) + .expect("parse extension schema"); + + for zero_copy in [true, false] { + let code = RustCodegen::new(RustOptions { + zero_copy, + ..Default::default() + }) + .generate_module(&ir); + let helper = section(&code, "fn read_element_text"); + for expected in [ + "Event::CData(c)", + "Event::GeneralRef(r)", + "r.is_char_ref()", + "r.resolve_char_ref()", + "resolve_xml_entity(r.as_ref())", + ] { + assert!( + helper.contains(expected), + "zero_copy={zero_copy} helper missing {expected:?}:\n{helper}" + ); + } + // Segments append instead of overwriting the previous one. + if zero_copy { + assert!( + helper.contains("text.to_mut().push_str(&raw)"), + "zero_copy helper must append segments:\n{helper}" + ); + } else { + assert!( + helper.contains("text.push_str(&unescaped)"), + "owned helper must append segments:\n{helper}" + ); + assert!( + !helper.contains("text = unescaped.into_owned()"), + "last-wins overwrite resurfaced:\n{helper}" + ); + } + } +} diff --git a/crates/polyxml-core/tests/test_transcoder.rs b/crates/polyxml-core/tests/test_transcoder.rs index 51b6afd5..ec039c2a 100644 --- a/crates/polyxml-core/tests/test_transcoder.rs +++ b/crates/polyxml-core/tests/test_transcoder.rs @@ -151,3 +151,33 @@ fn test_transcoder_errors() { let err = json_to_xml(b"{ not json }", None, None, None, None, None).unwrap_err(); assert!(err.to_string().contains("Invalid JSON")); } + +/// quick-xml splits text at entity references (Text/GeneralRef/Text) and reports +/// CDATA as its own event. The transcoder used to fall through to `_ => {}` for +/// general refs (dropping them) and left attribute entities raw, corrupting +/// xml -> json -> xml round trips. +#[test] +fn test_schemaless_preserves_refs_cdata_and_attr_entities() { + let xml = br#"x & y]]>AB"#; + let json_bytes = xml_to_json(xml, None, None, true).expect("xml_to_json"); + let json_str = String::from_utf8(json_bytes).unwrap(); + + for expected in [ + "\"a\":\"x & y\"", + "\"b\":\"raw & \"", + "\"c\":\"AB\"", + "\"@name\":\"A & B\"", + ] { + assert!( + json_str.contains(expected), + "missing {expected} in:\n{json_str}" + ); + } + + let roundtrip = + json_to_xml(json_str.as_bytes(), None, None, None, None, None).expect("json_to_xml"); + let rt = String::from_utf8(roundtrip).unwrap(); + for expected in ["x & y", "A & B"] { + assert!(rt.contains(expected), "roundtrip lost {expected} in:\n{rt}"); + } +} From dce03c1c1633e22c73821e0e7cbd1a728922b97a Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 09:00:00 -0500 Subject: [PATCH 08/10] feat(rust-codegen): perfect-hash element tag dispatch via --feature phf Emit a per-struct __{Struct}ElementId enum plus a phf::Map keyed by tag name at Event::Start/Event::Empty when the `phf` feature is requested (#49). Default output stays byte-identical; attrs/unions/enum values keep match dispatch. Criterion bench (benches/tag_dispatch.rs, tiers 16/120/600/1500, hit+miss) and perf counters (scripts/perf_stat.{c,sh}) show match's L1I misses grow 277x from 120->1500 tags while phf stays flat; throughput crossover lands near 835 elements, so >=~900 is the documented auto-enable threshold. Closes the uniquify-parity gap discovered while testing group emission. Also adds scripts/memcap.sh, a portable 60%-of-available-RAM guard (systemd-run scope with ulimit fallback) wired into the benchmark entry points so heavy builds OOM-kill inside their cgroup instead of freezing the host. Compile-time/size comparison at the 600/1500 tiers exceeded the cap for every variant (scales with field count, not dispatch) and is documented as deferred to a follow-up issue. Docs: docs/benchmarks/rust-phf-dispatch.md (throughput, counters, size, threshold, repro), memcap notes in docs/benchmarks.md and benchmarks/README.md, SKILL.md updated. --- .../skills/polyxml-codegen-workflow/SKILL.md | 32 +- Cargo.lock | 61 + benchmarks/README.md | 10 + benchmarks/cli/benchmark.sh | 6 + benchmarks/run_all.sh | 8 + crates/polyxml-cli/src/completions.rs | 1 + crates/polyxml-cli/src/main.rs | 3 + crates/polyxml-cli/src/options.rs | 4 +- crates/polyxml-cli/tests/test_cli.rs | 52 +- crates/polyxml-core/Cargo.toml | 6 + .../benches/support/tag_dispatch_fixtures.rs | 6766 +++++++++++++++++ crates/polyxml-core/benches/tag_dispatch.rs | 119 + crates/polyxml-core/src/codegen/rust/mod.rs | 141 +- .../polyxml-core/tests/test_rust_codegen.rs | 174 + docs/benchmarks.md | 9 + docs/benchmarks/rust-phf-dispatch.md | 282 + scripts/gen_tag_dispatch_fixtures.py | 140 + scripts/memcap.sh | 140 + scripts/perf_stat.c | 150 + scripts/perf_stat.sh | 17 + scripts/verify_codegen.sh | 7 +- 21 files changed, 8114 insertions(+), 14 deletions(-) create mode 100644 crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs create mode 100644 crates/polyxml-core/benches/tag_dispatch.rs create mode 100644 docs/benchmarks/rust-phf-dispatch.md create mode 100755 scripts/gen_tag_dispatch_fixtures.py create mode 100755 scripts/memcap.sh create mode 100644 scripts/perf_stat.c create mode 100755 scripts/perf_stat.sh diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 8802428c..7ac43647 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -287,8 +287,38 @@ derived types inherit their base's patterns at parse time): `--zero-copy=false --feature zero-copy` is still rejected as a contradiction. - Defaults remain unchanged, including C# record classes, Rust zero-copy, and Python slots/kw-only. Expose only implemented styles/features. The issue's - future examples (phf, aot, Python plain class, C# mutable struct) are not yet + future examples (aot, Python plain class, C# mutable struct) are not yet generator capabilities. +- Rust features today: `zero-copy`, `rkyv`, `phf`. `--feature phf` (issue #49) + emits a per-struct `__{Struct}ElementId` enum plus a + `__{STRUCT}_ELEMENT_DISPATCH: ::phf::Map<&'static str, ...>` static built with + `phf_codegen`, and routes both `Event::Start` and `Event::Empty` child arms + through `.get(e.local_name().as_ref())`; union branch tags all map to one + variant. Default (feature off) output must stay byte-identical — lock both + directions in the codegen test (`test_rust_phf_dispatch_emission`). Attr, + union, and enum-value `match` sites are intentionally left as `match`. + Consuming crates need `phf = "0.14"`. Benchmarks/docs live in + `benches/tag_dispatch.rs` (regenerate fixtures via + `scripts/gen_tag_dispatch_fixtures.py`) and + `docs/benchmarks/rust-phf-dispatch.md`; hardware counters on hosts without + the `perf` binary go through `scripts/perf_stat.sh` (`perf_event_open`). +- **Cap heavy builds/benches with `scripts/memcap.sh`.** Benchmark entry + points (`benchmarks/run_all.sh`, `benchmarks/cli/benchmark.sh`, + `scripts/perf_stat.sh`) already re-exec through it; ad-hoc + `cargo bench`/`--release` builds of large generated crates should be + wrapped too. It caps the tree at `POLYXML_MEMCAP_PCT` (default 60%) of + available RAM in a systemd scope (`MemorySwapMax=0`, `ulimit -v` + fallback): an over-limit build gets OOM-killed inside its cgroup + (exit 137) instead of freezing the host — uncapped 1500-element builds + froze a 7.7-GiB WSL box repeatedly before this guard existed. Wrap + measure/compile-size scripts per build step (own scope each) so one + OOM kill doesn't abort the rest; set `POLYXML_MEMCAP_LEVEL` is automatic, + `POLYXML_MEMCAP_DISABLE=1` opts out. Known datum: generated consumer + crates at **600 and 1500 elements need >=3.2 GiB for a single `rustc`** + (both the `match` and `phf` variants — it scales with field count, not + match-arm count), so compile-time/size measurements at those tiers are + deferred to a follow-up issue and must not be retried uncapped on + 8-GiB-class hosts. - Shared CLI options apply to every `--lang`, not just the preceding one. Use per-target manifest entries for heterogeneous configurations. Schema-less `generate` must not silently ignore generation overrides. diff --git a/Cargo.lock b/Cargo.lock index 77441c50..dd5114eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -547,6 +547,59 @@ dependencies = [ "winapi", ] +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b585a510fb76fdebead6897982ef2a03a21d8e6cbcca904999742a4afc6ffe" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + [[package]] name = "polyxml" version = "0.20.1" @@ -556,6 +609,8 @@ dependencies = [ "lexical-core", "memchr", "minijinja", + "phf", + "phf_codegen", "pretty_assertions", "quick-xml", "serde", @@ -828,6 +883,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + [[package]] name = "smallvec" version = "1.16.1" diff --git a/benchmarks/README.md b/benchmarks/README.md index b9d2da29..fcb4e54a 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -25,6 +25,7 @@ equivalent) and never at this root. | Suite | Location | Tooling | How to run | | :--- | :--- | :--- | :--- | | Rust core engine | [`crates/polyxml-core/benches/`](../crates/polyxml-core/benches/) | [Criterion.rs](https://github.com/bheisler/criterion.rs) | `cargo bench --bench core_benchmarks` | +| Rust tag dispatch | [`crates/polyxml-core/benches/`](../crates/polyxml-core/benches/) | [Criterion.rs](https://github.com/bheisler/criterion.rs) + `perf stat` | `cargo bench --bench tag_dispatch` ([results](../docs/benchmarks/rust-phf-dispatch.md)) | | Python comparative | [`python/`](python/README.md) | Custom CLI suite | `python -m benchmarks.python` or `./benchmarks/run_all.sh` | | CLI startup | [`cli/`](cli/README.md) | hyperfine | `./benchmarks/cli/benchmark.sh` | | Java four-runtime | [`java/`](java/README.md) | JMH (Maven) | See [`java/README.md`](java/README.md) | @@ -62,6 +63,15 @@ Suite READMEs document their own tooling; these rules apply to all of them: claim; a single run on a shared machine is noise. 4. Published figures live in [`docs/benchmarks.md`](../docs/benchmarks.md) — update them from a full, idle-host run, not from a smoke. +5. **Run under a memory cap on shared/small hosts.** Heavy builds and bench + runs go through [`scripts/memcap.sh`](../scripts/memcap.sh), which caps + the run at 60% of available RAM in an isolated cgroup (kernel OOM-kills + only the runaway process instead of freezing the host; `ulimit` + fallback where systemd is unavailable). The suite entry points + (`run_all.sh`, `cli/benchmark.sh`, `scripts/perf_stat.sh`) already + re-exec through it; wrap any ad-hoc `cargo bench`/`--release` build the + same way. Tune with `POLYXML_MEMCAP_PCT`, opt out with + `POLYXML_MEMCAP_DISABLE=1`. ## Adding a suite diff --git a/benchmarks/cli/benchmark.sh b/benchmarks/cli/benchmark.sh index c890700c..25e3da5b 100755 --- a/benchmarks/cli/benchmark.sh +++ b/benchmarks/cli/benchmark.sh @@ -2,7 +2,13 @@ # Measure fresh-process CLI startup and argument validation with hyperfine. set -euo pipefail repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd) +self="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/$(basename -- "${BASH_SOURCE[0]}")" cd "$repo_root" +# Release builds + hyperfine under a RAM cap (see scripts/memcap.sh); the +# LEVEL guard makes the re-exec idempotent. POLYXML_MEMCAP_DISABLE=1 opts out. +if [ -z "${POLYXML_MEMCAP_LEVEL:-}" ]; then + exec "$repo_root/scripts/memcap.sh" "$self" "$@" +fi command -v hyperfine >/dev/null || { echo 'Install hyperfine to run this benchmark.' >&2; exit 1; } cargo build --release -p polyxml-cli scratch_dir=$(mktemp -d) diff --git a/benchmarks/run_all.sh b/benchmarks/run_all.sh index aba60c44..435b6a46 100755 --- a/benchmarks/run_all.sh +++ b/benchmarks/run_all.sh @@ -8,6 +8,14 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +# Cap the whole run at POLYXML_MEMCAP_PCT (default 60%) of available RAM via +# scripts/memcap.sh: release builds + Criterion can exhaust small hosts and +# freeze them. The LEVEL guard makes the re-exec idempotent (no loop) on +# hosts without a systemd user manager; POLYXML_MEMCAP_DISABLE=1 opts out. +if [ -z "${POLYXML_MEMCAP_LEVEL:-}" ]; then + exec "${ROOT_DIR}/scripts/memcap.sh" "${BASH_SOURCE[0]}" "$@" +fi + echo "======================================================================" echo "⚡ PolyXML Performance & Benchmarking Suite" echo "======================================================================" diff --git a/crates/polyxml-cli/src/completions.rs b/crates/polyxml-cli/src/completions.rs index 48dad809..8d51bce0 100644 --- a/crates/polyxml-cli/src/completions.rs +++ b/crates/polyxml-cli/src/completions.rs @@ -106,6 +106,7 @@ pub fn candidates(kind: &str, words: &[String]) -> Vec { "feature" => &[ "zero-copy", "rkyv", + "phf", "builder", "direct-codec", "slots", diff --git a/crates/polyxml-cli/src/main.rs b/crates/polyxml-cli/src/main.rs index d69e3ecb..9c954be1 100644 --- a/crates/polyxml-cli/src/main.rs +++ b/crates/polyxml-cli/src/main.rs @@ -265,6 +265,7 @@ fn run_generate(args: GenerateArgs) -> Result<(), Box> { builder: None, codec: None, rkyv: None, + phf: None, custom_header: args.custom_header.as_deref(), }; let resolved_options = languages @@ -482,6 +483,7 @@ pub struct TargetEmitOptions<'a> { pub builder: Option, pub codec: Option<&'a str>, pub rkyv: Option, + pub phf: Option, pub custom_header: Option<&'a str>, } @@ -540,6 +542,7 @@ fn emit_target_code( emit_root_aliases: true, emit_codecs: opts.codecs.unwrap_or(true), emit_rkyv: opts.rkyv.unwrap_or(false), + phf: opts.phf.unwrap_or(false), custom_header: opts.custom_header.map(|s| s.to_string()), }; diff --git a/crates/polyxml-cli/src/options.rs b/crates/polyxml-cli/src/options.rs index 2c05ef49..e54e9932 100644 --- a/crates/polyxml-cli/src/options.rs +++ b/crates/polyxml-cli/src/options.rs @@ -23,6 +23,7 @@ pub(crate) fn target_options(target: &config::TargetConfig) -> TargetEmitOptions builder: None, codec: None, rkyv: None, + phf: None, custom_header: target.custom_header.as_deref(), } } @@ -100,7 +101,7 @@ impl<'a> TargetEmitOptions<'a> { return Err(invalid("slots and kw_only are Python-only options.")); } let supported_features: &[&str] = match target { - "rust" => &["zero-copy", "rkyv"], + "rust" => &["zero-copy", "rkyv", "phf"], "java" => &["builder", "direct-codec"], "python" => &["slots", "kw-only"], _ => &[], @@ -112,6 +113,7 @@ impl<'a> TargetEmitOptions<'a> { let option = match feature.as_str() { "zero-copy" => &mut self.zero_copy, "rkyv" => &mut self.rkyv, + "phf" => &mut self.phf, "builder" => &mut self.builder, "slots" => &mut self.slots, "kw-only" => &mut self.kw_only, diff --git a/crates/polyxml-cli/tests/test_cli.rs b/crates/polyxml-cli/tests/test_cli.rs index 5ac219cb..f76d613b 100644 --- a/crates/polyxml-cli/tests/test_cli.rs +++ b/crates/polyxml-cli/tests/test_cli.rs @@ -1430,6 +1430,51 @@ fn test_cli_rust_rkyv_feature() { assert!(rs_content.contains("#[cfg_attr(feature = \"rkyv\", rkyv(check_bytes))]")); } +#[test] +fn test_cli_rust_phf_feature() { + let dir = tempdir().unwrap(); + let schema_file = dir.path().join("event.xsd"); + fs::write( + &schema_file, + r#" + + + + + + + "#, + ) + .unwrap(); + + let out_dir = dir.path().join("rust_out"); + let status = Command::new(env!("CARGO_BIN_EXE_polyxml")) + .args([ + "generate", + schema_file.to_str().unwrap(), + "--lang", + "rust", + "--feature", + "phf", + "-o", + out_dir.to_str().unwrap(), + ]) + .status() + .expect("Failed to execute generate with phf"); + assert!(status.success()); + + let rs_content = fs::read_to_string(out_dir.join("event.rs")).unwrap(); + assert!(rs_content.contains("enum __EventElementId")); + assert!( + rs_content.contains( + "static __EVENT_ELEMENT_DISPATCH: ::phf::Map<&'static str, __EventElementId>" + ), + "phf dispatch table missing:\n{rs_content}" + ); + assert!(rs_content.contains("(\"Id\", __EventElementId::Id)")); + assert!(rs_content.contains("Some(&__EventElementId::Id) => {")); +} + #[test] fn test_cli_build_polyxml_manifest_with_new_features() { let dir = tempdir().unwrap(); @@ -1750,7 +1795,10 @@ fn unified_options_fail_before_schema_io_including_dry_run() { vec!["--lang", "python", "--feature", "rkyv"], "feature 'rkyv'", ), - (vec!["--lang", "rust", "--feature", "phf"], "feature 'phf'"), + ( + vec!["--lang", "rust", "--feature", "builder"], + "feature 'builder'", + ), ( vec![ "--lang", @@ -2040,7 +2088,7 @@ fn completion_candidates_follow_target_validation() { ( "feature", vec!["generate", "--lang", "rust"], - vec!["zero-copy", "rkyv"], + vec!["zero-copy", "rkyv", "phf"], ), ("feature", vec!["generate", "--backend=pydantic"], vec![]), ( diff --git a/crates/polyxml-core/Cargo.toml b/crates/polyxml-core/Cargo.toml index 4f0327d7..409ce052 100644 --- a/crates/polyxml-core/Cargo.toml +++ b/crates/polyxml-core/Cargo.toml @@ -20,12 +20,18 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" minijinja = { version = "2.8", features = ["builtins"] } heck = "0.5" +phf_codegen = "0.14.0" [dev-dependencies] criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support"] } +phf = { version = "0.14.0", features = ["macros"] } pretty_assertions = "1.4" tempfile = "3" [[bench]] name = "core_benchmarks" harness = false + +[[bench]] +name = "tag_dispatch" +harness = false diff --git a/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs b/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs new file mode 100644 index 00000000..d87af7fe --- /dev/null +++ b/crates/polyxml-core/benches/support/tag_dispatch_fixtures.rs @@ -0,0 +1,6766 @@ +// GENERATED by scripts/gen_tag_dispatch_fixtures.py — do not edit. +// Tag lists, string-match baselines, and phf tables for the issue #49 +// tag_dispatch Criterion bench. Regenerate with: +// python3 scripts/gen_tag_dispatch_fixtures.py + +#![allow(clippy::all)] + +// --- Tier 16 (small) --- +pub const TAGS_16: [&str; 16] = [ + "AmountPositionDate", + "AuthenticationPriorityReport", + "CategoryRoutingReport", + "CurrencyMarginSub", + "InstructionClearingReport", + "InstructionInstruction1", + "ReconciliationMarginDate", + "ReconciliationReconciliationId", + "ReferenceChannel", + "ReferenceReconciliation2", + "SettlementAmountDate", + "StatusReconciliationIndicator", + "StatusSequence1", + "TransmissionSequence2", + "ValueDateClearingCode", + "VersionMarginId", +]; + +pub fn match_dispatch_16(tag: &str) -> Option { + match tag { + "AmountPositionDate" => Some(0), + "AuthenticationPriorityReport" => Some(1), + "CategoryRoutingReport" => Some(2), + "CurrencyMarginSub" => Some(3), + "InstructionClearingReport" => Some(4), + "InstructionInstruction1" => Some(5), + "ReconciliationMarginDate" => Some(6), + "ReconciliationReconciliationId" => Some(7), + "ReferenceChannel" => Some(8), + "ReferenceReconciliation2" => Some(9), + "SettlementAmountDate" => Some(10), + "StatusReconciliationIndicator" => Some(11), + "StatusSequence1" => Some(12), + "TransmissionSequence2" => Some(13), + "ValueDateClearingCode" => Some(14), + "VersionMarginId" => Some(15), + _ => None, + } +} + +pub static PHF_16: phf::Map<&'static str, u32> = phf::phf_map! { + "AmountPositionDate" => 0u32, + "AuthenticationPriorityReport" => 1u32, + "CategoryRoutingReport" => 2u32, + "CurrencyMarginSub" => 3u32, + "InstructionClearingReport" => 4u32, + "InstructionInstruction1" => 5u32, + "ReconciliationMarginDate" => 6u32, + "ReconciliationReconciliationId" => 7u32, + "ReferenceChannel" => 8u32, + "ReferenceReconciliation2" => 9u32, + "SettlementAmountDate" => 10u32, + "StatusReconciliationIndicator" => 11u32, + "StatusSequence1" => 12u32, + "TransmissionSequence2" => 13u32, + "ValueDateClearingCode" => 14u32, + "VersionMarginId" => 15u32, +}; + +// --- Tier 120 (medium) --- +pub const TAGS_120: [&str; 120] = [ + "AccountAccountSub", + "AdjustmentAdjustment", + "AdjustmentAuthenticationDate", + "AdjustmentBeneficiary", + "AdjustmentSettlementId", + "AdjustmentVersionIndicator", + "AmountAccount1", + "AmountCategoryType", + "AmountChannel", + "AmountClearingReport", + "AmountInstructionId", + "AmountSettlement2", + "AuthenticationAuthentication", + "AuthenticationMarginType", + "AuthenticationPositionIndicator", + "BeneficiaryPriorityIndicator", + "BeneficiaryStatementCount", + "BeneficiaryStatementId", + "CategoryAmount", + "CategoryChannel1", + "CategoryParty1", + "ChannelAccountType", + "ChannelChannel", + "ClearingBeneficiaryFlag", + "ClearingStatement1", + "CollateralAuthentication", + "CollateralCategory", + "CollateralPositionType", + "CollateralPriorityIndicator", + "CollateralReferenceCode", + "CollateralSettlement1", + "CollateralTransactionType", + "CollateralTransmissionReport", + "CollateralVersionSub", + "CurrencyAdjustmentIndicator", + "CurrencyChannelFlag", + "CurrencyMargin1", + "CurrencyPartyType", + "CurrencyPriorityType", + "CurrencyReference", + "CurrencyValueDateId", + "CurrencyVersionIndicator", + "IdentificationAdjustment", + "IdentificationBeneficiaryType", + "IdentificationRoutingReport", + "IdentificationSequenceCode", + "IdentificationSettlement1", + "IdentificationStatementSub", + "IdentificationVersionId", + "InstructionAdjustmentSub", + "InstructionCategory2", + "InstructionPositionId", + "InstructionPositionSub", + "InstructionPriority1", + "InstructionStatement", + "InstructionStatementFlag", + "MarginRoutingReport", + "MarginValueDateFlag", + "PartyAuthenticationId", + "PositionAdjustmentId", + "PositionCategory", + "PositionReconciliationType", + "PositionTransmissionType", + "PositionVersion1", + "PriorityChannelCode", + "PriorityStatementCount", + "PriorityStatus1", + "PriorityVersionCode", + "ReconciliationSettlementFlag", + "ReconciliationStatus", + "ReconciliationValueDateCount", + "ReferenceAdjustment", + "ReferenceClearing", + "ReferenceClearingSub", + "ReferencePartyId", + "ReferencePositionCode", + "ReferenceSequenceType", + "ReferenceVersion1", + "RemittanceCollateralReport", + "RemittancePartyIndicator", + "RemittancePriorityFlag", + "RemittanceSettlement", + "RemittanceStatus", + "RemittanceValueDateCount", + "RoutingAccountId", + "RoutingAdjustmentCount", + "RoutingCurrencyType", + "RoutingReferenceCount", + "RoutingStatusDate", + "SequenceAuthenticationIndicator", + "SequenceBeneficiaryFlag", + "SequenceIdentificationIndicator", + "SequenceMargin", + "SequencePartyFlag", + "SequencePrioritySub", + "SequenceStatusIndicator", + "SettlementAccount", + "SettlementAuthenticationIndicator", + "SettlementReconciliationFlag", + "StatusCurrency", + "StatusStatementCode", + "StatusTransmissionId", + "StatusVersion", + "TransactionCategoryReport", + "TransactionChannelReport", + "TransactionCollateral", + "TransactionPosition", + "TransmissionAdjustment2", + "TransmissionAuthentication1", + "TransmissionCollateralFlag", + "TransmissionMarginFlag", + "TransmissionPartyFlag", + "ValueDateAccountSub", + "ValueDateCollateralId", + "ValueDateCurrencySub", + "ValueDateMargin", + "ValueDateTransactionCode", + "VersionAmountReport", + "VersionCategory2", + "VersionRemittanceIndicator", +]; + +pub fn match_dispatch_120(tag: &str) -> Option { + match tag { + "AccountAccountSub" => Some(0), + "AdjustmentAdjustment" => Some(1), + "AdjustmentAuthenticationDate" => Some(2), + "AdjustmentBeneficiary" => Some(3), + "AdjustmentSettlementId" => Some(4), + "AdjustmentVersionIndicator" => Some(5), + "AmountAccount1" => Some(6), + "AmountCategoryType" => Some(7), + "AmountChannel" => Some(8), + "AmountClearingReport" => Some(9), + "AmountInstructionId" => Some(10), + "AmountSettlement2" => Some(11), + "AuthenticationAuthentication" => Some(12), + "AuthenticationMarginType" => Some(13), + "AuthenticationPositionIndicator" => Some(14), + "BeneficiaryPriorityIndicator" => Some(15), + "BeneficiaryStatementCount" => Some(16), + "BeneficiaryStatementId" => Some(17), + "CategoryAmount" => Some(18), + "CategoryChannel1" => Some(19), + "CategoryParty1" => Some(20), + "ChannelAccountType" => Some(21), + "ChannelChannel" => Some(22), + "ClearingBeneficiaryFlag" => Some(23), + "ClearingStatement1" => Some(24), + "CollateralAuthentication" => Some(25), + "CollateralCategory" => Some(26), + "CollateralPositionType" => Some(27), + "CollateralPriorityIndicator" => Some(28), + "CollateralReferenceCode" => Some(29), + "CollateralSettlement1" => Some(30), + "CollateralTransactionType" => Some(31), + "CollateralTransmissionReport" => Some(32), + "CollateralVersionSub" => Some(33), + "CurrencyAdjustmentIndicator" => Some(34), + "CurrencyChannelFlag" => Some(35), + "CurrencyMargin1" => Some(36), + "CurrencyPartyType" => Some(37), + "CurrencyPriorityType" => Some(38), + "CurrencyReference" => Some(39), + "CurrencyValueDateId" => Some(40), + "CurrencyVersionIndicator" => Some(41), + "IdentificationAdjustment" => Some(42), + "IdentificationBeneficiaryType" => Some(43), + "IdentificationRoutingReport" => Some(44), + "IdentificationSequenceCode" => Some(45), + "IdentificationSettlement1" => Some(46), + "IdentificationStatementSub" => Some(47), + "IdentificationVersionId" => Some(48), + "InstructionAdjustmentSub" => Some(49), + "InstructionCategory2" => Some(50), + "InstructionPositionId" => Some(51), + "InstructionPositionSub" => Some(52), + "InstructionPriority1" => Some(53), + "InstructionStatement" => Some(54), + "InstructionStatementFlag" => Some(55), + "MarginRoutingReport" => Some(56), + "MarginValueDateFlag" => Some(57), + "PartyAuthenticationId" => Some(58), + "PositionAdjustmentId" => Some(59), + "PositionCategory" => Some(60), + "PositionReconciliationType" => Some(61), + "PositionTransmissionType" => Some(62), + "PositionVersion1" => Some(63), + "PriorityChannelCode" => Some(64), + "PriorityStatementCount" => Some(65), + "PriorityStatus1" => Some(66), + "PriorityVersionCode" => Some(67), + "ReconciliationSettlementFlag" => Some(68), + "ReconciliationStatus" => Some(69), + "ReconciliationValueDateCount" => Some(70), + "ReferenceAdjustment" => Some(71), + "ReferenceClearing" => Some(72), + "ReferenceClearingSub" => Some(73), + "ReferencePartyId" => Some(74), + "ReferencePositionCode" => Some(75), + "ReferenceSequenceType" => Some(76), + "ReferenceVersion1" => Some(77), + "RemittanceCollateralReport" => Some(78), + "RemittancePartyIndicator" => Some(79), + "RemittancePriorityFlag" => Some(80), + "RemittanceSettlement" => Some(81), + "RemittanceStatus" => Some(82), + "RemittanceValueDateCount" => Some(83), + "RoutingAccountId" => Some(84), + "RoutingAdjustmentCount" => Some(85), + "RoutingCurrencyType" => Some(86), + "RoutingReferenceCount" => Some(87), + "RoutingStatusDate" => Some(88), + "SequenceAuthenticationIndicator" => Some(89), + "SequenceBeneficiaryFlag" => Some(90), + "SequenceIdentificationIndicator" => Some(91), + "SequenceMargin" => Some(92), + "SequencePartyFlag" => Some(93), + "SequencePrioritySub" => Some(94), + "SequenceStatusIndicator" => Some(95), + "SettlementAccount" => Some(96), + "SettlementAuthenticationIndicator" => Some(97), + "SettlementReconciliationFlag" => Some(98), + "StatusCurrency" => Some(99), + "StatusStatementCode" => Some(100), + "StatusTransmissionId" => Some(101), + "StatusVersion" => Some(102), + "TransactionCategoryReport" => Some(103), + "TransactionChannelReport" => Some(104), + "TransactionCollateral" => Some(105), + "TransactionPosition" => Some(106), + "TransmissionAdjustment2" => Some(107), + "TransmissionAuthentication1" => Some(108), + "TransmissionCollateralFlag" => Some(109), + "TransmissionMarginFlag" => Some(110), + "TransmissionPartyFlag" => Some(111), + "ValueDateAccountSub" => Some(112), + "ValueDateCollateralId" => Some(113), + "ValueDateCurrencySub" => Some(114), + "ValueDateMargin" => Some(115), + "ValueDateTransactionCode" => Some(116), + "VersionAmountReport" => Some(117), + "VersionCategory2" => Some(118), + "VersionRemittanceIndicator" => Some(119), + _ => None, + } +} + +pub static PHF_120: phf::Map<&'static str, u32> = phf::phf_map! { + "AccountAccountSub" => 0u32, + "AdjustmentAdjustment" => 1u32, + "AdjustmentAuthenticationDate" => 2u32, + "AdjustmentBeneficiary" => 3u32, + "AdjustmentSettlementId" => 4u32, + "AdjustmentVersionIndicator" => 5u32, + "AmountAccount1" => 6u32, + "AmountCategoryType" => 7u32, + "AmountChannel" => 8u32, + "AmountClearingReport" => 9u32, + "AmountInstructionId" => 10u32, + "AmountSettlement2" => 11u32, + "AuthenticationAuthentication" => 12u32, + "AuthenticationMarginType" => 13u32, + "AuthenticationPositionIndicator" => 14u32, + "BeneficiaryPriorityIndicator" => 15u32, + "BeneficiaryStatementCount" => 16u32, + "BeneficiaryStatementId" => 17u32, + "CategoryAmount" => 18u32, + "CategoryChannel1" => 19u32, + "CategoryParty1" => 20u32, + "ChannelAccountType" => 21u32, + "ChannelChannel" => 22u32, + "ClearingBeneficiaryFlag" => 23u32, + "ClearingStatement1" => 24u32, + "CollateralAuthentication" => 25u32, + "CollateralCategory" => 26u32, + "CollateralPositionType" => 27u32, + "CollateralPriorityIndicator" => 28u32, + "CollateralReferenceCode" => 29u32, + "CollateralSettlement1" => 30u32, + "CollateralTransactionType" => 31u32, + "CollateralTransmissionReport" => 32u32, + "CollateralVersionSub" => 33u32, + "CurrencyAdjustmentIndicator" => 34u32, + "CurrencyChannelFlag" => 35u32, + "CurrencyMargin1" => 36u32, + "CurrencyPartyType" => 37u32, + "CurrencyPriorityType" => 38u32, + "CurrencyReference" => 39u32, + "CurrencyValueDateId" => 40u32, + "CurrencyVersionIndicator" => 41u32, + "IdentificationAdjustment" => 42u32, + "IdentificationBeneficiaryType" => 43u32, + "IdentificationRoutingReport" => 44u32, + "IdentificationSequenceCode" => 45u32, + "IdentificationSettlement1" => 46u32, + "IdentificationStatementSub" => 47u32, + "IdentificationVersionId" => 48u32, + "InstructionAdjustmentSub" => 49u32, + "InstructionCategory2" => 50u32, + "InstructionPositionId" => 51u32, + "InstructionPositionSub" => 52u32, + "InstructionPriority1" => 53u32, + "InstructionStatement" => 54u32, + "InstructionStatementFlag" => 55u32, + "MarginRoutingReport" => 56u32, + "MarginValueDateFlag" => 57u32, + "PartyAuthenticationId" => 58u32, + "PositionAdjustmentId" => 59u32, + "PositionCategory" => 60u32, + "PositionReconciliationType" => 61u32, + "PositionTransmissionType" => 62u32, + "PositionVersion1" => 63u32, + "PriorityChannelCode" => 64u32, + "PriorityStatementCount" => 65u32, + "PriorityStatus1" => 66u32, + "PriorityVersionCode" => 67u32, + "ReconciliationSettlementFlag" => 68u32, + "ReconciliationStatus" => 69u32, + "ReconciliationValueDateCount" => 70u32, + "ReferenceAdjustment" => 71u32, + "ReferenceClearing" => 72u32, + "ReferenceClearingSub" => 73u32, + "ReferencePartyId" => 74u32, + "ReferencePositionCode" => 75u32, + "ReferenceSequenceType" => 76u32, + "ReferenceVersion1" => 77u32, + "RemittanceCollateralReport" => 78u32, + "RemittancePartyIndicator" => 79u32, + "RemittancePriorityFlag" => 80u32, + "RemittanceSettlement" => 81u32, + "RemittanceStatus" => 82u32, + "RemittanceValueDateCount" => 83u32, + "RoutingAccountId" => 84u32, + "RoutingAdjustmentCount" => 85u32, + "RoutingCurrencyType" => 86u32, + "RoutingReferenceCount" => 87u32, + "RoutingStatusDate" => 88u32, + "SequenceAuthenticationIndicator" => 89u32, + "SequenceBeneficiaryFlag" => 90u32, + "SequenceIdentificationIndicator" => 91u32, + "SequenceMargin" => 92u32, + "SequencePartyFlag" => 93u32, + "SequencePrioritySub" => 94u32, + "SequenceStatusIndicator" => 95u32, + "SettlementAccount" => 96u32, + "SettlementAuthenticationIndicator" => 97u32, + "SettlementReconciliationFlag" => 98u32, + "StatusCurrency" => 99u32, + "StatusStatementCode" => 100u32, + "StatusTransmissionId" => 101u32, + "StatusVersion" => 102u32, + "TransactionCategoryReport" => 103u32, + "TransactionChannelReport" => 104u32, + "TransactionCollateral" => 105u32, + "TransactionPosition" => 106u32, + "TransmissionAdjustment2" => 107u32, + "TransmissionAuthentication1" => 108u32, + "TransmissionCollateralFlag" => 109u32, + "TransmissionMarginFlag" => 110u32, + "TransmissionPartyFlag" => 111u32, + "ValueDateAccountSub" => 112u32, + "ValueDateCollateralId" => 113u32, + "ValueDateCurrencySub" => 114u32, + "ValueDateMargin" => 115u32, + "ValueDateTransactionCode" => 116u32, + "VersionAmountReport" => 117u32, + "VersionCategory2" => 118u32, + "VersionRemittanceIndicator" => 119u32, +}; + +// --- Tier 600 (large) --- +pub const TAGS_600: [&str; 600] = [ + "AccountAccount", + "AccountAccountId", + "AccountAmount", + "AccountAuthenticationReport", + "AccountCategoryCode", + "AccountClearingCount", + "AccountInstruction2", + "AccountInstructionDate", + "AccountMarginId", + "AccountMarginIndicator", + "AccountPartyCount", + "AccountRoutingDate", + "AccountSettlement1", + "AccountStatement", + "AccountTransactionFlag", + "AccountTransactionReport", + "AccountTransmission", + "AccountTransmissionCount", + "AdjustmentAccountId", + "AdjustmentAuthenticationIndicator", + "AdjustmentAuthenticationSub", + "AdjustmentCategoryId", + "AdjustmentCategorySub", + "AdjustmentIdentificationType", + "AdjustmentMarginFlag", + "AdjustmentPosition", + "AdjustmentPriorityFlag", + "AdjustmentReferenceFlag", + "AdjustmentRemittanceReport", + "AdjustmentRoutingCount", + "AdjustmentSequence1", + "AdjustmentSettlementDate", + "AdjustmentSettlementType", + "AdjustmentStatementDate", + "AdjustmentStatusFlag", + "AdjustmentTransactionType", + "AdjustmentTransmissionReport", + "AdjustmentValueDateDate", + "AdjustmentValueDateSub", + "AdjustmentVersionCount", + "AmountAmount", + "AmountAuthentication", + "AmountAuthenticationId", + "AmountBeneficiaryCount", + "AmountBeneficiaryReport", + "AmountCategoryReport", + "AmountChannelType", + "AmountClearing", + "AmountClearing1", + "AmountInstructionId", + "AmountMarginReport", + "AmountPriority2", + "AmountReconciliationType", + "AmountSettlementType", + "AmountStatementDate", + "AmountStatementIndicator", + "AmountTransmission", + "AmountTransmission2", + "AmountTransmissionDate", + "AmountValueDate", + "AmountValueDate1", + "AuthenticationAccountId", + "AuthenticationAdjustmentReport", + "AuthenticationAdjustmentSub", + "AuthenticationClearing", + "AuthenticationCurrencyDate", + "AuthenticationInstruction", + "AuthenticationMarginFlag", + "AuthenticationPartySub", + "AuthenticationPartyType", + "AuthenticationPosition1", + "AuthenticationPositionId", + "AuthenticationReferenceDate", + "AuthenticationRoutingReport", + "AuthenticationSettlementSub", + "AuthenticationStatus", + "AuthenticationTransaction", + "AuthenticationTransactionIndicator", + "AuthenticationTransactionType", + "AuthenticationValueDate2", + "AuthenticationValueDateReport", + "AuthenticationVersion", + "AuthenticationVersionCode", + "AuthenticationVersionIndicator", + "BeneficiaryAccountDate", + "BeneficiaryAmountReport", + "BeneficiaryAuthenticationIndicator", + "BeneficiaryChannelCount", + "BeneficiaryCurrency", + "BeneficiaryCurrencyFlag", + "BeneficiaryIdentification2", + "BeneficiaryIdentificationType", + "BeneficiaryInstruction", + "BeneficiaryPriorityCount", + "BeneficiaryRoutingCount", + "BeneficiaryRoutingId", + "BeneficiarySequenceReport", + "BeneficiarySequenceType", + "BeneficiaryStatementIndicator", + "BeneficiaryTransaction2", + "BeneficiaryValueDateCode", + "BeneficiaryVersionId", + "CategoryBeneficiaryFlag", + "CategoryCategory", + "CategoryCollateral", + "CategoryCurrency", + "CategoryCurrencyType", + "CategoryPartyDate", + "CategoryPositionCode", + "CategoryPositionIndicator", + "CategoryPriorityFlag", + "CategoryStatement", + "CategoryStatementId", + "CategoryStatusReport", + "CategoryTransaction1", + "CategoryTransactionCount", + "CategoryTransactionFlag", + "CategoryValueDateFlag", + "ChannelAccountCount", + "ChannelAdjustmentCount", + "ChannelAdjustmentSub", + "ChannelBeneficiaryDate", + "ChannelClearingFlag", + "ChannelCollateral", + "ChannelInstructionIndicator", + "ChannelPositionCount", + "ChannelPositionIndicator", + "ChannelPriorityCount", + "ChannelReconciliationCount", + "ChannelReconciliationReport", + "ChannelReferenceFlag", + "ChannelRemittance", + "ChannelRemittanceFlag", + "ChannelRemittanceId", + "ChannelRemittanceType", + "ChannelStatus", + "ChannelTransaction2", + "ChannelValueDateSub", + "ChannelVersion1", + "ChannelVersionId", + "ChannelVersionType", + "ClearingAccountReport", + "ClearingAdjustmentId", + "ClearingAmountFlag", + "ClearingBeneficiarySub", + "ClearingClearing2", + "ClearingIdentification1", + "ClearingInstructionFlag", + "ClearingPartyCount", + "ClearingPartyDate", + "ClearingPositionFlag", + "ClearingReference", + "ClearingSequence2", + "ClearingStatementId", + "ClearingTransactionCount", + "ClearingValueDate1", + "ClearingValueDateCount", + "CollateralCollateral1", + "CollateralInstructionCount", + "CollateralPrioritySub", + "CollateralReconciliationCode", + "CollateralReference1", + "CollateralReference2", + "CollateralSequenceType", + "CollateralStatusSub", + "CollateralValueDateCode", + "CollateralValueDateSub", + "CollateralVersionDate", + "CurrencyAccount", + "CurrencyAdjustmentIndicator", + "CurrencyBeneficiaryReport", + "CurrencyCategory2", + "CurrencyChannelType", + "CurrencyCollateralCode", + "CurrencyCollateralId", + "CurrencyIdentificationFlag", + "CurrencyIdentificationIndicator", + "CurrencyInstructionId", + "CurrencyMargin", + "CurrencyPriorityReport", + "CurrencyReconciliationId", + "CurrencyReference", + "CurrencySequenceReport", + "CurrencyStatementCode", + "CurrencyStatusCode", + "CurrencyStatusDate", + "CurrencyTransactionFlag", + "CurrencyTransactionReport", + "CurrencyValueDateCount", + "CurrencyValueDateSub", + "CurrencyVersionSub", + "IdentificationAdjustmentFlag", + "IdentificationChannelSub", + "IdentificationClearingDate", + "IdentificationCollateralDate", + "IdentificationCurrencyId", + "IdentificationMarginCount", + "IdentificationMarginReport", + "IdentificationMarginSub", + "IdentificationParty", + "IdentificationPartyType", + "IdentificationPriority1", + "IdentificationReconciliation", + "IdentificationReconciliationCode", + "IdentificationSettlementCount", + "IdentificationStatementFlag", + "IdentificationTransaction1", + "IdentificationTransactionDate", + "IdentificationTransmissionReport", + "InstructionAccountId", + "InstructionAdjustmentReport", + "InstructionAuthentication", + "InstructionBeneficiary", + "InstructionBeneficiary1", + "InstructionCategory1", + "InstructionChannel2", + "InstructionChannelSub", + "InstructionCurrency", + "InstructionCurrency2", + "InstructionInstructionCode", + "InstructionPriority", + "InstructionPriorityCount", + "InstructionReconciliation1", + "InstructionReconciliationIndicator", + "InstructionReference", + "InstructionReferenceDate", + "InstructionReferenceIndicator", + "InstructionRemittanceReport", + "InstructionRouting2", + "InstructionRoutingFlag", + "InstructionRoutingType", + "InstructionSequence2", + "InstructionSequenceId", + "InstructionStatusCount", + "InstructionTransmissionType", + "InstructionValueDateSub", + "InstructionVersion", + "MarginAccountReport", + "MarginAccountSub", + "MarginAmount", + "MarginAuthentication", + "MarginBeneficiaryType", + "MarginCategoryFlag", + "MarginCollateralReport", + "MarginMarginDate", + "MarginMarginIndicator", + "MarginPartyType", + "MarginPosition", + "MarginPositionCount", + "MarginPriorityId", + "MarginReference1", + "MarginRemittanceReport", + "MarginRouting", + "MarginSettlementCode", + "MarginTransactionCode", + "MarginTransmission", + "MarginValueDateId", + "PartyAccount1", + "PartyBeneficiary1", + "PartyBeneficiaryId", + "PartyCategory", + "PartyClearing", + "PartyClearingDate", + "PartyCurrencyDate", + "PartyPartyId", + "PartyPriorityCount", + "PartyReconciliationCount", + "PartyReconciliationType", + "PartyRouting", + "PartyRoutingId", + "PartyRoutingReport", + "PartySettlement1", + "PartyStatement", + "PartyStatusDate", + "PartyStatusIndicator", + "PartyTransaction2", + "PartyTransactionFlag", + "PartyTransactionReport", + "PartyTransmissionReport", + "PartyVersion1", + "PositionAdjustmentFlag", + "PositionAdjustmentSub", + "PositionAmountSub", + "PositionAuthenticationDate", + "PositionCollateralFlag", + "PositionIdentification", + "PositionIdentificationType", + "PositionInstruction", + "PositionInstructionIndicator", + "PositionMargin", + "PositionPartyFlag", + "PositionPositionFlag", + "PositionReconciliationCode", + "PositionReconciliationId", + "PositionReconciliationIndicator", + "PositionReconciliationReport", + "PositionReferenceCount", + "PositionReferenceDate", + "PositionRemittanceIndicator", + "PositionSequence2", + "PositionStatement", + "PositionStatusCode", + "PositionTransmissionId", + "PositionVersion2", + "PriorityAdjustmentSub", + "PriorityAmount2", + "PriorityAmountIndicator", + "PriorityBeneficiaryIndicator", + "PriorityCategoryId", + "PriorityChannel2", + "PriorityChannelFlag", + "PriorityChannelReport", + "PriorityClearing2", + "PriorityCollateralCode", + "PriorityCurrency", + "PriorityIdentificationIndicator", + "PriorityInstructionCode", + "PriorityMarginId", + "PriorityMarginSub", + "PriorityPartyId", + "PriorityPositionFlag", + "PriorityPriorityCount", + "PriorityReconciliationId", + "PriorityRemittanceCount", + "PriorityRemittanceFlag", + "PriorityRemittanceType", + "PriorityRouting1", + "PriorityRoutingCount", + "PriorityRoutingType", + "PrioritySequenceDate", + "PrioritySettlement", + "PrioritySettlementCode", + "PriorityStatus2", + "PriorityStatusCode", + "PriorityStatusDate", + "PriorityTransactionCount", + "PriorityTransmission2", + "PriorityTransmissionFlag", + "PriorityValueDate1", + "ReconciliationAdjustment1", + "ReconciliationAdjustmentSub", + "ReconciliationBeneficiary2", + "ReconciliationChannelFlag", + "ReconciliationCollateralType", + "ReconciliationCurrencyReport", + "ReconciliationIdentification1", + "ReconciliationInstructionType", + "ReconciliationParty", + "ReconciliationPartyDate", + "ReconciliationPartyType", + "ReconciliationPositionDate", + "ReconciliationPositionId", + "ReconciliationPriority1", + "ReconciliationReconciliationCode", + "ReconciliationReconciliationDate", + "ReconciliationRouting", + "ReconciliationRoutingCode", + "ReconciliationRoutingIndicator", + "ReconciliationSettlementSub", + "ReconciliationStatementReport", + "ReferenceAmountType", + "ReferenceBeneficiaryDate", + "ReferenceChannel", + "ReferenceClearing2", + "ReferenceClearingIndicator", + "ReferenceCurrency", + "ReferenceInstructionFlag", + "ReferencePartyCount", + "ReferencePartyType", + "ReferencePosition", + "ReferenceReconciliationType", + "ReferenceReferenceSub", + "ReferenceRemittance", + "ReferenceRemittance2", + "ReferenceRemittanceCount", + "ReferenceSequenceSub", + "ReferenceStatement", + "ReferenceStatus", + "ReferenceTransactionDate", + "ReferenceTransmissionSub", + "ReferenceVersion", + "RemittanceAdjustmentType", + "RemittanceCategoryFlag", + "RemittanceCategoryType", + "RemittanceCollateral2", + "RemittanceCollateralFlag", + "RemittanceIdentification1", + "RemittanceInstructionType", + "RemittanceMargin", + "RemittancePositionCount", + "RemittancePriority", + "RemittancePriorityCount", + "RemittanceRemittance", + "RemittanceSequenceDate", + "RemittanceStatusCount", + "RemittanceStatusIndicator", + "RemittanceTransmission2", + "RemittanceTransmissionCode", + "RemittanceTransmissionIndicator", + "RemittanceVersionDate", + "RoutingAccountDate", + "RoutingAmount1", + "RoutingAmountCode", + "RoutingAuthenticationIndicator", + "RoutingCategoryCode", + "RoutingCategoryType", + "RoutingClearingDate", + "RoutingCurrencySub", + "RoutingIdentificationCode", + "RoutingPartyType", + "RoutingPriority2", + "RoutingPriorityFlag", + "RoutingPriorityId", + "RoutingReconciliationSub", + "RoutingReferenceDate", + "RoutingRemittance", + "RoutingRemittance2", + "RoutingRouting", + "RoutingRoutingDate", + "RoutingRoutingId", + "RoutingRoutingIndicator", + "RoutingSequenceType", + "RoutingSettlement1", + "RoutingTransactionReport", + "RoutingTransmissionIndicator", + "RoutingValueDateCode", + "RoutingValueDateCount", + "RoutingVersionIndicator", + "RoutingVersionType", + "SequenceAccount2", + "SequenceAccountType", + "SequenceAdjustmentDate", + "SequenceAdjustmentIndicator", + "SequenceAmount1", + "SequenceAmountType", + "SequenceBeneficiary1", + "SequenceBeneficiaryCount", + "SequenceChannel", + "SequenceParty", + "SequencePartyId", + "SequenceReconciliationId", + "SequenceReconciliationReport", + "SequenceReferenceId", + "SequenceStatus", + "SequenceTransaction1", + "SequenceTransactionDate", + "SequenceTransactionFlag", + "SequenceTransmission", + "SequenceTransmissionDate", + "SettlementAccount", + "SettlementBeneficiaryDate", + "SettlementBeneficiaryType", + "SettlementCategoryCount", + "SettlementChannel", + "SettlementInstructionCode", + "SettlementPositionIndicator", + "SettlementReferenceReport", + "SettlementRemittanceIndicator", + "SettlementRoutingReport", + "SettlementSequenceReport", + "SettlementStatementType", + "SettlementStatusCount", + "SettlementTransmissionCode", + "SettlementTransmissionFlag", + "SettlementVersion", + "StatementAccount", + "StatementAuthentication", + "StatementAuthenticationFlag", + "StatementBeneficiary", + "StatementBeneficiarySub", + "StatementChannelReport", + "StatementMargin", + "StatementPositionType", + "StatementPriority2", + "StatementReference", + "StatementReferenceCode", + "StatementReferenceIndicator", + "StatementRemittanceFlag", + "StatementRoutingCode", + "StatementSequenceId", + "StatementSettlement", + "StatementSettlement2", + "StatementValueDateCount", + "StatementValueDateReport", + "StatusAccountType", + "StatusAmount2", + "StatusAuthentication", + "StatusAuthentication2", + "StatusAuthenticationId", + "StatusAuthenticationIndicator", + "StatusCategoryIndicator", + "StatusChannelFlag", + "StatusClearingReport", + "StatusCurrency2", + "StatusInstructionReport", + "StatusPartyIndicator", + "StatusPositionSub", + "StatusPriority1", + "StatusPriorityCount", + "StatusPriorityIndicator", + "StatusReconciliationReport", + "StatusRouting", + "StatusRoutingCode", + "StatusRoutingFlag", + "StatusTransmission", + "StatusTransmissionType", + "StatusValueDateSub", + "StatusVersionDate", + "TransactionAuthentication", + "TransactionAuthentication1", + "TransactionAuthenticationId", + "TransactionClearing", + "TransactionClearingCode", + "TransactionCollateralId", + "TransactionCurrencyIndicator", + "TransactionInstructionCode", + "TransactionPartySub", + "TransactionPosition", + "TransactionPriorityCode", + "TransactionReference2", + "TransactionRouting1", + "TransactionRoutingIndicator", + "TransactionStatementDate", + "TransactionTransactionId", + "TransactionTransmissionType", + "TransactionVersion1", + "TransactionVersionType", + "TransmissionAccountId", + "TransmissionAdjustmentSub", + "TransmissionAmountFlag", + "TransmissionBeneficiaryReport", + "TransmissionCategoryIndicator", + "TransmissionCollateral1", + "TransmissionIdentificationFlag", + "TransmissionInstructionId", + "TransmissionMargin2", + "TransmissionMarginDate", + "TransmissionPositionType", + "TransmissionPriorityCode", + "TransmissionPriorityIndicator", + "TransmissionReconciliation", + "TransmissionReconciliationCount", + "TransmissionReconciliationDate", + "TransmissionRemittanceSub", + "TransmissionRoutingCount", + "TransmissionRoutingId", + "TransmissionSequenceDate", + "TransmissionStatementId", + "TransmissionStatusCode", + "TransmissionTransmission", + "TransmissionTransmissionFlag", + "TransmissionVersion", + "ValueDateAccountFlag", + "ValueDateAdjustmentReport", + "ValueDateChannel1", + "ValueDateChannelFlag", + "ValueDateChannelId", + "ValueDateClearingDate", + "ValueDateClearingReport", + "ValueDateCurrencyId", + "ValueDateCurrencyType", + "ValueDateIdentification2", + "ValueDateIdentificationIndicator", + "ValueDateInstruction", + "ValueDateInstruction1", + "ValueDateMarginType", + "ValueDatePosition2", + "ValueDatePositionCount", + "ValueDateReconciliationCount", + "ValueDateReconciliationId", + "ValueDateReferenceFlag", + "ValueDateRemittanceCount", + "ValueDateRemittanceIndicator", + "ValueDateRoutingFlag", + "ValueDateSequenceId", + "ValueDateSettlementCount", + "ValueDateStatementDate", + "ValueDateTransmission", + "ValueDateTransmissionCode", + "ValueDateValueDateReport", + "ValueDateVersionReport", + "VersionAccount", + "VersionAdjustment", + "VersionAdjustment1", + "VersionAuthenticationCode", + "VersionBeneficiaryCount", + "VersionBeneficiaryFlag", + "VersionCategoryFlag", + "VersionClearingFlag", + "VersionMargin", + "VersionMarginFlag", + "VersionMarginReport", + "VersionReconciliation1", + "VersionReferenceDate", + "VersionReferenceIndicator", + "VersionRemittance", + "VersionStatusCode", + "VersionStatusType", + "VersionTransactionId", + "VersionTransmissionCount", +]; + +pub fn match_dispatch_600(tag: &str) -> Option { + match tag { + "AccountAccount" => Some(0), + "AccountAccountId" => Some(1), + "AccountAmount" => Some(2), + "AccountAuthenticationReport" => Some(3), + "AccountCategoryCode" => Some(4), + "AccountClearingCount" => Some(5), + "AccountInstruction2" => Some(6), + "AccountInstructionDate" => Some(7), + "AccountMarginId" => Some(8), + "AccountMarginIndicator" => Some(9), + "AccountPartyCount" => Some(10), + "AccountRoutingDate" => Some(11), + "AccountSettlement1" => Some(12), + "AccountStatement" => Some(13), + "AccountTransactionFlag" => Some(14), + "AccountTransactionReport" => Some(15), + "AccountTransmission" => Some(16), + "AccountTransmissionCount" => Some(17), + "AdjustmentAccountId" => Some(18), + "AdjustmentAuthenticationIndicator" => Some(19), + "AdjustmentAuthenticationSub" => Some(20), + "AdjustmentCategoryId" => Some(21), + "AdjustmentCategorySub" => Some(22), + "AdjustmentIdentificationType" => Some(23), + "AdjustmentMarginFlag" => Some(24), + "AdjustmentPosition" => Some(25), + "AdjustmentPriorityFlag" => Some(26), + "AdjustmentReferenceFlag" => Some(27), + "AdjustmentRemittanceReport" => Some(28), + "AdjustmentRoutingCount" => Some(29), + "AdjustmentSequence1" => Some(30), + "AdjustmentSettlementDate" => Some(31), + "AdjustmentSettlementType" => Some(32), + "AdjustmentStatementDate" => Some(33), + "AdjustmentStatusFlag" => Some(34), + "AdjustmentTransactionType" => Some(35), + "AdjustmentTransmissionReport" => Some(36), + "AdjustmentValueDateDate" => Some(37), + "AdjustmentValueDateSub" => Some(38), + "AdjustmentVersionCount" => Some(39), + "AmountAmount" => Some(40), + "AmountAuthentication" => Some(41), + "AmountAuthenticationId" => Some(42), + "AmountBeneficiaryCount" => Some(43), + "AmountBeneficiaryReport" => Some(44), + "AmountCategoryReport" => Some(45), + "AmountChannelType" => Some(46), + "AmountClearing" => Some(47), + "AmountClearing1" => Some(48), + "AmountInstructionId" => Some(49), + "AmountMarginReport" => Some(50), + "AmountPriority2" => Some(51), + "AmountReconciliationType" => Some(52), + "AmountSettlementType" => Some(53), + "AmountStatementDate" => Some(54), + "AmountStatementIndicator" => Some(55), + "AmountTransmission" => Some(56), + "AmountTransmission2" => Some(57), + "AmountTransmissionDate" => Some(58), + "AmountValueDate" => Some(59), + "AmountValueDate1" => Some(60), + "AuthenticationAccountId" => Some(61), + "AuthenticationAdjustmentReport" => Some(62), + "AuthenticationAdjustmentSub" => Some(63), + "AuthenticationClearing" => Some(64), + "AuthenticationCurrencyDate" => Some(65), + "AuthenticationInstruction" => Some(66), + "AuthenticationMarginFlag" => Some(67), + "AuthenticationPartySub" => Some(68), + "AuthenticationPartyType" => Some(69), + "AuthenticationPosition1" => Some(70), + "AuthenticationPositionId" => Some(71), + "AuthenticationReferenceDate" => Some(72), + "AuthenticationRoutingReport" => Some(73), + "AuthenticationSettlementSub" => Some(74), + "AuthenticationStatus" => Some(75), + "AuthenticationTransaction" => Some(76), + "AuthenticationTransactionIndicator" => Some(77), + "AuthenticationTransactionType" => Some(78), + "AuthenticationValueDate2" => Some(79), + "AuthenticationValueDateReport" => Some(80), + "AuthenticationVersion" => Some(81), + "AuthenticationVersionCode" => Some(82), + "AuthenticationVersionIndicator" => Some(83), + "BeneficiaryAccountDate" => Some(84), + "BeneficiaryAmountReport" => Some(85), + "BeneficiaryAuthenticationIndicator" => Some(86), + "BeneficiaryChannelCount" => Some(87), + "BeneficiaryCurrency" => Some(88), + "BeneficiaryCurrencyFlag" => Some(89), + "BeneficiaryIdentification2" => Some(90), + "BeneficiaryIdentificationType" => Some(91), + "BeneficiaryInstruction" => Some(92), + "BeneficiaryPriorityCount" => Some(93), + "BeneficiaryRoutingCount" => Some(94), + "BeneficiaryRoutingId" => Some(95), + "BeneficiarySequenceReport" => Some(96), + "BeneficiarySequenceType" => Some(97), + "BeneficiaryStatementIndicator" => Some(98), + "BeneficiaryTransaction2" => Some(99), + "BeneficiaryValueDateCode" => Some(100), + "BeneficiaryVersionId" => Some(101), + "CategoryBeneficiaryFlag" => Some(102), + "CategoryCategory" => Some(103), + "CategoryCollateral" => Some(104), + "CategoryCurrency" => Some(105), + "CategoryCurrencyType" => Some(106), + "CategoryPartyDate" => Some(107), + "CategoryPositionCode" => Some(108), + "CategoryPositionIndicator" => Some(109), + "CategoryPriorityFlag" => Some(110), + "CategoryStatement" => Some(111), + "CategoryStatementId" => Some(112), + "CategoryStatusReport" => Some(113), + "CategoryTransaction1" => Some(114), + "CategoryTransactionCount" => Some(115), + "CategoryTransactionFlag" => Some(116), + "CategoryValueDateFlag" => Some(117), + "ChannelAccountCount" => Some(118), + "ChannelAdjustmentCount" => Some(119), + "ChannelAdjustmentSub" => Some(120), + "ChannelBeneficiaryDate" => Some(121), + "ChannelClearingFlag" => Some(122), + "ChannelCollateral" => Some(123), + "ChannelInstructionIndicator" => Some(124), + "ChannelPositionCount" => Some(125), + "ChannelPositionIndicator" => Some(126), + "ChannelPriorityCount" => Some(127), + "ChannelReconciliationCount" => Some(128), + "ChannelReconciliationReport" => Some(129), + "ChannelReferenceFlag" => Some(130), + "ChannelRemittance" => Some(131), + "ChannelRemittanceFlag" => Some(132), + "ChannelRemittanceId" => Some(133), + "ChannelRemittanceType" => Some(134), + "ChannelStatus" => Some(135), + "ChannelTransaction2" => Some(136), + "ChannelValueDateSub" => Some(137), + "ChannelVersion1" => Some(138), + "ChannelVersionId" => Some(139), + "ChannelVersionType" => Some(140), + "ClearingAccountReport" => Some(141), + "ClearingAdjustmentId" => Some(142), + "ClearingAmountFlag" => Some(143), + "ClearingBeneficiarySub" => Some(144), + "ClearingClearing2" => Some(145), + "ClearingIdentification1" => Some(146), + "ClearingInstructionFlag" => Some(147), + "ClearingPartyCount" => Some(148), + "ClearingPartyDate" => Some(149), + "ClearingPositionFlag" => Some(150), + "ClearingReference" => Some(151), + "ClearingSequence2" => Some(152), + "ClearingStatementId" => Some(153), + "ClearingTransactionCount" => Some(154), + "ClearingValueDate1" => Some(155), + "ClearingValueDateCount" => Some(156), + "CollateralCollateral1" => Some(157), + "CollateralInstructionCount" => Some(158), + "CollateralPrioritySub" => Some(159), + "CollateralReconciliationCode" => Some(160), + "CollateralReference1" => Some(161), + "CollateralReference2" => Some(162), + "CollateralSequenceType" => Some(163), + "CollateralStatusSub" => Some(164), + "CollateralValueDateCode" => Some(165), + "CollateralValueDateSub" => Some(166), + "CollateralVersionDate" => Some(167), + "CurrencyAccount" => Some(168), + "CurrencyAdjustmentIndicator" => Some(169), + "CurrencyBeneficiaryReport" => Some(170), + "CurrencyCategory2" => Some(171), + "CurrencyChannelType" => Some(172), + "CurrencyCollateralCode" => Some(173), + "CurrencyCollateralId" => Some(174), + "CurrencyIdentificationFlag" => Some(175), + "CurrencyIdentificationIndicator" => Some(176), + "CurrencyInstructionId" => Some(177), + "CurrencyMargin" => Some(178), + "CurrencyPriorityReport" => Some(179), + "CurrencyReconciliationId" => Some(180), + "CurrencyReference" => Some(181), + "CurrencySequenceReport" => Some(182), + "CurrencyStatementCode" => Some(183), + "CurrencyStatusCode" => Some(184), + "CurrencyStatusDate" => Some(185), + "CurrencyTransactionFlag" => Some(186), + "CurrencyTransactionReport" => Some(187), + "CurrencyValueDateCount" => Some(188), + "CurrencyValueDateSub" => Some(189), + "CurrencyVersionSub" => Some(190), + "IdentificationAdjustmentFlag" => Some(191), + "IdentificationChannelSub" => Some(192), + "IdentificationClearingDate" => Some(193), + "IdentificationCollateralDate" => Some(194), + "IdentificationCurrencyId" => Some(195), + "IdentificationMarginCount" => Some(196), + "IdentificationMarginReport" => Some(197), + "IdentificationMarginSub" => Some(198), + "IdentificationParty" => Some(199), + "IdentificationPartyType" => Some(200), + "IdentificationPriority1" => Some(201), + "IdentificationReconciliation" => Some(202), + "IdentificationReconciliationCode" => Some(203), + "IdentificationSettlementCount" => Some(204), + "IdentificationStatementFlag" => Some(205), + "IdentificationTransaction1" => Some(206), + "IdentificationTransactionDate" => Some(207), + "IdentificationTransmissionReport" => Some(208), + "InstructionAccountId" => Some(209), + "InstructionAdjustmentReport" => Some(210), + "InstructionAuthentication" => Some(211), + "InstructionBeneficiary" => Some(212), + "InstructionBeneficiary1" => Some(213), + "InstructionCategory1" => Some(214), + "InstructionChannel2" => Some(215), + "InstructionChannelSub" => Some(216), + "InstructionCurrency" => Some(217), + "InstructionCurrency2" => Some(218), + "InstructionInstructionCode" => Some(219), + "InstructionPriority" => Some(220), + "InstructionPriorityCount" => Some(221), + "InstructionReconciliation1" => Some(222), + "InstructionReconciliationIndicator" => Some(223), + "InstructionReference" => Some(224), + "InstructionReferenceDate" => Some(225), + "InstructionReferenceIndicator" => Some(226), + "InstructionRemittanceReport" => Some(227), + "InstructionRouting2" => Some(228), + "InstructionRoutingFlag" => Some(229), + "InstructionRoutingType" => Some(230), + "InstructionSequence2" => Some(231), + "InstructionSequenceId" => Some(232), + "InstructionStatusCount" => Some(233), + "InstructionTransmissionType" => Some(234), + "InstructionValueDateSub" => Some(235), + "InstructionVersion" => Some(236), + "MarginAccountReport" => Some(237), + "MarginAccountSub" => Some(238), + "MarginAmount" => Some(239), + "MarginAuthentication" => Some(240), + "MarginBeneficiaryType" => Some(241), + "MarginCategoryFlag" => Some(242), + "MarginCollateralReport" => Some(243), + "MarginMarginDate" => Some(244), + "MarginMarginIndicator" => Some(245), + "MarginPartyType" => Some(246), + "MarginPosition" => Some(247), + "MarginPositionCount" => Some(248), + "MarginPriorityId" => Some(249), + "MarginReference1" => Some(250), + "MarginRemittanceReport" => Some(251), + "MarginRouting" => Some(252), + "MarginSettlementCode" => Some(253), + "MarginTransactionCode" => Some(254), + "MarginTransmission" => Some(255), + "MarginValueDateId" => Some(256), + "PartyAccount1" => Some(257), + "PartyBeneficiary1" => Some(258), + "PartyBeneficiaryId" => Some(259), + "PartyCategory" => Some(260), + "PartyClearing" => Some(261), + "PartyClearingDate" => Some(262), + "PartyCurrencyDate" => Some(263), + "PartyPartyId" => Some(264), + "PartyPriorityCount" => Some(265), + "PartyReconciliationCount" => Some(266), + "PartyReconciliationType" => Some(267), + "PartyRouting" => Some(268), + "PartyRoutingId" => Some(269), + "PartyRoutingReport" => Some(270), + "PartySettlement1" => Some(271), + "PartyStatement" => Some(272), + "PartyStatusDate" => Some(273), + "PartyStatusIndicator" => Some(274), + "PartyTransaction2" => Some(275), + "PartyTransactionFlag" => Some(276), + "PartyTransactionReport" => Some(277), + "PartyTransmissionReport" => Some(278), + "PartyVersion1" => Some(279), + "PositionAdjustmentFlag" => Some(280), + "PositionAdjustmentSub" => Some(281), + "PositionAmountSub" => Some(282), + "PositionAuthenticationDate" => Some(283), + "PositionCollateralFlag" => Some(284), + "PositionIdentification" => Some(285), + "PositionIdentificationType" => Some(286), + "PositionInstruction" => Some(287), + "PositionInstructionIndicator" => Some(288), + "PositionMargin" => Some(289), + "PositionPartyFlag" => Some(290), + "PositionPositionFlag" => Some(291), + "PositionReconciliationCode" => Some(292), + "PositionReconciliationId" => Some(293), + "PositionReconciliationIndicator" => Some(294), + "PositionReconciliationReport" => Some(295), + "PositionReferenceCount" => Some(296), + "PositionReferenceDate" => Some(297), + "PositionRemittanceIndicator" => Some(298), + "PositionSequence2" => Some(299), + "PositionStatement" => Some(300), + "PositionStatusCode" => Some(301), + "PositionTransmissionId" => Some(302), + "PositionVersion2" => Some(303), + "PriorityAdjustmentSub" => Some(304), + "PriorityAmount2" => Some(305), + "PriorityAmountIndicator" => Some(306), + "PriorityBeneficiaryIndicator" => Some(307), + "PriorityCategoryId" => Some(308), + "PriorityChannel2" => Some(309), + "PriorityChannelFlag" => Some(310), + "PriorityChannelReport" => Some(311), + "PriorityClearing2" => Some(312), + "PriorityCollateralCode" => Some(313), + "PriorityCurrency" => Some(314), + "PriorityIdentificationIndicator" => Some(315), + "PriorityInstructionCode" => Some(316), + "PriorityMarginId" => Some(317), + "PriorityMarginSub" => Some(318), + "PriorityPartyId" => Some(319), + "PriorityPositionFlag" => Some(320), + "PriorityPriorityCount" => Some(321), + "PriorityReconciliationId" => Some(322), + "PriorityRemittanceCount" => Some(323), + "PriorityRemittanceFlag" => Some(324), + "PriorityRemittanceType" => Some(325), + "PriorityRouting1" => Some(326), + "PriorityRoutingCount" => Some(327), + "PriorityRoutingType" => Some(328), + "PrioritySequenceDate" => Some(329), + "PrioritySettlement" => Some(330), + "PrioritySettlementCode" => Some(331), + "PriorityStatus2" => Some(332), + "PriorityStatusCode" => Some(333), + "PriorityStatusDate" => Some(334), + "PriorityTransactionCount" => Some(335), + "PriorityTransmission2" => Some(336), + "PriorityTransmissionFlag" => Some(337), + "PriorityValueDate1" => Some(338), + "ReconciliationAdjustment1" => Some(339), + "ReconciliationAdjustmentSub" => Some(340), + "ReconciliationBeneficiary2" => Some(341), + "ReconciliationChannelFlag" => Some(342), + "ReconciliationCollateralType" => Some(343), + "ReconciliationCurrencyReport" => Some(344), + "ReconciliationIdentification1" => Some(345), + "ReconciliationInstructionType" => Some(346), + "ReconciliationParty" => Some(347), + "ReconciliationPartyDate" => Some(348), + "ReconciliationPartyType" => Some(349), + "ReconciliationPositionDate" => Some(350), + "ReconciliationPositionId" => Some(351), + "ReconciliationPriority1" => Some(352), + "ReconciliationReconciliationCode" => Some(353), + "ReconciliationReconciliationDate" => Some(354), + "ReconciliationRouting" => Some(355), + "ReconciliationRoutingCode" => Some(356), + "ReconciliationRoutingIndicator" => Some(357), + "ReconciliationSettlementSub" => Some(358), + "ReconciliationStatementReport" => Some(359), + "ReferenceAmountType" => Some(360), + "ReferenceBeneficiaryDate" => Some(361), + "ReferenceChannel" => Some(362), + "ReferenceClearing2" => Some(363), + "ReferenceClearingIndicator" => Some(364), + "ReferenceCurrency" => Some(365), + "ReferenceInstructionFlag" => Some(366), + "ReferencePartyCount" => Some(367), + "ReferencePartyType" => Some(368), + "ReferencePosition" => Some(369), + "ReferenceReconciliationType" => Some(370), + "ReferenceReferenceSub" => Some(371), + "ReferenceRemittance" => Some(372), + "ReferenceRemittance2" => Some(373), + "ReferenceRemittanceCount" => Some(374), + "ReferenceSequenceSub" => Some(375), + "ReferenceStatement" => Some(376), + "ReferenceStatus" => Some(377), + "ReferenceTransactionDate" => Some(378), + "ReferenceTransmissionSub" => Some(379), + "ReferenceVersion" => Some(380), + "RemittanceAdjustmentType" => Some(381), + "RemittanceCategoryFlag" => Some(382), + "RemittanceCategoryType" => Some(383), + "RemittanceCollateral2" => Some(384), + "RemittanceCollateralFlag" => Some(385), + "RemittanceIdentification1" => Some(386), + "RemittanceInstructionType" => Some(387), + "RemittanceMargin" => Some(388), + "RemittancePositionCount" => Some(389), + "RemittancePriority" => Some(390), + "RemittancePriorityCount" => Some(391), + "RemittanceRemittance" => Some(392), + "RemittanceSequenceDate" => Some(393), + "RemittanceStatusCount" => Some(394), + "RemittanceStatusIndicator" => Some(395), + "RemittanceTransmission2" => Some(396), + "RemittanceTransmissionCode" => Some(397), + "RemittanceTransmissionIndicator" => Some(398), + "RemittanceVersionDate" => Some(399), + "RoutingAccountDate" => Some(400), + "RoutingAmount1" => Some(401), + "RoutingAmountCode" => Some(402), + "RoutingAuthenticationIndicator" => Some(403), + "RoutingCategoryCode" => Some(404), + "RoutingCategoryType" => Some(405), + "RoutingClearingDate" => Some(406), + "RoutingCurrencySub" => Some(407), + "RoutingIdentificationCode" => Some(408), + "RoutingPartyType" => Some(409), + "RoutingPriority2" => Some(410), + "RoutingPriorityFlag" => Some(411), + "RoutingPriorityId" => Some(412), + "RoutingReconciliationSub" => Some(413), + "RoutingReferenceDate" => Some(414), + "RoutingRemittance" => Some(415), + "RoutingRemittance2" => Some(416), + "RoutingRouting" => Some(417), + "RoutingRoutingDate" => Some(418), + "RoutingRoutingId" => Some(419), + "RoutingRoutingIndicator" => Some(420), + "RoutingSequenceType" => Some(421), + "RoutingSettlement1" => Some(422), + "RoutingTransactionReport" => Some(423), + "RoutingTransmissionIndicator" => Some(424), + "RoutingValueDateCode" => Some(425), + "RoutingValueDateCount" => Some(426), + "RoutingVersionIndicator" => Some(427), + "RoutingVersionType" => Some(428), + "SequenceAccount2" => Some(429), + "SequenceAccountType" => Some(430), + "SequenceAdjustmentDate" => Some(431), + "SequenceAdjustmentIndicator" => Some(432), + "SequenceAmount1" => Some(433), + "SequenceAmountType" => Some(434), + "SequenceBeneficiary1" => Some(435), + "SequenceBeneficiaryCount" => Some(436), + "SequenceChannel" => Some(437), + "SequenceParty" => Some(438), + "SequencePartyId" => Some(439), + "SequenceReconciliationId" => Some(440), + "SequenceReconciliationReport" => Some(441), + "SequenceReferenceId" => Some(442), + "SequenceStatus" => Some(443), + "SequenceTransaction1" => Some(444), + "SequenceTransactionDate" => Some(445), + "SequenceTransactionFlag" => Some(446), + "SequenceTransmission" => Some(447), + "SequenceTransmissionDate" => Some(448), + "SettlementAccount" => Some(449), + "SettlementBeneficiaryDate" => Some(450), + "SettlementBeneficiaryType" => Some(451), + "SettlementCategoryCount" => Some(452), + "SettlementChannel" => Some(453), + "SettlementInstructionCode" => Some(454), + "SettlementPositionIndicator" => Some(455), + "SettlementReferenceReport" => Some(456), + "SettlementRemittanceIndicator" => Some(457), + "SettlementRoutingReport" => Some(458), + "SettlementSequenceReport" => Some(459), + "SettlementStatementType" => Some(460), + "SettlementStatusCount" => Some(461), + "SettlementTransmissionCode" => Some(462), + "SettlementTransmissionFlag" => Some(463), + "SettlementVersion" => Some(464), + "StatementAccount" => Some(465), + "StatementAuthentication" => Some(466), + "StatementAuthenticationFlag" => Some(467), + "StatementBeneficiary" => Some(468), + "StatementBeneficiarySub" => Some(469), + "StatementChannelReport" => Some(470), + "StatementMargin" => Some(471), + "StatementPositionType" => Some(472), + "StatementPriority2" => Some(473), + "StatementReference" => Some(474), + "StatementReferenceCode" => Some(475), + "StatementReferenceIndicator" => Some(476), + "StatementRemittanceFlag" => Some(477), + "StatementRoutingCode" => Some(478), + "StatementSequenceId" => Some(479), + "StatementSettlement" => Some(480), + "StatementSettlement2" => Some(481), + "StatementValueDateCount" => Some(482), + "StatementValueDateReport" => Some(483), + "StatusAccountType" => Some(484), + "StatusAmount2" => Some(485), + "StatusAuthentication" => Some(486), + "StatusAuthentication2" => Some(487), + "StatusAuthenticationId" => Some(488), + "StatusAuthenticationIndicator" => Some(489), + "StatusCategoryIndicator" => Some(490), + "StatusChannelFlag" => Some(491), + "StatusClearingReport" => Some(492), + "StatusCurrency2" => Some(493), + "StatusInstructionReport" => Some(494), + "StatusPartyIndicator" => Some(495), + "StatusPositionSub" => Some(496), + "StatusPriority1" => Some(497), + "StatusPriorityCount" => Some(498), + "StatusPriorityIndicator" => Some(499), + "StatusReconciliationReport" => Some(500), + "StatusRouting" => Some(501), + "StatusRoutingCode" => Some(502), + "StatusRoutingFlag" => Some(503), + "StatusTransmission" => Some(504), + "StatusTransmissionType" => Some(505), + "StatusValueDateSub" => Some(506), + "StatusVersionDate" => Some(507), + "TransactionAuthentication" => Some(508), + "TransactionAuthentication1" => Some(509), + "TransactionAuthenticationId" => Some(510), + "TransactionClearing" => Some(511), + "TransactionClearingCode" => Some(512), + "TransactionCollateralId" => Some(513), + "TransactionCurrencyIndicator" => Some(514), + "TransactionInstructionCode" => Some(515), + "TransactionPartySub" => Some(516), + "TransactionPosition" => Some(517), + "TransactionPriorityCode" => Some(518), + "TransactionReference2" => Some(519), + "TransactionRouting1" => Some(520), + "TransactionRoutingIndicator" => Some(521), + "TransactionStatementDate" => Some(522), + "TransactionTransactionId" => Some(523), + "TransactionTransmissionType" => Some(524), + "TransactionVersion1" => Some(525), + "TransactionVersionType" => Some(526), + "TransmissionAccountId" => Some(527), + "TransmissionAdjustmentSub" => Some(528), + "TransmissionAmountFlag" => Some(529), + "TransmissionBeneficiaryReport" => Some(530), + "TransmissionCategoryIndicator" => Some(531), + "TransmissionCollateral1" => Some(532), + "TransmissionIdentificationFlag" => Some(533), + "TransmissionInstructionId" => Some(534), + "TransmissionMargin2" => Some(535), + "TransmissionMarginDate" => Some(536), + "TransmissionPositionType" => Some(537), + "TransmissionPriorityCode" => Some(538), + "TransmissionPriorityIndicator" => Some(539), + "TransmissionReconciliation" => Some(540), + "TransmissionReconciliationCount" => Some(541), + "TransmissionReconciliationDate" => Some(542), + "TransmissionRemittanceSub" => Some(543), + "TransmissionRoutingCount" => Some(544), + "TransmissionRoutingId" => Some(545), + "TransmissionSequenceDate" => Some(546), + "TransmissionStatementId" => Some(547), + "TransmissionStatusCode" => Some(548), + "TransmissionTransmission" => Some(549), + "TransmissionTransmissionFlag" => Some(550), + "TransmissionVersion" => Some(551), + "ValueDateAccountFlag" => Some(552), + "ValueDateAdjustmentReport" => Some(553), + "ValueDateChannel1" => Some(554), + "ValueDateChannelFlag" => Some(555), + "ValueDateChannelId" => Some(556), + "ValueDateClearingDate" => Some(557), + "ValueDateClearingReport" => Some(558), + "ValueDateCurrencyId" => Some(559), + "ValueDateCurrencyType" => Some(560), + "ValueDateIdentification2" => Some(561), + "ValueDateIdentificationIndicator" => Some(562), + "ValueDateInstruction" => Some(563), + "ValueDateInstruction1" => Some(564), + "ValueDateMarginType" => Some(565), + "ValueDatePosition2" => Some(566), + "ValueDatePositionCount" => Some(567), + "ValueDateReconciliationCount" => Some(568), + "ValueDateReconciliationId" => Some(569), + "ValueDateReferenceFlag" => Some(570), + "ValueDateRemittanceCount" => Some(571), + "ValueDateRemittanceIndicator" => Some(572), + "ValueDateRoutingFlag" => Some(573), + "ValueDateSequenceId" => Some(574), + "ValueDateSettlementCount" => Some(575), + "ValueDateStatementDate" => Some(576), + "ValueDateTransmission" => Some(577), + "ValueDateTransmissionCode" => Some(578), + "ValueDateValueDateReport" => Some(579), + "ValueDateVersionReport" => Some(580), + "VersionAccount" => Some(581), + "VersionAdjustment" => Some(582), + "VersionAdjustment1" => Some(583), + "VersionAuthenticationCode" => Some(584), + "VersionBeneficiaryCount" => Some(585), + "VersionBeneficiaryFlag" => Some(586), + "VersionCategoryFlag" => Some(587), + "VersionClearingFlag" => Some(588), + "VersionMargin" => Some(589), + "VersionMarginFlag" => Some(590), + "VersionMarginReport" => Some(591), + "VersionReconciliation1" => Some(592), + "VersionReferenceDate" => Some(593), + "VersionReferenceIndicator" => Some(594), + "VersionRemittance" => Some(595), + "VersionStatusCode" => Some(596), + "VersionStatusType" => Some(597), + "VersionTransactionId" => Some(598), + "VersionTransmissionCount" => Some(599), + _ => None, + } +} + +pub static PHF_600: phf::Map<&'static str, u32> = phf::phf_map! { + "AccountAccount" => 0u32, + "AccountAccountId" => 1u32, + "AccountAmount" => 2u32, + "AccountAuthenticationReport" => 3u32, + "AccountCategoryCode" => 4u32, + "AccountClearingCount" => 5u32, + "AccountInstruction2" => 6u32, + "AccountInstructionDate" => 7u32, + "AccountMarginId" => 8u32, + "AccountMarginIndicator" => 9u32, + "AccountPartyCount" => 10u32, + "AccountRoutingDate" => 11u32, + "AccountSettlement1" => 12u32, + "AccountStatement" => 13u32, + "AccountTransactionFlag" => 14u32, + "AccountTransactionReport" => 15u32, + "AccountTransmission" => 16u32, + "AccountTransmissionCount" => 17u32, + "AdjustmentAccountId" => 18u32, + "AdjustmentAuthenticationIndicator" => 19u32, + "AdjustmentAuthenticationSub" => 20u32, + "AdjustmentCategoryId" => 21u32, + "AdjustmentCategorySub" => 22u32, + "AdjustmentIdentificationType" => 23u32, + "AdjustmentMarginFlag" => 24u32, + "AdjustmentPosition" => 25u32, + "AdjustmentPriorityFlag" => 26u32, + "AdjustmentReferenceFlag" => 27u32, + "AdjustmentRemittanceReport" => 28u32, + "AdjustmentRoutingCount" => 29u32, + "AdjustmentSequence1" => 30u32, + "AdjustmentSettlementDate" => 31u32, + "AdjustmentSettlementType" => 32u32, + "AdjustmentStatementDate" => 33u32, + "AdjustmentStatusFlag" => 34u32, + "AdjustmentTransactionType" => 35u32, + "AdjustmentTransmissionReport" => 36u32, + "AdjustmentValueDateDate" => 37u32, + "AdjustmentValueDateSub" => 38u32, + "AdjustmentVersionCount" => 39u32, + "AmountAmount" => 40u32, + "AmountAuthentication" => 41u32, + "AmountAuthenticationId" => 42u32, + "AmountBeneficiaryCount" => 43u32, + "AmountBeneficiaryReport" => 44u32, + "AmountCategoryReport" => 45u32, + "AmountChannelType" => 46u32, + "AmountClearing" => 47u32, + "AmountClearing1" => 48u32, + "AmountInstructionId" => 49u32, + "AmountMarginReport" => 50u32, + "AmountPriority2" => 51u32, + "AmountReconciliationType" => 52u32, + "AmountSettlementType" => 53u32, + "AmountStatementDate" => 54u32, + "AmountStatementIndicator" => 55u32, + "AmountTransmission" => 56u32, + "AmountTransmission2" => 57u32, + "AmountTransmissionDate" => 58u32, + "AmountValueDate" => 59u32, + "AmountValueDate1" => 60u32, + "AuthenticationAccountId" => 61u32, + "AuthenticationAdjustmentReport" => 62u32, + "AuthenticationAdjustmentSub" => 63u32, + "AuthenticationClearing" => 64u32, + "AuthenticationCurrencyDate" => 65u32, + "AuthenticationInstruction" => 66u32, + "AuthenticationMarginFlag" => 67u32, + "AuthenticationPartySub" => 68u32, + "AuthenticationPartyType" => 69u32, + "AuthenticationPosition1" => 70u32, + "AuthenticationPositionId" => 71u32, + "AuthenticationReferenceDate" => 72u32, + "AuthenticationRoutingReport" => 73u32, + "AuthenticationSettlementSub" => 74u32, + "AuthenticationStatus" => 75u32, + "AuthenticationTransaction" => 76u32, + "AuthenticationTransactionIndicator" => 77u32, + "AuthenticationTransactionType" => 78u32, + "AuthenticationValueDate2" => 79u32, + "AuthenticationValueDateReport" => 80u32, + "AuthenticationVersion" => 81u32, + "AuthenticationVersionCode" => 82u32, + "AuthenticationVersionIndicator" => 83u32, + "BeneficiaryAccountDate" => 84u32, + "BeneficiaryAmountReport" => 85u32, + "BeneficiaryAuthenticationIndicator" => 86u32, + "BeneficiaryChannelCount" => 87u32, + "BeneficiaryCurrency" => 88u32, + "BeneficiaryCurrencyFlag" => 89u32, + "BeneficiaryIdentification2" => 90u32, + "BeneficiaryIdentificationType" => 91u32, + "BeneficiaryInstruction" => 92u32, + "BeneficiaryPriorityCount" => 93u32, + "BeneficiaryRoutingCount" => 94u32, + "BeneficiaryRoutingId" => 95u32, + "BeneficiarySequenceReport" => 96u32, + "BeneficiarySequenceType" => 97u32, + "BeneficiaryStatementIndicator" => 98u32, + "BeneficiaryTransaction2" => 99u32, + "BeneficiaryValueDateCode" => 100u32, + "BeneficiaryVersionId" => 101u32, + "CategoryBeneficiaryFlag" => 102u32, + "CategoryCategory" => 103u32, + "CategoryCollateral" => 104u32, + "CategoryCurrency" => 105u32, + "CategoryCurrencyType" => 106u32, + "CategoryPartyDate" => 107u32, + "CategoryPositionCode" => 108u32, + "CategoryPositionIndicator" => 109u32, + "CategoryPriorityFlag" => 110u32, + "CategoryStatement" => 111u32, + "CategoryStatementId" => 112u32, + "CategoryStatusReport" => 113u32, + "CategoryTransaction1" => 114u32, + "CategoryTransactionCount" => 115u32, + "CategoryTransactionFlag" => 116u32, + "CategoryValueDateFlag" => 117u32, + "ChannelAccountCount" => 118u32, + "ChannelAdjustmentCount" => 119u32, + "ChannelAdjustmentSub" => 120u32, + "ChannelBeneficiaryDate" => 121u32, + "ChannelClearingFlag" => 122u32, + "ChannelCollateral" => 123u32, + "ChannelInstructionIndicator" => 124u32, + "ChannelPositionCount" => 125u32, + "ChannelPositionIndicator" => 126u32, + "ChannelPriorityCount" => 127u32, + "ChannelReconciliationCount" => 128u32, + "ChannelReconciliationReport" => 129u32, + "ChannelReferenceFlag" => 130u32, + "ChannelRemittance" => 131u32, + "ChannelRemittanceFlag" => 132u32, + "ChannelRemittanceId" => 133u32, + "ChannelRemittanceType" => 134u32, + "ChannelStatus" => 135u32, + "ChannelTransaction2" => 136u32, + "ChannelValueDateSub" => 137u32, + "ChannelVersion1" => 138u32, + "ChannelVersionId" => 139u32, + "ChannelVersionType" => 140u32, + "ClearingAccountReport" => 141u32, + "ClearingAdjustmentId" => 142u32, + "ClearingAmountFlag" => 143u32, + "ClearingBeneficiarySub" => 144u32, + "ClearingClearing2" => 145u32, + "ClearingIdentification1" => 146u32, + "ClearingInstructionFlag" => 147u32, + "ClearingPartyCount" => 148u32, + "ClearingPartyDate" => 149u32, + "ClearingPositionFlag" => 150u32, + "ClearingReference" => 151u32, + "ClearingSequence2" => 152u32, + "ClearingStatementId" => 153u32, + "ClearingTransactionCount" => 154u32, + "ClearingValueDate1" => 155u32, + "ClearingValueDateCount" => 156u32, + "CollateralCollateral1" => 157u32, + "CollateralInstructionCount" => 158u32, + "CollateralPrioritySub" => 159u32, + "CollateralReconciliationCode" => 160u32, + "CollateralReference1" => 161u32, + "CollateralReference2" => 162u32, + "CollateralSequenceType" => 163u32, + "CollateralStatusSub" => 164u32, + "CollateralValueDateCode" => 165u32, + "CollateralValueDateSub" => 166u32, + "CollateralVersionDate" => 167u32, + "CurrencyAccount" => 168u32, + "CurrencyAdjustmentIndicator" => 169u32, + "CurrencyBeneficiaryReport" => 170u32, + "CurrencyCategory2" => 171u32, + "CurrencyChannelType" => 172u32, + "CurrencyCollateralCode" => 173u32, + "CurrencyCollateralId" => 174u32, + "CurrencyIdentificationFlag" => 175u32, + "CurrencyIdentificationIndicator" => 176u32, + "CurrencyInstructionId" => 177u32, + "CurrencyMargin" => 178u32, + "CurrencyPriorityReport" => 179u32, + "CurrencyReconciliationId" => 180u32, + "CurrencyReference" => 181u32, + "CurrencySequenceReport" => 182u32, + "CurrencyStatementCode" => 183u32, + "CurrencyStatusCode" => 184u32, + "CurrencyStatusDate" => 185u32, + "CurrencyTransactionFlag" => 186u32, + "CurrencyTransactionReport" => 187u32, + "CurrencyValueDateCount" => 188u32, + "CurrencyValueDateSub" => 189u32, + "CurrencyVersionSub" => 190u32, + "IdentificationAdjustmentFlag" => 191u32, + "IdentificationChannelSub" => 192u32, + "IdentificationClearingDate" => 193u32, + "IdentificationCollateralDate" => 194u32, + "IdentificationCurrencyId" => 195u32, + "IdentificationMarginCount" => 196u32, + "IdentificationMarginReport" => 197u32, + "IdentificationMarginSub" => 198u32, + "IdentificationParty" => 199u32, + "IdentificationPartyType" => 200u32, + "IdentificationPriority1" => 201u32, + "IdentificationReconciliation" => 202u32, + "IdentificationReconciliationCode" => 203u32, + "IdentificationSettlementCount" => 204u32, + "IdentificationStatementFlag" => 205u32, + "IdentificationTransaction1" => 206u32, + "IdentificationTransactionDate" => 207u32, + "IdentificationTransmissionReport" => 208u32, + "InstructionAccountId" => 209u32, + "InstructionAdjustmentReport" => 210u32, + "InstructionAuthentication" => 211u32, + "InstructionBeneficiary" => 212u32, + "InstructionBeneficiary1" => 213u32, + "InstructionCategory1" => 214u32, + "InstructionChannel2" => 215u32, + "InstructionChannelSub" => 216u32, + "InstructionCurrency" => 217u32, + "InstructionCurrency2" => 218u32, + "InstructionInstructionCode" => 219u32, + "InstructionPriority" => 220u32, + "InstructionPriorityCount" => 221u32, + "InstructionReconciliation1" => 222u32, + "InstructionReconciliationIndicator" => 223u32, + "InstructionReference" => 224u32, + "InstructionReferenceDate" => 225u32, + "InstructionReferenceIndicator" => 226u32, + "InstructionRemittanceReport" => 227u32, + "InstructionRouting2" => 228u32, + "InstructionRoutingFlag" => 229u32, + "InstructionRoutingType" => 230u32, + "InstructionSequence2" => 231u32, + "InstructionSequenceId" => 232u32, + "InstructionStatusCount" => 233u32, + "InstructionTransmissionType" => 234u32, + "InstructionValueDateSub" => 235u32, + "InstructionVersion" => 236u32, + "MarginAccountReport" => 237u32, + "MarginAccountSub" => 238u32, + "MarginAmount" => 239u32, + "MarginAuthentication" => 240u32, + "MarginBeneficiaryType" => 241u32, + "MarginCategoryFlag" => 242u32, + "MarginCollateralReport" => 243u32, + "MarginMarginDate" => 244u32, + "MarginMarginIndicator" => 245u32, + "MarginPartyType" => 246u32, + "MarginPosition" => 247u32, + "MarginPositionCount" => 248u32, + "MarginPriorityId" => 249u32, + "MarginReference1" => 250u32, + "MarginRemittanceReport" => 251u32, + "MarginRouting" => 252u32, + "MarginSettlementCode" => 253u32, + "MarginTransactionCode" => 254u32, + "MarginTransmission" => 255u32, + "MarginValueDateId" => 256u32, + "PartyAccount1" => 257u32, + "PartyBeneficiary1" => 258u32, + "PartyBeneficiaryId" => 259u32, + "PartyCategory" => 260u32, + "PartyClearing" => 261u32, + "PartyClearingDate" => 262u32, + "PartyCurrencyDate" => 263u32, + "PartyPartyId" => 264u32, + "PartyPriorityCount" => 265u32, + "PartyReconciliationCount" => 266u32, + "PartyReconciliationType" => 267u32, + "PartyRouting" => 268u32, + "PartyRoutingId" => 269u32, + "PartyRoutingReport" => 270u32, + "PartySettlement1" => 271u32, + "PartyStatement" => 272u32, + "PartyStatusDate" => 273u32, + "PartyStatusIndicator" => 274u32, + "PartyTransaction2" => 275u32, + "PartyTransactionFlag" => 276u32, + "PartyTransactionReport" => 277u32, + "PartyTransmissionReport" => 278u32, + "PartyVersion1" => 279u32, + "PositionAdjustmentFlag" => 280u32, + "PositionAdjustmentSub" => 281u32, + "PositionAmountSub" => 282u32, + "PositionAuthenticationDate" => 283u32, + "PositionCollateralFlag" => 284u32, + "PositionIdentification" => 285u32, + "PositionIdentificationType" => 286u32, + "PositionInstruction" => 287u32, + "PositionInstructionIndicator" => 288u32, + "PositionMargin" => 289u32, + "PositionPartyFlag" => 290u32, + "PositionPositionFlag" => 291u32, + "PositionReconciliationCode" => 292u32, + "PositionReconciliationId" => 293u32, + "PositionReconciliationIndicator" => 294u32, + "PositionReconciliationReport" => 295u32, + "PositionReferenceCount" => 296u32, + "PositionReferenceDate" => 297u32, + "PositionRemittanceIndicator" => 298u32, + "PositionSequence2" => 299u32, + "PositionStatement" => 300u32, + "PositionStatusCode" => 301u32, + "PositionTransmissionId" => 302u32, + "PositionVersion2" => 303u32, + "PriorityAdjustmentSub" => 304u32, + "PriorityAmount2" => 305u32, + "PriorityAmountIndicator" => 306u32, + "PriorityBeneficiaryIndicator" => 307u32, + "PriorityCategoryId" => 308u32, + "PriorityChannel2" => 309u32, + "PriorityChannelFlag" => 310u32, + "PriorityChannelReport" => 311u32, + "PriorityClearing2" => 312u32, + "PriorityCollateralCode" => 313u32, + "PriorityCurrency" => 314u32, + "PriorityIdentificationIndicator" => 315u32, + "PriorityInstructionCode" => 316u32, + "PriorityMarginId" => 317u32, + "PriorityMarginSub" => 318u32, + "PriorityPartyId" => 319u32, + "PriorityPositionFlag" => 320u32, + "PriorityPriorityCount" => 321u32, + "PriorityReconciliationId" => 322u32, + "PriorityRemittanceCount" => 323u32, + "PriorityRemittanceFlag" => 324u32, + "PriorityRemittanceType" => 325u32, + "PriorityRouting1" => 326u32, + "PriorityRoutingCount" => 327u32, + "PriorityRoutingType" => 328u32, + "PrioritySequenceDate" => 329u32, + "PrioritySettlement" => 330u32, + "PrioritySettlementCode" => 331u32, + "PriorityStatus2" => 332u32, + "PriorityStatusCode" => 333u32, + "PriorityStatusDate" => 334u32, + "PriorityTransactionCount" => 335u32, + "PriorityTransmission2" => 336u32, + "PriorityTransmissionFlag" => 337u32, + "PriorityValueDate1" => 338u32, + "ReconciliationAdjustment1" => 339u32, + "ReconciliationAdjustmentSub" => 340u32, + "ReconciliationBeneficiary2" => 341u32, + "ReconciliationChannelFlag" => 342u32, + "ReconciliationCollateralType" => 343u32, + "ReconciliationCurrencyReport" => 344u32, + "ReconciliationIdentification1" => 345u32, + "ReconciliationInstructionType" => 346u32, + "ReconciliationParty" => 347u32, + "ReconciliationPartyDate" => 348u32, + "ReconciliationPartyType" => 349u32, + "ReconciliationPositionDate" => 350u32, + "ReconciliationPositionId" => 351u32, + "ReconciliationPriority1" => 352u32, + "ReconciliationReconciliationCode" => 353u32, + "ReconciliationReconciliationDate" => 354u32, + "ReconciliationRouting" => 355u32, + "ReconciliationRoutingCode" => 356u32, + "ReconciliationRoutingIndicator" => 357u32, + "ReconciliationSettlementSub" => 358u32, + "ReconciliationStatementReport" => 359u32, + "ReferenceAmountType" => 360u32, + "ReferenceBeneficiaryDate" => 361u32, + "ReferenceChannel" => 362u32, + "ReferenceClearing2" => 363u32, + "ReferenceClearingIndicator" => 364u32, + "ReferenceCurrency" => 365u32, + "ReferenceInstructionFlag" => 366u32, + "ReferencePartyCount" => 367u32, + "ReferencePartyType" => 368u32, + "ReferencePosition" => 369u32, + "ReferenceReconciliationType" => 370u32, + "ReferenceReferenceSub" => 371u32, + "ReferenceRemittance" => 372u32, + "ReferenceRemittance2" => 373u32, + "ReferenceRemittanceCount" => 374u32, + "ReferenceSequenceSub" => 375u32, + "ReferenceStatement" => 376u32, + "ReferenceStatus" => 377u32, + "ReferenceTransactionDate" => 378u32, + "ReferenceTransmissionSub" => 379u32, + "ReferenceVersion" => 380u32, + "RemittanceAdjustmentType" => 381u32, + "RemittanceCategoryFlag" => 382u32, + "RemittanceCategoryType" => 383u32, + "RemittanceCollateral2" => 384u32, + "RemittanceCollateralFlag" => 385u32, + "RemittanceIdentification1" => 386u32, + "RemittanceInstructionType" => 387u32, + "RemittanceMargin" => 388u32, + "RemittancePositionCount" => 389u32, + "RemittancePriority" => 390u32, + "RemittancePriorityCount" => 391u32, + "RemittanceRemittance" => 392u32, + "RemittanceSequenceDate" => 393u32, + "RemittanceStatusCount" => 394u32, + "RemittanceStatusIndicator" => 395u32, + "RemittanceTransmission2" => 396u32, + "RemittanceTransmissionCode" => 397u32, + "RemittanceTransmissionIndicator" => 398u32, + "RemittanceVersionDate" => 399u32, + "RoutingAccountDate" => 400u32, + "RoutingAmount1" => 401u32, + "RoutingAmountCode" => 402u32, + "RoutingAuthenticationIndicator" => 403u32, + "RoutingCategoryCode" => 404u32, + "RoutingCategoryType" => 405u32, + "RoutingClearingDate" => 406u32, + "RoutingCurrencySub" => 407u32, + "RoutingIdentificationCode" => 408u32, + "RoutingPartyType" => 409u32, + "RoutingPriority2" => 410u32, + "RoutingPriorityFlag" => 411u32, + "RoutingPriorityId" => 412u32, + "RoutingReconciliationSub" => 413u32, + "RoutingReferenceDate" => 414u32, + "RoutingRemittance" => 415u32, + "RoutingRemittance2" => 416u32, + "RoutingRouting" => 417u32, + "RoutingRoutingDate" => 418u32, + "RoutingRoutingId" => 419u32, + "RoutingRoutingIndicator" => 420u32, + "RoutingSequenceType" => 421u32, + "RoutingSettlement1" => 422u32, + "RoutingTransactionReport" => 423u32, + "RoutingTransmissionIndicator" => 424u32, + "RoutingValueDateCode" => 425u32, + "RoutingValueDateCount" => 426u32, + "RoutingVersionIndicator" => 427u32, + "RoutingVersionType" => 428u32, + "SequenceAccount2" => 429u32, + "SequenceAccountType" => 430u32, + "SequenceAdjustmentDate" => 431u32, + "SequenceAdjustmentIndicator" => 432u32, + "SequenceAmount1" => 433u32, + "SequenceAmountType" => 434u32, + "SequenceBeneficiary1" => 435u32, + "SequenceBeneficiaryCount" => 436u32, + "SequenceChannel" => 437u32, + "SequenceParty" => 438u32, + "SequencePartyId" => 439u32, + "SequenceReconciliationId" => 440u32, + "SequenceReconciliationReport" => 441u32, + "SequenceReferenceId" => 442u32, + "SequenceStatus" => 443u32, + "SequenceTransaction1" => 444u32, + "SequenceTransactionDate" => 445u32, + "SequenceTransactionFlag" => 446u32, + "SequenceTransmission" => 447u32, + "SequenceTransmissionDate" => 448u32, + "SettlementAccount" => 449u32, + "SettlementBeneficiaryDate" => 450u32, + "SettlementBeneficiaryType" => 451u32, + "SettlementCategoryCount" => 452u32, + "SettlementChannel" => 453u32, + "SettlementInstructionCode" => 454u32, + "SettlementPositionIndicator" => 455u32, + "SettlementReferenceReport" => 456u32, + "SettlementRemittanceIndicator" => 457u32, + "SettlementRoutingReport" => 458u32, + "SettlementSequenceReport" => 459u32, + "SettlementStatementType" => 460u32, + "SettlementStatusCount" => 461u32, + "SettlementTransmissionCode" => 462u32, + "SettlementTransmissionFlag" => 463u32, + "SettlementVersion" => 464u32, + "StatementAccount" => 465u32, + "StatementAuthentication" => 466u32, + "StatementAuthenticationFlag" => 467u32, + "StatementBeneficiary" => 468u32, + "StatementBeneficiarySub" => 469u32, + "StatementChannelReport" => 470u32, + "StatementMargin" => 471u32, + "StatementPositionType" => 472u32, + "StatementPriority2" => 473u32, + "StatementReference" => 474u32, + "StatementReferenceCode" => 475u32, + "StatementReferenceIndicator" => 476u32, + "StatementRemittanceFlag" => 477u32, + "StatementRoutingCode" => 478u32, + "StatementSequenceId" => 479u32, + "StatementSettlement" => 480u32, + "StatementSettlement2" => 481u32, + "StatementValueDateCount" => 482u32, + "StatementValueDateReport" => 483u32, + "StatusAccountType" => 484u32, + "StatusAmount2" => 485u32, + "StatusAuthentication" => 486u32, + "StatusAuthentication2" => 487u32, + "StatusAuthenticationId" => 488u32, + "StatusAuthenticationIndicator" => 489u32, + "StatusCategoryIndicator" => 490u32, + "StatusChannelFlag" => 491u32, + "StatusClearingReport" => 492u32, + "StatusCurrency2" => 493u32, + "StatusInstructionReport" => 494u32, + "StatusPartyIndicator" => 495u32, + "StatusPositionSub" => 496u32, + "StatusPriority1" => 497u32, + "StatusPriorityCount" => 498u32, + "StatusPriorityIndicator" => 499u32, + "StatusReconciliationReport" => 500u32, + "StatusRouting" => 501u32, + "StatusRoutingCode" => 502u32, + "StatusRoutingFlag" => 503u32, + "StatusTransmission" => 504u32, + "StatusTransmissionType" => 505u32, + "StatusValueDateSub" => 506u32, + "StatusVersionDate" => 507u32, + "TransactionAuthentication" => 508u32, + "TransactionAuthentication1" => 509u32, + "TransactionAuthenticationId" => 510u32, + "TransactionClearing" => 511u32, + "TransactionClearingCode" => 512u32, + "TransactionCollateralId" => 513u32, + "TransactionCurrencyIndicator" => 514u32, + "TransactionInstructionCode" => 515u32, + "TransactionPartySub" => 516u32, + "TransactionPosition" => 517u32, + "TransactionPriorityCode" => 518u32, + "TransactionReference2" => 519u32, + "TransactionRouting1" => 520u32, + "TransactionRoutingIndicator" => 521u32, + "TransactionStatementDate" => 522u32, + "TransactionTransactionId" => 523u32, + "TransactionTransmissionType" => 524u32, + "TransactionVersion1" => 525u32, + "TransactionVersionType" => 526u32, + "TransmissionAccountId" => 527u32, + "TransmissionAdjustmentSub" => 528u32, + "TransmissionAmountFlag" => 529u32, + "TransmissionBeneficiaryReport" => 530u32, + "TransmissionCategoryIndicator" => 531u32, + "TransmissionCollateral1" => 532u32, + "TransmissionIdentificationFlag" => 533u32, + "TransmissionInstructionId" => 534u32, + "TransmissionMargin2" => 535u32, + "TransmissionMarginDate" => 536u32, + "TransmissionPositionType" => 537u32, + "TransmissionPriorityCode" => 538u32, + "TransmissionPriorityIndicator" => 539u32, + "TransmissionReconciliation" => 540u32, + "TransmissionReconciliationCount" => 541u32, + "TransmissionReconciliationDate" => 542u32, + "TransmissionRemittanceSub" => 543u32, + "TransmissionRoutingCount" => 544u32, + "TransmissionRoutingId" => 545u32, + "TransmissionSequenceDate" => 546u32, + "TransmissionStatementId" => 547u32, + "TransmissionStatusCode" => 548u32, + "TransmissionTransmission" => 549u32, + "TransmissionTransmissionFlag" => 550u32, + "TransmissionVersion" => 551u32, + "ValueDateAccountFlag" => 552u32, + "ValueDateAdjustmentReport" => 553u32, + "ValueDateChannel1" => 554u32, + "ValueDateChannelFlag" => 555u32, + "ValueDateChannelId" => 556u32, + "ValueDateClearingDate" => 557u32, + "ValueDateClearingReport" => 558u32, + "ValueDateCurrencyId" => 559u32, + "ValueDateCurrencyType" => 560u32, + "ValueDateIdentification2" => 561u32, + "ValueDateIdentificationIndicator" => 562u32, + "ValueDateInstruction" => 563u32, + "ValueDateInstruction1" => 564u32, + "ValueDateMarginType" => 565u32, + "ValueDatePosition2" => 566u32, + "ValueDatePositionCount" => 567u32, + "ValueDateReconciliationCount" => 568u32, + "ValueDateReconciliationId" => 569u32, + "ValueDateReferenceFlag" => 570u32, + "ValueDateRemittanceCount" => 571u32, + "ValueDateRemittanceIndicator" => 572u32, + "ValueDateRoutingFlag" => 573u32, + "ValueDateSequenceId" => 574u32, + "ValueDateSettlementCount" => 575u32, + "ValueDateStatementDate" => 576u32, + "ValueDateTransmission" => 577u32, + "ValueDateTransmissionCode" => 578u32, + "ValueDateValueDateReport" => 579u32, + "ValueDateVersionReport" => 580u32, + "VersionAccount" => 581u32, + "VersionAdjustment" => 582u32, + "VersionAdjustment1" => 583u32, + "VersionAuthenticationCode" => 584u32, + "VersionBeneficiaryCount" => 585u32, + "VersionBeneficiaryFlag" => 586u32, + "VersionCategoryFlag" => 587u32, + "VersionClearingFlag" => 588u32, + "VersionMargin" => 589u32, + "VersionMarginFlag" => 590u32, + "VersionMarginReport" => 591u32, + "VersionReconciliation1" => 592u32, + "VersionReferenceDate" => 593u32, + "VersionReferenceIndicator" => 594u32, + "VersionRemittance" => 595u32, + "VersionStatusCode" => 596u32, + "VersionStatusType" => 597u32, + "VersionTransactionId" => 598u32, + "VersionTransmissionCount" => 599u32, +}; + +// --- Tier 1500 (large) --- +pub const TAGS_1500: [&str; 1500] = [ + "AccountAccount", + "AccountAccount1", + "AccountAdjustment2", + "AccountAmountDate", + "AccountAmountFlag", + "AccountAmountType", + "AccountAuthenticationType", + "AccountBeneficiaryCode", + "AccountBeneficiaryCount", + "AccountBeneficiaryFlag", + "AccountCategory", + "AccountCategoryDate", + "AccountCategoryFlag", + "AccountCategoryIndicator", + "AccountChannel", + "AccountChannelCount", + "AccountChannelId", + "AccountChannelIndicator", + "AccountClearing1", + "AccountCollateral2", + "AccountCollateralFlag", + "AccountCollateralReport", + "AccountCurrency1", + "AccountCurrencyFlag", + "AccountCurrencyId", + "AccountCurrencyIndicator", + "AccountCurrencySub", + "AccountIdentification", + "AccountIdentification1", + "AccountInstructionReport", + "AccountMarginFlag", + "AccountMarginType", + "AccountParty1", + "AccountPartyCount", + "AccountPartyFlag", + "AccountPartyIndicator", + "AccountPartyType", + "AccountPosition", + "AccountPriorityDate", + "AccountReconciliation", + "AccountReconciliationType", + "AccountReferenceId", + "AccountReferenceIndicator", + "AccountReferenceType", + "AccountRemittanceId", + "AccountRemittanceIndicator", + "AccountRoutingCount", + "AccountRoutingIndicator", + "AccountRoutingType", + "AccountSequence", + "AccountSequence1", + "AccountSequence2", + "AccountSettlementDate", + "AccountSettlementType", + "AccountStatement", + "AccountStatementCount", + "AccountStatusFlag", + "AccountStatusType", + "AccountTransaction", + "AccountTransactionCode", + "AccountTransactionSub", + "AccountTransactionType", + "AccountTransmission", + "AccountTransmission1", + "AccountVersion", + "AccountVersionSub", + "AccountVersionType", + "AdjustmentAccount", + "AdjustmentAccount1", + "AdjustmentAccountCode", + "AdjustmentAdjustment2", + "AdjustmentAdjustmentIndicator", + "AdjustmentAmount", + "AdjustmentAuthentication", + "AdjustmentAuthenticationFlag", + "AdjustmentBeneficiaryCount", + "AdjustmentBeneficiaryId", + "AdjustmentCategory1", + "AdjustmentChannelFlag", + "AdjustmentChannelType", + "AdjustmentClearingDate", + "AdjustmentClearingId", + "AdjustmentCollateralIndicator", + "AdjustmentCurrency", + "AdjustmentCurrencyId", + "AdjustmentCurrencyReport", + "AdjustmentIdentificationReport", + "AdjustmentInstructionDate", + "AdjustmentInstructionType", + "AdjustmentMarginCode", + "AdjustmentMarginReport", + "AdjustmentPosition2", + "AdjustmentPositionCount", + "AdjustmentPositionDate", + "AdjustmentReconciliation", + "AdjustmentReconciliationCount", + "AdjustmentReference1", + "AdjustmentRemittance", + "AdjustmentRemittance1", + "AdjustmentRemittanceFlag", + "AdjustmentRoutingIndicator", + "AdjustmentSequence", + "AdjustmentSequenceReport", + "AdjustmentSettlementFlag", + "AdjustmentStatement", + "AdjustmentStatementCode", + "AdjustmentStatementFlag", + "AdjustmentStatementId", + "AdjustmentStatementIndicator", + "AdjustmentStatusDate", + "AdjustmentStatusType", + "AdjustmentTransactionDate", + "AdjustmentTransmissionCode", + "AdjustmentTransmissionCount", + "AdjustmentTransmissionId", + "AdjustmentTransmissionSub", + "AdjustmentValueDateFlag", + "AdjustmentVersionType", + "AmountAccount1", + "AmountAccountCode", + "AmountAccountFlag", + "AmountAdjustment", + "AmountAdjustmentCode", + "AmountAdjustmentFlag", + "AmountAuthentication", + "AmountAuthenticationId", + "AmountAuthenticationIndicator", + "AmountChannel", + "AmountChannelCode", + "AmountChannelDate", + "AmountCollateralFlag", + "AmountCurrency", + "AmountCurrencyCount", + "AmountIdentificationIndicator", + "AmountIdentificationSub", + "AmountInstructionFlag", + "AmountInstructionReport", + "AmountPartyId", + "AmountPartyIndicator", + "AmountPriorityReport", + "AmountReconciliationIndicator", + "AmountReconciliationSub", + "AmountReference", + "AmountReferenceIndicator", + "AmountRemittance", + "AmountRemittance1", + "AmountRemittance2", + "AmountRemittanceFlag", + "AmountRemittanceReport", + "AmountSequenceDate", + "AmountSequenceReport", + "AmountSettlementDate", + "AmountStatementIndicator", + "AmountStatusFlag", + "AmountStatusIndicator", + "AmountTransaction", + "AmountTransactionCount", + "AmountTransmission2", + "AmountTransmissionCode", + "AmountTransmissionIndicator", + "AmountValueDate", + "AmountValueDate1", + "AmountVersionDate", + "AuthenticationAccountDate", + "AuthenticationAccountId", + "AuthenticationAccountType", + "AuthenticationAdjustmentType", + "AuthenticationAmountFlag", + "AuthenticationAuthentication1", + "AuthenticationBeneficiaryCode", + "AuthenticationBeneficiaryDate", + "AuthenticationBeneficiaryId", + "AuthenticationBeneficiaryIndicator", + "AuthenticationBeneficiarySub", + "AuthenticationCategory1", + "AuthenticationCategoryDate", + "AuthenticationCategoryFlag", + "AuthenticationChannel1", + "AuthenticationChannel2", + "AuthenticationChannelFlag", + "AuthenticationClearing", + "AuthenticationClearingIndicator", + "AuthenticationClearingReport", + "AuthenticationCollateralType", + "AuthenticationCurrencyIndicator", + "AuthenticationCurrencyType", + "AuthenticationIdentificationFlag", + "AuthenticationIdentificationReport", + "AuthenticationInstruction", + "AuthenticationInstructionId", + "AuthenticationMarginCount", + "AuthenticationPartyCode", + "AuthenticationPosition1", + "AuthenticationPositionFlag", + "AuthenticationPositionReport", + "AuthenticationPriorityCode", + "AuthenticationPriorityIndicator", + "AuthenticationReconciliationIndicator", + "AuthenticationReference2", + "AuthenticationReferenceFlag", + "AuthenticationReferenceId", + "AuthenticationRemittance1", + "AuthenticationRemittanceFlag", + "AuthenticationRemittanceIndicator", + "AuthenticationRemittanceType", + "AuthenticationRouting", + "AuthenticationRouting1", + "AuthenticationSequenceCode", + "AuthenticationSequenceId", + "AuthenticationSequenceIndicator", + "AuthenticationSequenceSub", + "AuthenticationSettlement", + "AuthenticationSettlement1", + "AuthenticationSettlementId", + "AuthenticationSettlementSub", + "AuthenticationStatementCount", + "AuthenticationStatementId", + "AuthenticationStatusId", + "AuthenticationTransaction1", + "AuthenticationTransactionDate", + "AuthenticationTransactionSub", + "AuthenticationTransactionType", + "AuthenticationTransmissionCode", + "AuthenticationTransmissionIndicator", + "AuthenticationTransmissionReport", + "AuthenticationValueDate2", + "AuthenticationVersion1", + "BeneficiaryAccount1", + "BeneficiaryAccountReport", + "BeneficiaryAccountType", + "BeneficiaryAdjustmentCount", + "BeneficiaryAdjustmentDate", + "BeneficiaryAdjustmentType", + "BeneficiaryAmountType", + "BeneficiaryAuthenticationCode", + "BeneficiaryAuthenticationSub", + "BeneficiaryAuthenticationType", + "BeneficiaryBeneficiary", + "BeneficiaryBeneficiary2", + "BeneficiaryBeneficiaryCode", + "BeneficiaryBeneficiaryCount", + "BeneficiaryBeneficiaryIndicator", + "BeneficiaryBeneficiaryReport", + "BeneficiaryCategory", + "BeneficiaryCategoryCode", + "BeneficiaryChannel2", + "BeneficiaryChannelCount", + "BeneficiaryChannelId", + "BeneficiaryCollateralCount", + "BeneficiaryCollateralFlag", + "BeneficiaryCollateralIndicator", + "BeneficiaryCurrencyType", + "BeneficiaryIdentification1", + "BeneficiaryIdentification2", + "BeneficiaryIdentificationReport", + "BeneficiaryInstructionReport", + "BeneficiaryInstructionSub", + "BeneficiaryMarginId", + "BeneficiaryPartyCode", + "BeneficiaryPartyFlag", + "BeneficiaryPartyIndicator", + "BeneficiaryPosition", + "BeneficiaryPriorityType", + "BeneficiaryReference1", + "BeneficiaryReferenceDate", + "BeneficiaryReferenceFlag", + "BeneficiaryReferenceReport", + "BeneficiaryRemittance", + "BeneficiaryRemittance2", + "BeneficiaryRemittanceCount", + "BeneficiaryRemittanceSub", + "BeneficiaryRoutingIndicator", + "BeneficiarySequenceId", + "BeneficiarySettlementIndicator", + "BeneficiaryStatus2", + "BeneficiaryStatusCount", + "BeneficiaryStatusDate", + "BeneficiaryTransactionSub", + "BeneficiaryTransmission", + "BeneficiaryTransmission1", + "BeneficiaryTransmission2", + "BeneficiaryTransmissionFlag", + "BeneficiaryTransmissionIndicator", + "BeneficiaryValueDate", + "BeneficiaryValueDate1", + "BeneficiaryVersionCount", + "BeneficiaryVersionFlag", + "CategoryAccount1", + "CategoryAccountDate", + "CategoryAccountFlag", + "CategoryAccountType", + "CategoryAdjustment1", + "CategoryAdjustmentDate", + "CategoryAmount", + "CategoryAmountIndicator", + "CategoryAuthentication1", + "CategoryAuthentication2", + "CategoryAuthenticationIndicator", + "CategoryAuthenticationType", + "CategoryBeneficiaryCode", + "CategoryBeneficiaryIndicator", + "CategoryChannel", + "CategoryChannelIndicator", + "CategoryClearingFlag", + "CategoryClearingType", + "CategoryCollateral1", + "CategoryCollateralFlag", + "CategoryCollateralType", + "CategoryCurrency", + "CategoryCurrency1", + "CategoryCurrencyReport", + "CategoryCurrencySub", + "CategoryCurrencyType", + "CategoryIdentification1", + "CategoryIdentificationSub", + "CategoryIdentificationType", + "CategoryInstruction", + "CategoryInstructionCount", + "CategoryInstructionIndicator", + "CategoryMargin", + "CategoryPartySub", + "CategoryPositionId", + "CategoryPositionIndicator", + "CategoryPositionReport", + "CategoryPriority1", + "CategoryPriorityId", + "CategoryReconciliation1", + "CategoryReconciliation2", + "CategoryReconciliationId", + "CategoryReconciliationSub", + "CategoryReferenceDate", + "CategoryRemittance1", + "CategoryRemittanceDate", + "CategoryRemittanceFlag", + "CategoryRemittanceReport", + "CategoryRouting", + "CategoryRoutingIndicator", + "CategorySequenceCount", + "CategorySequenceIndicator", + "CategorySettlement", + "CategoryStatusCount", + "CategoryStatusReport", + "CategoryTransaction", + "CategoryTransactionDate", + "CategoryTransmission", + "CategoryTransmissionCode", + "CategoryValueDateIndicator", + "CategoryVersion", + "CategoryVersion1", + "ChannelAccount2", + "ChannelAccountCode", + "ChannelAccountFlag", + "ChannelAdjustment", + "ChannelAdjustmentIndicator", + "ChannelAmount", + "ChannelAmountIndicator", + "ChannelAmountReport", + "ChannelAuthentication", + "ChannelAuthenticationReport", + "ChannelAuthenticationType", + "ChannelBeneficiary1", + "ChannelBeneficiaryDate", + "ChannelCategoryId", + "ChannelCategoryType", + "ChannelChannelCode", + "ChannelClearingDate", + "ChannelCollateralCount", + "ChannelCollateralReport", + "ChannelCollateralSub", + "ChannelIdentification", + "ChannelIdentificationFlag", + "ChannelIdentificationId", + "ChannelInstructionFlag", + "ChannelMargin2", + "ChannelMarginCount", + "ChannelMarginDate", + "ChannelMarginIndicator", + "ChannelParty", + "ChannelParty2", + "ChannelPosition", + "ChannelPositionId", + "ChannelPositionIndicator", + "ChannelPriority", + "ChannelPriority2", + "ChannelPriorityCount", + "ChannelReconciliation", + "ChannelReference", + "ChannelReferenceCode", + "ChannelReferenceFlag", + "ChannelRemittance", + "ChannelRemittanceFlag", + "ChannelRemittanceId", + "ChannelRoutingId", + "ChannelRoutingReport", + "ChannelRoutingSub", + "ChannelSettlement", + "ChannelSettlementSub", + "ChannelStatement1", + "ChannelStatementId", + "ChannelStatus1", + "ChannelTransactionCount", + "ChannelTransactionFlag", + "ChannelTransmissionIndicator", + "ChannelValueDate1", + "ChannelValueDateId", + "ChannelValueDateSub", + "ChannelValueDateType", + "ClearingAccount", + "ClearingAccountFlag", + "ClearingAdjustment", + "ClearingAdjustmentDate", + "ClearingAmountCount", + "ClearingAuthenticationDate", + "ClearingBeneficiaryId", + "ClearingBeneficiaryReport", + "ClearingClearingDate", + "ClearingClearingId", + "ClearingClearingReport", + "ClearingCollateral", + "ClearingCollateral2", + "ClearingCollateralCount", + "ClearingIdentificationType", + "ClearingInstructionCount", + "ClearingInstructionIndicator", + "ClearingMarginFlag", + "ClearingParty", + "ClearingPartyCount", + "ClearingPartyId", + "ClearingPartyIndicator", + "ClearingPosition", + "ClearingReconciliation", + "ClearingReconciliationSub", + "ClearingReferenceCode", + "ClearingReferenceSub", + "ClearingRemittanceReport", + "ClearingRouting", + "ClearingRoutingCount", + "ClearingSequenceCount", + "ClearingSequenceId", + "ClearingSettlement", + "ClearingStatementType", + "ClearingStatus2", + "ClearingStatusFlag", + "ClearingStatusId", + "ClearingTransactionIndicator", + "ClearingTransmissionCount", + "ClearingTransmissionReport", + "ClearingTransmissionType", + "ClearingValueDateCode", + "ClearingValueDateDate", + "CollateralAccount2", + "CollateralAccountCount", + "CollateralAdjustment2", + "CollateralAdjustmentDate", + "CollateralAdjustmentSub", + "CollateralAmountType", + "CollateralAuthentication2", + "CollateralAuthenticationCode", + "CollateralBeneficiary", + "CollateralBeneficiaryCode", + "CollateralBeneficiaryIndicator", + "CollateralCategory", + "CollateralCategory2", + "CollateralCategoryType", + "CollateralChannelCode", + "CollateralChannelCount", + "CollateralChannelDate", + "CollateralChannelReport", + "CollateralChannelSub", + "CollateralClearingDate", + "CollateralCollateralCode", + "CollateralCollateralCount", + "CollateralCollateralFlag", + "CollateralIdentification", + "CollateralInstructionCode", + "CollateralInstructionFlag", + "CollateralInstructionSub", + "CollateralMarginId", + "CollateralParty", + "CollateralPartyReport", + "CollateralPositionCount", + "CollateralPositionSub", + "CollateralPositionType", + "CollateralPriority2", + "CollateralPriorityDate", + "CollateralPriorityReport", + "CollateralReconciliation", + "CollateralReconciliation2", + "CollateralReconciliationCount", + "CollateralReconciliationDate", + "CollateralReference", + "CollateralReference2", + "CollateralReferenceType", + "CollateralRoutingReport", + "CollateralSequence2", + "CollateralSequenceCount", + "CollateralSettlementCode", + "CollateralSettlementDate", + "CollateralStatement", + "CollateralStatementReport", + "CollateralStatus", + "CollateralStatus2", + "CollateralStatusIndicator", + "CollateralTransaction2", + "CollateralTransmission2", + "CollateralValueDate2", + "CollateralValueDateCode", + "CollateralVersionIndicator", + "CollateralVersionType", + "CurrencyAccountId", + "CurrencyAccountSub", + "CurrencyAmountIndicator", + "CurrencyAuthentication", + "CurrencyBeneficiaryIndicator", + "CurrencyCategoryDate", + "CurrencyChannel2", + "CurrencyChannelCount", + "CurrencyChannelId", + "CurrencyClearing", + "CurrencyCollateralCount", + "CurrencyCollateralType", + "CurrencyCurrency", + "CurrencyCurrency1", + "CurrencyInstruction1", + "CurrencyInstructionSub", + "CurrencyMarginDate", + "CurrencyMarginId", + "CurrencyPartyCount", + "CurrencyPartyId", + "CurrencyPartySub", + "CurrencyPosition", + "CurrencyPositionCount", + "CurrencyPositionSub", + "CurrencyPriority", + "CurrencyReference2", + "CurrencyReferenceIndicator", + "CurrencyRouting", + "CurrencyRouting2", + "CurrencyRoutingId", + "CurrencySequenceCode", + "CurrencySequenceId", + "CurrencySequenceReport", + "CurrencySettlementType", + "CurrencyStatementSub", + "CurrencyStatusCode", + "CurrencyStatusType", + "CurrencyTransactionIndicator", + "CurrencyTransmissionDate", + "CurrencyValueDateIndicator", + "CurrencyVersionDate", + "IdentificationAccountIndicator", + "IdentificationAdjustmentCount", + "IdentificationAdjustmentIndicator", + "IdentificationAdjustmentReport", + "IdentificationAdjustmentType", + "IdentificationAmount", + "IdentificationAmountCount", + "IdentificationAmountId", + "IdentificationAmountSub", + "IdentificationAuthenticationId", + "IdentificationAuthenticationIndicator", + "IdentificationCategoryId", + "IdentificationClearing", + "IdentificationCollateral2", + "IdentificationCurrencyIndicator", + "IdentificationCurrencyType", + "IdentificationIdentificationCount", + "IdentificationIdentificationDate", + "IdentificationIdentificationSub", + "IdentificationInstructionDate", + "IdentificationInstructionId", + "IdentificationInstructionIndicator", + "IdentificationInstructionType", + "IdentificationMarginIndicator", + "IdentificationPartyFlag", + "IdentificationPartyType", + "IdentificationPositionType", + "IdentificationPriorityDate", + "IdentificationReconciliationCount", + "IdentificationReference1", + "IdentificationReferenceCode", + "IdentificationRouting", + "IdentificationSequence", + "IdentificationSequenceCode", + "IdentificationSequenceIndicator", + "IdentificationSettlement1", + "IdentificationSettlementId", + "IdentificationSettlementSub", + "IdentificationStatementFlag", + "IdentificationStatementIndicator", + "IdentificationStatementReport", + "IdentificationStatusCode", + "IdentificationStatusReport", + "IdentificationTransaction", + "IdentificationTransaction1", + "IdentificationTransactionType", + "IdentificationTransmission1", + "IdentificationValueDate", + "IdentificationValueDateCount", + "IdentificationValueDateDate", + "IdentificationValueDateId", + "IdentificationValueDateIndicator", + "IdentificationValueDateType", + "IdentificationVersion1", + "IdentificationVersionReport", + "InstructionAccount", + "InstructionAccountCode", + "InstructionAdjustment", + "InstructionAdjustmentId", + "InstructionAuthentication2", + "InstructionAuthenticationIndicator", + "InstructionBeneficiary", + "InstructionBeneficiaryDate", + "InstructionBeneficiaryReport", + "InstructionBeneficiaryType", + "InstructionCategoryCode", + "InstructionChannelIndicator", + "InstructionCurrency1", + "InstructionCurrencyCount", + "InstructionCurrencyId", + "InstructionIdentification", + "InstructionIdentificationType", + "InstructionInstruction2", + "InstructionInstructionIndicator", + "InstructionInstructionSub", + "InstructionMarginCode", + "InstructionParty", + "InstructionPositionCode", + "InstructionPositionType", + "InstructionPriority", + "InstructionPriorityFlag", + "InstructionPriorityId", + "InstructionReconciliation2", + "InstructionReconciliationType", + "InstructionReferenceIndicator", + "InstructionRemittanceCount", + "InstructionRemittanceId", + "InstructionSequenceFlag", + "InstructionSettlementType", + "InstructionStatusCode", + "InstructionStatusType", + "InstructionTransactionSub", + "InstructionTransmissionCode", + "InstructionTransmissionType", + "InstructionValueDate", + "InstructionValueDate1", + "InstructionValueDateIndicator", + "InstructionVersion2", + "MarginAccountIndicator", + "MarginAdjustmentDate", + "MarginAmount2", + "MarginAmountReport", + "MarginAuthentication2", + "MarginBeneficiary", + "MarginBeneficiaryReport", + "MarginBeneficiaryType", + "MarginCategory2", + "MarginChannel2", + "MarginClearing", + "MarginClearingCount", + "MarginCollateral1", + "MarginCurrencyCode", + "MarginIdentification", + "MarginIdentificationType", + "MarginInstruction1", + "MarginInstructionIndicator", + "MarginMarginDate", + "MarginMarginId", + "MarginPartyIndicator", + "MarginPositionCode", + "MarginPriorityDate", + "MarginPriorityFlag", + "MarginPrioritySub", + "MarginReconciliation", + "MarginReconciliation1", + "MarginReconciliationFlag", + "MarginReconciliationId", + "MarginReference1", + "MarginReferenceFlag", + "MarginReferenceId", + "MarginRemittance", + "MarginRemittanceType", + "MarginRouting", + "MarginRoutingType", + "MarginSequenceFlag", + "MarginSequenceId", + "MarginStatement", + "MarginStatementCount", + "MarginStatementReport", + "MarginStatusId", + "MarginTransactionCount", + "MarginTransactionFlag", + "MarginTransmissionId", + "MarginTransmissionSub", + "MarginValueDate2", + "PartyAccountReport", + "PartyAccountSub", + "PartyAccountType", + "PartyAdjustmentCode", + "PartyAdjustmentIndicator", + "PartyAdjustmentReport", + "PartyAmount1", + "PartyAmountDate", + "PartyAmountFlag", + "PartyAuthentication1", + "PartyCategoryCount", + "PartyChannel", + "PartyChannel2", + "PartyChannelSub", + "PartyCollateralReport", + "PartyCollateralSub", + "PartyCurrency", + "PartyCurrencyCode", + "PartyCurrencyType", + "PartyIdentificationIndicator", + "PartyInstructionDate", + "PartyInstructionId", + "PartyInstructionReport", + "PartyMarginType", + "PartyPosition2", + "PartyPositionId", + "PartyPositionType", + "PartyPriority", + "PartyPriority1", + "PartyPriorityCount", + "PartyReconciliationCode", + "PartyReconciliationFlag", + "PartyReconciliationId", + "PartyReferenceIndicator", + "PartyReferenceSub", + "PartyRemittanceType", + "PartyRouting1", + "PartyRouting2", + "PartyRoutingId", + "PartySequenceIndicator", + "PartySequenceReport", + "PartySettlement1", + "PartySettlementCount", + "PartyStatement", + "PartyStatementId", + "PartyStatementIndicator", + "PartyStatusId", + "PartyTransactionIndicator", + "PartyValueDateCount", + "PartyValueDateType", + "PartyVersion", + "PositionAccount", + "PositionAccount1", + "PositionAdjustmentCode", + "PositionAdjustmentDate", + "PositionAmount", + "PositionAmount1", + "PositionAmountSub", + "PositionAuthenticationFlag", + "PositionBeneficiaryFlag", + "PositionBeneficiaryReport", + "PositionCategorySub", + "PositionChannelFlag", + "PositionClearingReport", + "PositionClearingType", + "PositionCollateralCode", + "PositionCurrency2", + "PositionCurrencyReport", + "PositionCurrencySub", + "PositionIdentification1", + "PositionIdentificationCode", + "PositionInstruction", + "PositionInstructionReport", + "PositionMarginCode", + "PositionMarginCount", + "PositionMarginFlag", + "PositionMarginType", + "PositionPartyFlag", + "PositionPartyIndicator", + "PositionPositionReport", + "PositionPriorityFlag", + "PositionPriorityIndicator", + "PositionReconciliation1", + "PositionReconciliationReport", + "PositionReferenceSub", + "PositionRemittanceIndicator", + "PositionRoutingCount", + "PositionRoutingIndicator", + "PositionSequence", + "PositionSequenceCode", + "PositionSequenceDate", + "PositionSequenceIndicator", + "PositionStatement1", + "PositionStatementCount", + "PositionStatusSub", + "PositionTransmission", + "PositionTransmissionDate", + "PositionTransmissionType", + "PositionValueDate1", + "PositionValueDateFlag", + "PositionValueDateIndicator", + "PositionValueDateReport", + "PositionValueDateSub", + "PositionValueDateType", + "PriorityAccount1", + "PriorityAccountDate", + "PriorityAccountIndicator", + "PriorityAdjustment", + "PriorityAdjustment1", + "PriorityAdjustment2", + "PriorityAdjustmentCode", + "PriorityAdjustmentFlag", + "PriorityAdjustmentReport", + "PriorityAmount", + "PriorityAmountSub", + "PriorityAuthentication", + "PriorityAuthenticationDate", + "PriorityBeneficiaryCount", + "PriorityBeneficiaryIndicator", + "PriorityBeneficiaryType", + "PriorityChannelCount", + "PriorityChannelReport", + "PriorityChannelType", + "PriorityClearingType", + "PriorityCurrency", + "PriorityCurrencyId", + "PriorityCurrencyType", + "PriorityIdentificationDate", + "PriorityIdentificationId", + "PriorityInstruction2", + "PriorityInstructionCode", + "PriorityInstructionFlag", + "PriorityMargin2", + "PriorityMarginCount", + "PriorityParty", + "PriorityParty1", + "PriorityPartyDate", + "PriorityPartyFlag", + "PriorityPosition", + "PriorityPositionId", + "PriorityPositionType", + "PriorityPriority", + "PriorityReconciliation2", + "PriorityReconciliationCode", + "PriorityReconciliationFlag", + "PriorityReconciliationReport", + "PriorityReference1", + "PriorityReferenceCount", + "PriorityReferenceDate", + "PriorityReferenceId", + "PriorityRemittanceCode", + "PriorityRemittanceCount", + "PriorityRemittanceFlag", + "PriorityRoutingFlag", + "PrioritySequenceCount", + "PrioritySettlement", + "PriorityStatementCount", + "PriorityStatementFlag", + "PriorityStatementId", + "PriorityStatusId", + "PriorityStatusSub", + "PriorityTransactionCount", + "PriorityTransactionIndicator", + "PriorityTransmission", + "PriorityValueDate1", + "PriorityValueDate2", + "PriorityValueDateIndicator", + "PriorityVersion", + "PriorityVersionIndicator", + "PriorityVersionType", + "ReconciliationAccountReport", + "ReconciliationAdjustment", + "ReconciliationAmountCount", + "ReconciliationAuthenticationDate", + "ReconciliationBeneficiary1", + "ReconciliationBeneficiaryDate", + "ReconciliationChannel", + "ReconciliationChannelFlag", + "ReconciliationChannelSub", + "ReconciliationClearing1", + "ReconciliationClearingId", + "ReconciliationCurrency", + "ReconciliationIdentificationId", + "ReconciliationInstruction2", + "ReconciliationInstructionFlag", + "ReconciliationInstructionSub", + "ReconciliationMarginId", + "ReconciliationParty", + "ReconciliationPartyIndicator", + "ReconciliationPositionCount", + "ReconciliationPriority", + "ReconciliationPriority2", + "ReconciliationReconciliation1", + "ReconciliationReconciliationCount", + "ReconciliationReconciliationSub", + "ReconciliationReferenceCount", + "ReconciliationReferenceIndicator", + "ReconciliationRemittance", + "ReconciliationRemittanceId", + "ReconciliationRemittanceIndicator", + "ReconciliationRemittanceSub", + "ReconciliationSequenceCode", + "ReconciliationSequenceCount", + "ReconciliationStatusCount", + "ReconciliationStatusReport", + "ReconciliationTransactionDate", + "ReconciliationTransactionId", + "ReconciliationTransactionSub", + "ReconciliationTransactionType", + "ReconciliationTransmissionCount", + "ReconciliationTransmissionDate", + "ReconciliationValueDate2", + "ReconciliationValueDateType", + "ReconciliationVersion", + "ReconciliationVersionIndicator", + "ReconciliationVersionReport", + "ReferenceAccount", + "ReferenceAccount2", + "ReferenceAccountId", + "ReferenceAccountReport", + "ReferenceAmountCode", + "ReferenceAmountType", + "ReferenceAuthenticationSub", + "ReferenceBeneficiaryIndicator", + "ReferenceCategory2", + "ReferenceCategoryDate", + "ReferenceCategoryIndicator", + "ReferenceChannelReport", + "ReferenceClearing", + "ReferenceClearingId", + "ReferenceClearingType", + "ReferenceCollateral", + "ReferenceCollateral1", + "ReferenceCollateral2", + "ReferenceCurrency", + "ReferenceCurrencyCode", + "ReferenceIdentification", + "ReferenceIdentification1", + "ReferenceIdentificationCount", + "ReferenceIdentificationDate", + "ReferenceIdentificationReport", + "ReferenceInstructionDate", + "ReferenceInstructionFlag", + "ReferenceInstructionType", + "ReferenceMargin", + "ReferenceParty", + "ReferencePartySub", + "ReferencePositionId", + "ReferencePositionType", + "ReferencePriority", + "ReferencePriority1", + "ReferencePriorityReport", + "ReferenceReconciliation", + "ReferenceReconciliation1", + "ReferenceReconciliation2", + "ReferenceReferenceDate", + "ReferenceReferenceFlag", + "ReferenceRouting", + "ReferenceRoutingCount", + "ReferenceRoutingDate", + "ReferenceRoutingIndicator", + "ReferenceRoutingSub", + "ReferenceSequence", + "ReferenceSequenceId", + "ReferenceStatementDate", + "ReferenceTransactionDate", + "ReferenceTransactionSub", + "ReferenceTransmission", + "ReferenceTransmissionCode", + "ReferenceTransmissionCount", + "ReferenceValueDate2", + "ReferenceValueDateCode", + "ReferenceVersion1", + "ReferenceVersion2", + "RemittanceAccount", + "RemittanceAdjustmentDate", + "RemittanceAmountCode", + "RemittanceAmountDate", + "RemittanceAmountFlag", + "RemittanceAmountReport", + "RemittanceAuthentication", + "RemittanceAuthentication2", + "RemittanceAuthenticationCode", + "RemittanceAuthenticationDate", + "RemittanceBeneficiaryCount", + "RemittanceCategoryDate", + "RemittanceCategoryFlag", + "RemittanceChannel1", + "RemittanceClearing1", + "RemittanceClearingFlag", + "RemittanceClearingType", + "RemittanceCollateral", + "RemittanceCollateralCode", + "RemittanceCollateralSub", + "RemittanceCurrencyIndicator", + "RemittanceIdentificationReport", + "RemittanceIdentificationSub", + "RemittanceInstruction", + "RemittanceInstructionIndicator", + "RemittanceMarginId", + "RemittanceMarginSub", + "RemittancePositionId", + "RemittancePositionSub", + "RemittancePriorityReport", + "RemittanceReconciliation1", + "RemittanceReference", + "RemittanceReference1", + "RemittanceReferenceDate", + "RemittanceRemittance1", + "RemittanceRemittanceCount", + "RemittanceRemittanceDate", + "RemittanceRemittanceReport", + "RemittanceRemittanceType", + "RemittanceRouting", + "RemittanceRouting1", + "RemittanceRoutingSub", + "RemittanceSequenceReport", + "RemittanceSettlementFlag", + "RemittanceSettlementReport", + "RemittanceSettlementType", + "RemittanceStatementSub", + "RemittanceStatementType", + "RemittanceStatus2", + "RemittanceTransactionCount", + "RemittanceTransactionReport", + "RemittanceTransmission2", + "RemittanceTransmissionCode", + "RemittanceTransmissionFlag", + "RemittanceValueDateCount", + "RemittanceVersionDate", + "RemittanceVersionFlag", + "RemittanceVersionIndicator", + "RemittanceVersionSub", + "RoutingAccount", + "RoutingAccountIndicator", + "RoutingAdjustmentFlag", + "RoutingAdjustmentSub", + "RoutingAmountSub", + "RoutingAmountType", + "RoutingAuthentication", + "RoutingAuthenticationFlag", + "RoutingBeneficiaryDate", + "RoutingBeneficiaryIndicator", + "RoutingCategoryCount", + "RoutingCategorySub", + "RoutingChannel", + "RoutingChannel2", + "RoutingChannelId", + "RoutingClearing", + "RoutingClearingCode", + "RoutingCollateralIndicator", + "RoutingCurrency", + "RoutingCurrencyCount", + "RoutingCurrencyId", + "RoutingCurrencyReport", + "RoutingIdentificationSub", + "RoutingInstruction", + "RoutingInstructionId", + "RoutingInstructionIndicator", + "RoutingInstructionType", + "RoutingMarginType", + "RoutingParty2", + "RoutingPartyCode", + "RoutingPartyId", + "RoutingPartyIndicator", + "RoutingPartyReport", + "RoutingPartyType", + "RoutingPosition1", + "RoutingPositionFlag", + "RoutingPositionSub", + "RoutingPriorityId", + "RoutingReconciliation2", + "RoutingReconciliationCode", + "RoutingReconciliationId", + "RoutingReferenceReport", + "RoutingRouting", + "RoutingRoutingCode", + "RoutingSequenceReport", + "RoutingSequenceSub", + "RoutingSettlement1", + "RoutingStatement2", + "RoutingStatementCount", + "RoutingStatementDate", + "RoutingStatementId", + "RoutingTransaction1", + "RoutingTransactionDate", + "RoutingTransactionIndicator", + "RoutingTransmissionId", + "RoutingTransmissionSub", + "RoutingValueDate1", + "RoutingVersionDate", + "RoutingVersionReport", + "SequenceAccount2", + "SequenceAccountId", + "SequenceAdjustment", + "SequenceAmountCount", + "SequenceAmountReport", + "SequenceAuthentication2", + "SequenceAuthenticationDate", + "SequenceAuthenticationType", + "SequenceCategoryDate", + "SequenceCategorySub", + "SequenceChannelDate", + "SequenceClearingDate", + "SequenceClearingId", + "SequenceClearingIndicator", + "SequenceCollateralReport", + "SequenceCurrency", + "SequenceIdentificationDate", + "SequenceIdentificationId", + "SequenceInstructionSub", + "SequenceMargin", + "SequenceMarginCount", + "SequencePositionId", + "SequencePriorityCount", + "SequencePrioritySub", + "SequenceReconciliationDate", + "SequenceReference", + "SequenceReferenceIndicator", + "SequenceRemittanceCount", + "SequenceRemittanceFlag", + "SequenceRemittanceIndicator", + "SequenceRouting2", + "SequenceSettlement", + "SequenceSettlementIndicator", + "SequenceStatement", + "SequenceStatementType", + "SequenceStatusSub", + "SequenceTransactionFlag", + "SequenceTransactionSub", + "SequenceTransactionType", + "SequenceValueDateReport", + "SequenceValueDateSub", + "SequenceVersion2", + "SequenceVersionIndicator", + "SettlementAccount", + "SettlementAdjustment1", + "SettlementAdjustment2", + "SettlementAdjustmentFlag", + "SettlementAdjustmentReport", + "SettlementAmount1", + "SettlementAmount2", + "SettlementAmountIndicator", + "SettlementAuthentication2", + "SettlementAuthenticationCount", + "SettlementBeneficiaryFlag", + "SettlementBeneficiaryType", + "SettlementCategoryCode", + "SettlementChannel", + "SettlementChannelCode", + "SettlementClearingReport", + "SettlementCollateral2", + "SettlementCollateralCode", + "SettlementCollateralDate", + "SettlementCollateralSub", + "SettlementCurrency", + "SettlementCurrency2", + "SettlementIdentificationCount", + "SettlementIdentificationIndicator", + "SettlementInstruction", + "SettlementInstructionDate", + "SettlementMargin2", + "SettlementMarginId", + "SettlementMarginSub", + "SettlementPartyCode", + "SettlementPartyFlag", + "SettlementPartyType", + "SettlementPositionType", + "SettlementPriorityCount", + "SettlementPriorityFlag", + "SettlementPriorityIndicator", + "SettlementPriorityReport", + "SettlementReconciliation1", + "SettlementReconciliationSub", + "SettlementReferenceReport", + "SettlementRemittanceSub", + "SettlementSequence2", + "SettlementSequenceSub", + "SettlementSequenceType", + "SettlementSettlementId", + "SettlementSettlementReport", + "SettlementStatementCode", + "SettlementStatus1", + "SettlementTransaction", + "SettlementTransactionReport", + "SettlementTransmission", + "SettlementValueDate", + "SettlementValueDate2", + "SettlementValueDateFlag", + "SettlementValueDateSub", + "StatementAccount2", + "StatementAdjustmentDate", + "StatementAdjustmentIndicator", + "StatementAdjustmentReport", + "StatementAdjustmentSub", + "StatementAdjustmentType", + "StatementAmount", + "StatementAmount2", + "StatementAmountCode", + "StatementBeneficiaryCode", + "StatementBeneficiaryFlag", + "StatementBeneficiaryId", + "StatementBeneficiaryType", + "StatementCategory", + "StatementChannel1", + "StatementChannelCode", + "StatementChannelId", + "StatementCollateral", + "StatementCollateralSub", + "StatementCurrencyCode", + "StatementCurrencyType", + "StatementIdentification", + "StatementIdentificationReport", + "StatementInstructionReport", + "StatementMargin1", + "StatementMarginCount", + "StatementMarginReport", + "StatementMarginType", + "StatementParty", + "StatementPosition", + "StatementPositionDate", + "StatementReconciliationIndicator", + "StatementReference2", + "StatementReferenceCode", + "StatementReferenceSub", + "StatementRouting", + "StatementRoutingCode", + "StatementRoutingId", + "StatementRoutingIndicator", + "StatementRoutingType", + "StatementSequenceCode", + "StatementSequenceCount", + "StatementSequenceSub", + "StatementSettlementReport", + "StatementStatement2", + "StatementStatementId", + "StatementStatus2", + "StatementTransaction1", + "StatementTransactionIndicator", + "StatementTransactionSub", + "StatementTransmissionReport", + "StatementVersion1", + "StatementVersionCount", + "StatementVersionDate", + "StatementVersionType", + "StatusAccountId", + "StatusAccountIndicator", + "StatusAccountType", + "StatusAdjustmentCode", + "StatusAdjustmentDate", + "StatusAdjustmentFlag", + "StatusAmount", + "StatusAmount1", + "StatusAmountCode", + "StatusAmountDate", + "StatusAmountReport", + "StatusAuthenticationFlag", + "StatusAuthenticationReport", + "StatusBeneficiary", + "StatusCategory", + "StatusCategory2", + "StatusChannel", + "StatusChannel1", + "StatusChannelFlag", + "StatusChannelType", + "StatusClearing", + "StatusCollateral2", + "StatusCollateralCount", + "StatusCollateralDate", + "StatusCollateralFlag", + "StatusCollateralSub", + "StatusCurrencyDate", + "StatusCurrencySub", + "StatusCurrencyType", + "StatusIdentificationId", + "StatusInstructionCount", + "StatusInstructionIndicator", + "StatusMargin2", + "StatusMarginCode", + "StatusMarginId", + "StatusPartyReport", + "StatusPositionCount", + "StatusPositionFlag", + "StatusPositionIndicator", + "StatusPriority", + "StatusPriorityReport", + "StatusReconciliationDate", + "StatusReconciliationId", + "StatusReconciliationType", + "StatusRemittanceCount", + "StatusRemittanceDate", + "StatusRouting", + "StatusSequenceCode", + "StatusSettlementIndicator", + "StatusTransactionCode", + "StatusTransactionCount", + "StatusTransactionFlag", + "StatusTransactionReport", + "StatusTransmissionCount", + "StatusTransmissionType", + "StatusValueDate2", + "StatusValueDateCode", + "StatusValueDateCount", + "StatusVersion1", + "TransactionAccount", + "TransactionAccountCount", + "TransactionAdjustment1", + "TransactionAdjustmentCode", + "TransactionAdjustmentFlag", + "TransactionAmount", + "TransactionAmountCount", + "TransactionAmountIndicator", + "TransactionAuthenticationIndicator", + "TransactionBeneficiary2", + "TransactionBeneficiarySub", + "TransactionCategoryDate", + "TransactionCategoryFlag", + "TransactionChannelSub", + "TransactionChannelType", + "TransactionClearing", + "TransactionClearingReport", + "TransactionClearingSub", + "TransactionCollateral", + "TransactionCollateralFlag", + "TransactionCollateralId", + "TransactionIdentificationIndicator", + "TransactionInstruction", + "TransactionInstructionCode", + "TransactionInstructionSub", + "TransactionMargin2", + "TransactionMarginCount", + "TransactionPosition2", + "TransactionPositionIndicator", + "TransactionPriorityId", + "TransactionReconciliationCount", + "TransactionRemittanceFlag", + "TransactionRemittanceReport", + "TransactionRemittanceType", + "TransactionRoutingFlag", + "TransactionSequenceFlag", + "TransactionSequenceId", + "TransactionSequenceSub", + "TransactionSequenceType", + "TransactionSettlementDate", + "TransactionSettlementFlag", + "TransactionStatement", + "TransactionStatementIndicator", + "TransactionStatusType", + "TransactionTransactionDate", + "TransactionTransmissionCount", + "TransactionVersionCode", + "TransactionVersionType", + "TransmissionAdjustment1", + "TransmissionAdjustmentDate", + "TransmissionAdjustmentFlag", + "TransmissionAdjustmentType", + "TransmissionCategory", + "TransmissionCategoryFlag", + "TransmissionCategoryId", + "TransmissionChannel", + "TransmissionClearingId", + "TransmissionCollateralDate", + "TransmissionCurrencyIndicator", + "TransmissionCurrencyReport", + "TransmissionIdentification1", + "TransmissionInstruction", + "TransmissionInstruction2", + "TransmissionInstructionFlag", + "TransmissionInstructionType", + "TransmissionMargin2", + "TransmissionMarginDate", + "TransmissionPartyCode", + "TransmissionPartyDate", + "TransmissionPositionReport", + "TransmissionReconciliation2", + "TransmissionReconciliationSub", + "TransmissionReference1", + "TransmissionReferenceIndicator", + "TransmissionRemittanceDate", + "TransmissionRemittanceReport", + "TransmissionRoutingCount", + "TransmissionRoutingSub", + "TransmissionSequenceDate", + "TransmissionSettlementCount", + "TransmissionSettlementFlag", + "TransmissionStatement", + "TransmissionStatement2", + "TransmissionStatementCount", + "TransmissionStatusCode", + "TransmissionTransaction", + "TransmissionTransmissionDate", + "TransmissionValueDate", + "TransmissionValueDateId", + "TransmissionValueDateIndicator", + "ValueDateAccount2", + "ValueDateAccountCount", + "ValueDateAdjustment", + "ValueDateAdjustmentCount", + "ValueDateAdjustmentId", + "ValueDateAdjustmentReport", + "ValueDateAdjustmentType", + "ValueDateAmountCode", + "ValueDateAmountCount", + "ValueDateBeneficiary", + "ValueDateBeneficiarySub", + "ValueDateClearingDate", + "ValueDateCurrency", + "ValueDateIdentification2", + "ValueDateIdentificationFlag", + "ValueDateIdentificationIndicator", + "ValueDateIdentificationReport", + "ValueDateMargin", + "ValueDatePartyId", + "ValueDatePosition", + "ValueDatePositionType", + "ValueDatePriority", + "ValueDatePriority2", + "ValueDatePriorityCount", + "ValueDatePriorityReport", + "ValueDateReconciliationIndicator", + "ValueDateReferenceType", + "ValueDateRemittance2", + "ValueDateRemittanceReport", + "ValueDateRoutingReport", + "ValueDateSequence", + "ValueDateSequence1", + "ValueDateSequenceCount", + "ValueDateSequenceType", + "ValueDateSettlement2", + "ValueDateSettlementCount", + "ValueDateStatementCode", + "ValueDateStatementReport", + "ValueDateStatementSub", + "ValueDateStatusCode", + "ValueDateStatusType", + "ValueDateTransaction2", + "ValueDateTransactionCount", + "ValueDateTransactionId", + "ValueDateTransactionReport", + "ValueDateValueDateCode", + "ValueDateValueDateIndicator", + "ValueDateValueDateReport", + "ValueDateVersionFlag", + "ValueDateVersionSub", + "VersionAccount", + "VersionAccountCode", + "VersionAccountCount", + "VersionAccountIndicator", + "VersionAdjustmentIndicator", + "VersionAdjustmentSub", + "VersionAmount", + "VersionAmount2", + "VersionAmountCode", + "VersionAuthentication1", + "VersionAuthenticationCode", + "VersionBeneficiary1", + "VersionBeneficiary2", + "VersionBeneficiaryIndicator", + "VersionCategory2", + "VersionChannel", + "VersionChannelDate", + "VersionChannelFlag", + "VersionClearing", + "VersionClearingCode", + "VersionClearingCount", + "VersionClearingFlag", + "VersionClearingSub", + "VersionCollateralFlag", + "VersionCurrencyCode", + "VersionCurrencyFlag", + "VersionCurrencyIndicator", + "VersionIdentificationCount", + "VersionIdentificationDate", + "VersionIdentificationFlag", + "VersionIdentificationIndicator", + "VersionInstructionFlag", + "VersionMargin1", + "VersionMarginId", + "VersionPartyId", + "VersionPosition1", + "VersionPosition2", + "VersionPositionReport", + "VersionPriority2", + "VersionPriorityCode", + "VersionReconciliationCount", + "VersionReconciliationId", + "VersionReconciliationSub", + "VersionReference2", + "VersionReferenceCode", + "VersionRemittanceIndicator", + "VersionRoutingReport", + "VersionSequence2", + "VersionSequenceFlag", + "VersionSequenceType", + "VersionSettlement", + "VersionSettlementType", + "VersionStatement", + "VersionStatementFlag", + "VersionStatementId", + "VersionStatusIndicator", + "VersionTransactionCode", + "VersionTransactionCount", + "VersionTransmissionType", + "VersionValueDateCount", + "VersionValueDateFlag", +]; + +pub fn match_dispatch_1500(tag: &str) -> Option { + match tag { + "AccountAccount" => Some(0), + "AccountAccount1" => Some(1), + "AccountAdjustment2" => Some(2), + "AccountAmountDate" => Some(3), + "AccountAmountFlag" => Some(4), + "AccountAmountType" => Some(5), + "AccountAuthenticationType" => Some(6), + "AccountBeneficiaryCode" => Some(7), + "AccountBeneficiaryCount" => Some(8), + "AccountBeneficiaryFlag" => Some(9), + "AccountCategory" => Some(10), + "AccountCategoryDate" => Some(11), + "AccountCategoryFlag" => Some(12), + "AccountCategoryIndicator" => Some(13), + "AccountChannel" => Some(14), + "AccountChannelCount" => Some(15), + "AccountChannelId" => Some(16), + "AccountChannelIndicator" => Some(17), + "AccountClearing1" => Some(18), + "AccountCollateral2" => Some(19), + "AccountCollateralFlag" => Some(20), + "AccountCollateralReport" => Some(21), + "AccountCurrency1" => Some(22), + "AccountCurrencyFlag" => Some(23), + "AccountCurrencyId" => Some(24), + "AccountCurrencyIndicator" => Some(25), + "AccountCurrencySub" => Some(26), + "AccountIdentification" => Some(27), + "AccountIdentification1" => Some(28), + "AccountInstructionReport" => Some(29), + "AccountMarginFlag" => Some(30), + "AccountMarginType" => Some(31), + "AccountParty1" => Some(32), + "AccountPartyCount" => Some(33), + "AccountPartyFlag" => Some(34), + "AccountPartyIndicator" => Some(35), + "AccountPartyType" => Some(36), + "AccountPosition" => Some(37), + "AccountPriorityDate" => Some(38), + "AccountReconciliation" => Some(39), + "AccountReconciliationType" => Some(40), + "AccountReferenceId" => Some(41), + "AccountReferenceIndicator" => Some(42), + "AccountReferenceType" => Some(43), + "AccountRemittanceId" => Some(44), + "AccountRemittanceIndicator" => Some(45), + "AccountRoutingCount" => Some(46), + "AccountRoutingIndicator" => Some(47), + "AccountRoutingType" => Some(48), + "AccountSequence" => Some(49), + "AccountSequence1" => Some(50), + "AccountSequence2" => Some(51), + "AccountSettlementDate" => Some(52), + "AccountSettlementType" => Some(53), + "AccountStatement" => Some(54), + "AccountStatementCount" => Some(55), + "AccountStatusFlag" => Some(56), + "AccountStatusType" => Some(57), + "AccountTransaction" => Some(58), + "AccountTransactionCode" => Some(59), + "AccountTransactionSub" => Some(60), + "AccountTransactionType" => Some(61), + "AccountTransmission" => Some(62), + "AccountTransmission1" => Some(63), + "AccountVersion" => Some(64), + "AccountVersionSub" => Some(65), + "AccountVersionType" => Some(66), + "AdjustmentAccount" => Some(67), + "AdjustmentAccount1" => Some(68), + "AdjustmentAccountCode" => Some(69), + "AdjustmentAdjustment2" => Some(70), + "AdjustmentAdjustmentIndicator" => Some(71), + "AdjustmentAmount" => Some(72), + "AdjustmentAuthentication" => Some(73), + "AdjustmentAuthenticationFlag" => Some(74), + "AdjustmentBeneficiaryCount" => Some(75), + "AdjustmentBeneficiaryId" => Some(76), + "AdjustmentCategory1" => Some(77), + "AdjustmentChannelFlag" => Some(78), + "AdjustmentChannelType" => Some(79), + "AdjustmentClearingDate" => Some(80), + "AdjustmentClearingId" => Some(81), + "AdjustmentCollateralIndicator" => Some(82), + "AdjustmentCurrency" => Some(83), + "AdjustmentCurrencyId" => Some(84), + "AdjustmentCurrencyReport" => Some(85), + "AdjustmentIdentificationReport" => Some(86), + "AdjustmentInstructionDate" => Some(87), + "AdjustmentInstructionType" => Some(88), + "AdjustmentMarginCode" => Some(89), + "AdjustmentMarginReport" => Some(90), + "AdjustmentPosition2" => Some(91), + "AdjustmentPositionCount" => Some(92), + "AdjustmentPositionDate" => Some(93), + "AdjustmentReconciliation" => Some(94), + "AdjustmentReconciliationCount" => Some(95), + "AdjustmentReference1" => Some(96), + "AdjustmentRemittance" => Some(97), + "AdjustmentRemittance1" => Some(98), + "AdjustmentRemittanceFlag" => Some(99), + "AdjustmentRoutingIndicator" => Some(100), + "AdjustmentSequence" => Some(101), + "AdjustmentSequenceReport" => Some(102), + "AdjustmentSettlementFlag" => Some(103), + "AdjustmentStatement" => Some(104), + "AdjustmentStatementCode" => Some(105), + "AdjustmentStatementFlag" => Some(106), + "AdjustmentStatementId" => Some(107), + "AdjustmentStatementIndicator" => Some(108), + "AdjustmentStatusDate" => Some(109), + "AdjustmentStatusType" => Some(110), + "AdjustmentTransactionDate" => Some(111), + "AdjustmentTransmissionCode" => Some(112), + "AdjustmentTransmissionCount" => Some(113), + "AdjustmentTransmissionId" => Some(114), + "AdjustmentTransmissionSub" => Some(115), + "AdjustmentValueDateFlag" => Some(116), + "AdjustmentVersionType" => Some(117), + "AmountAccount1" => Some(118), + "AmountAccountCode" => Some(119), + "AmountAccountFlag" => Some(120), + "AmountAdjustment" => Some(121), + "AmountAdjustmentCode" => Some(122), + "AmountAdjustmentFlag" => Some(123), + "AmountAuthentication" => Some(124), + "AmountAuthenticationId" => Some(125), + "AmountAuthenticationIndicator" => Some(126), + "AmountChannel" => Some(127), + "AmountChannelCode" => Some(128), + "AmountChannelDate" => Some(129), + "AmountCollateralFlag" => Some(130), + "AmountCurrency" => Some(131), + "AmountCurrencyCount" => Some(132), + "AmountIdentificationIndicator" => Some(133), + "AmountIdentificationSub" => Some(134), + "AmountInstructionFlag" => Some(135), + "AmountInstructionReport" => Some(136), + "AmountPartyId" => Some(137), + "AmountPartyIndicator" => Some(138), + "AmountPriorityReport" => Some(139), + "AmountReconciliationIndicator" => Some(140), + "AmountReconciliationSub" => Some(141), + "AmountReference" => Some(142), + "AmountReferenceIndicator" => Some(143), + "AmountRemittance" => Some(144), + "AmountRemittance1" => Some(145), + "AmountRemittance2" => Some(146), + "AmountRemittanceFlag" => Some(147), + "AmountRemittanceReport" => Some(148), + "AmountSequenceDate" => Some(149), + "AmountSequenceReport" => Some(150), + "AmountSettlementDate" => Some(151), + "AmountStatementIndicator" => Some(152), + "AmountStatusFlag" => Some(153), + "AmountStatusIndicator" => Some(154), + "AmountTransaction" => Some(155), + "AmountTransactionCount" => Some(156), + "AmountTransmission2" => Some(157), + "AmountTransmissionCode" => Some(158), + "AmountTransmissionIndicator" => Some(159), + "AmountValueDate" => Some(160), + "AmountValueDate1" => Some(161), + "AmountVersionDate" => Some(162), + "AuthenticationAccountDate" => Some(163), + "AuthenticationAccountId" => Some(164), + "AuthenticationAccountType" => Some(165), + "AuthenticationAdjustmentType" => Some(166), + "AuthenticationAmountFlag" => Some(167), + "AuthenticationAuthentication1" => Some(168), + "AuthenticationBeneficiaryCode" => Some(169), + "AuthenticationBeneficiaryDate" => Some(170), + "AuthenticationBeneficiaryId" => Some(171), + "AuthenticationBeneficiaryIndicator" => Some(172), + "AuthenticationBeneficiarySub" => Some(173), + "AuthenticationCategory1" => Some(174), + "AuthenticationCategoryDate" => Some(175), + "AuthenticationCategoryFlag" => Some(176), + "AuthenticationChannel1" => Some(177), + "AuthenticationChannel2" => Some(178), + "AuthenticationChannelFlag" => Some(179), + "AuthenticationClearing" => Some(180), + "AuthenticationClearingIndicator" => Some(181), + "AuthenticationClearingReport" => Some(182), + "AuthenticationCollateralType" => Some(183), + "AuthenticationCurrencyIndicator" => Some(184), + "AuthenticationCurrencyType" => Some(185), + "AuthenticationIdentificationFlag" => Some(186), + "AuthenticationIdentificationReport" => Some(187), + "AuthenticationInstruction" => Some(188), + "AuthenticationInstructionId" => Some(189), + "AuthenticationMarginCount" => Some(190), + "AuthenticationPartyCode" => Some(191), + "AuthenticationPosition1" => Some(192), + "AuthenticationPositionFlag" => Some(193), + "AuthenticationPositionReport" => Some(194), + "AuthenticationPriorityCode" => Some(195), + "AuthenticationPriorityIndicator" => Some(196), + "AuthenticationReconciliationIndicator" => Some(197), + "AuthenticationReference2" => Some(198), + "AuthenticationReferenceFlag" => Some(199), + "AuthenticationReferenceId" => Some(200), + "AuthenticationRemittance1" => Some(201), + "AuthenticationRemittanceFlag" => Some(202), + "AuthenticationRemittanceIndicator" => Some(203), + "AuthenticationRemittanceType" => Some(204), + "AuthenticationRouting" => Some(205), + "AuthenticationRouting1" => Some(206), + "AuthenticationSequenceCode" => Some(207), + "AuthenticationSequenceId" => Some(208), + "AuthenticationSequenceIndicator" => Some(209), + "AuthenticationSequenceSub" => Some(210), + "AuthenticationSettlement" => Some(211), + "AuthenticationSettlement1" => Some(212), + "AuthenticationSettlementId" => Some(213), + "AuthenticationSettlementSub" => Some(214), + "AuthenticationStatementCount" => Some(215), + "AuthenticationStatementId" => Some(216), + "AuthenticationStatusId" => Some(217), + "AuthenticationTransaction1" => Some(218), + "AuthenticationTransactionDate" => Some(219), + "AuthenticationTransactionSub" => Some(220), + "AuthenticationTransactionType" => Some(221), + "AuthenticationTransmissionCode" => Some(222), + "AuthenticationTransmissionIndicator" => Some(223), + "AuthenticationTransmissionReport" => Some(224), + "AuthenticationValueDate2" => Some(225), + "AuthenticationVersion1" => Some(226), + "BeneficiaryAccount1" => Some(227), + "BeneficiaryAccountReport" => Some(228), + "BeneficiaryAccountType" => Some(229), + "BeneficiaryAdjustmentCount" => Some(230), + "BeneficiaryAdjustmentDate" => Some(231), + "BeneficiaryAdjustmentType" => Some(232), + "BeneficiaryAmountType" => Some(233), + "BeneficiaryAuthenticationCode" => Some(234), + "BeneficiaryAuthenticationSub" => Some(235), + "BeneficiaryAuthenticationType" => Some(236), + "BeneficiaryBeneficiary" => Some(237), + "BeneficiaryBeneficiary2" => Some(238), + "BeneficiaryBeneficiaryCode" => Some(239), + "BeneficiaryBeneficiaryCount" => Some(240), + "BeneficiaryBeneficiaryIndicator" => Some(241), + "BeneficiaryBeneficiaryReport" => Some(242), + "BeneficiaryCategory" => Some(243), + "BeneficiaryCategoryCode" => Some(244), + "BeneficiaryChannel2" => Some(245), + "BeneficiaryChannelCount" => Some(246), + "BeneficiaryChannelId" => Some(247), + "BeneficiaryCollateralCount" => Some(248), + "BeneficiaryCollateralFlag" => Some(249), + "BeneficiaryCollateralIndicator" => Some(250), + "BeneficiaryCurrencyType" => Some(251), + "BeneficiaryIdentification1" => Some(252), + "BeneficiaryIdentification2" => Some(253), + "BeneficiaryIdentificationReport" => Some(254), + "BeneficiaryInstructionReport" => Some(255), + "BeneficiaryInstructionSub" => Some(256), + "BeneficiaryMarginId" => Some(257), + "BeneficiaryPartyCode" => Some(258), + "BeneficiaryPartyFlag" => Some(259), + "BeneficiaryPartyIndicator" => Some(260), + "BeneficiaryPosition" => Some(261), + "BeneficiaryPriorityType" => Some(262), + "BeneficiaryReference1" => Some(263), + "BeneficiaryReferenceDate" => Some(264), + "BeneficiaryReferenceFlag" => Some(265), + "BeneficiaryReferenceReport" => Some(266), + "BeneficiaryRemittance" => Some(267), + "BeneficiaryRemittance2" => Some(268), + "BeneficiaryRemittanceCount" => Some(269), + "BeneficiaryRemittanceSub" => Some(270), + "BeneficiaryRoutingIndicator" => Some(271), + "BeneficiarySequenceId" => Some(272), + "BeneficiarySettlementIndicator" => Some(273), + "BeneficiaryStatus2" => Some(274), + "BeneficiaryStatusCount" => Some(275), + "BeneficiaryStatusDate" => Some(276), + "BeneficiaryTransactionSub" => Some(277), + "BeneficiaryTransmission" => Some(278), + "BeneficiaryTransmission1" => Some(279), + "BeneficiaryTransmission2" => Some(280), + "BeneficiaryTransmissionFlag" => Some(281), + "BeneficiaryTransmissionIndicator" => Some(282), + "BeneficiaryValueDate" => Some(283), + "BeneficiaryValueDate1" => Some(284), + "BeneficiaryVersionCount" => Some(285), + "BeneficiaryVersionFlag" => Some(286), + "CategoryAccount1" => Some(287), + "CategoryAccountDate" => Some(288), + "CategoryAccountFlag" => Some(289), + "CategoryAccountType" => Some(290), + "CategoryAdjustment1" => Some(291), + "CategoryAdjustmentDate" => Some(292), + "CategoryAmount" => Some(293), + "CategoryAmountIndicator" => Some(294), + "CategoryAuthentication1" => Some(295), + "CategoryAuthentication2" => Some(296), + "CategoryAuthenticationIndicator" => Some(297), + "CategoryAuthenticationType" => Some(298), + "CategoryBeneficiaryCode" => Some(299), + "CategoryBeneficiaryIndicator" => Some(300), + "CategoryChannel" => Some(301), + "CategoryChannelIndicator" => Some(302), + "CategoryClearingFlag" => Some(303), + "CategoryClearingType" => Some(304), + "CategoryCollateral1" => Some(305), + "CategoryCollateralFlag" => Some(306), + "CategoryCollateralType" => Some(307), + "CategoryCurrency" => Some(308), + "CategoryCurrency1" => Some(309), + "CategoryCurrencyReport" => Some(310), + "CategoryCurrencySub" => Some(311), + "CategoryCurrencyType" => Some(312), + "CategoryIdentification1" => Some(313), + "CategoryIdentificationSub" => Some(314), + "CategoryIdentificationType" => Some(315), + "CategoryInstruction" => Some(316), + "CategoryInstructionCount" => Some(317), + "CategoryInstructionIndicator" => Some(318), + "CategoryMargin" => Some(319), + "CategoryPartySub" => Some(320), + "CategoryPositionId" => Some(321), + "CategoryPositionIndicator" => Some(322), + "CategoryPositionReport" => Some(323), + "CategoryPriority1" => Some(324), + "CategoryPriorityId" => Some(325), + "CategoryReconciliation1" => Some(326), + "CategoryReconciliation2" => Some(327), + "CategoryReconciliationId" => Some(328), + "CategoryReconciliationSub" => Some(329), + "CategoryReferenceDate" => Some(330), + "CategoryRemittance1" => Some(331), + "CategoryRemittanceDate" => Some(332), + "CategoryRemittanceFlag" => Some(333), + "CategoryRemittanceReport" => Some(334), + "CategoryRouting" => Some(335), + "CategoryRoutingIndicator" => Some(336), + "CategorySequenceCount" => Some(337), + "CategorySequenceIndicator" => Some(338), + "CategorySettlement" => Some(339), + "CategoryStatusCount" => Some(340), + "CategoryStatusReport" => Some(341), + "CategoryTransaction" => Some(342), + "CategoryTransactionDate" => Some(343), + "CategoryTransmission" => Some(344), + "CategoryTransmissionCode" => Some(345), + "CategoryValueDateIndicator" => Some(346), + "CategoryVersion" => Some(347), + "CategoryVersion1" => Some(348), + "ChannelAccount2" => Some(349), + "ChannelAccountCode" => Some(350), + "ChannelAccountFlag" => Some(351), + "ChannelAdjustment" => Some(352), + "ChannelAdjustmentIndicator" => Some(353), + "ChannelAmount" => Some(354), + "ChannelAmountIndicator" => Some(355), + "ChannelAmountReport" => Some(356), + "ChannelAuthentication" => Some(357), + "ChannelAuthenticationReport" => Some(358), + "ChannelAuthenticationType" => Some(359), + "ChannelBeneficiary1" => Some(360), + "ChannelBeneficiaryDate" => Some(361), + "ChannelCategoryId" => Some(362), + "ChannelCategoryType" => Some(363), + "ChannelChannelCode" => Some(364), + "ChannelClearingDate" => Some(365), + "ChannelCollateralCount" => Some(366), + "ChannelCollateralReport" => Some(367), + "ChannelCollateralSub" => Some(368), + "ChannelIdentification" => Some(369), + "ChannelIdentificationFlag" => Some(370), + "ChannelIdentificationId" => Some(371), + "ChannelInstructionFlag" => Some(372), + "ChannelMargin2" => Some(373), + "ChannelMarginCount" => Some(374), + "ChannelMarginDate" => Some(375), + "ChannelMarginIndicator" => Some(376), + "ChannelParty" => Some(377), + "ChannelParty2" => Some(378), + "ChannelPosition" => Some(379), + "ChannelPositionId" => Some(380), + "ChannelPositionIndicator" => Some(381), + "ChannelPriority" => Some(382), + "ChannelPriority2" => Some(383), + "ChannelPriorityCount" => Some(384), + "ChannelReconciliation" => Some(385), + "ChannelReference" => Some(386), + "ChannelReferenceCode" => Some(387), + "ChannelReferenceFlag" => Some(388), + "ChannelRemittance" => Some(389), + "ChannelRemittanceFlag" => Some(390), + "ChannelRemittanceId" => Some(391), + "ChannelRoutingId" => Some(392), + "ChannelRoutingReport" => Some(393), + "ChannelRoutingSub" => Some(394), + "ChannelSettlement" => Some(395), + "ChannelSettlementSub" => Some(396), + "ChannelStatement1" => Some(397), + "ChannelStatementId" => Some(398), + "ChannelStatus1" => Some(399), + "ChannelTransactionCount" => Some(400), + "ChannelTransactionFlag" => Some(401), + "ChannelTransmissionIndicator" => Some(402), + "ChannelValueDate1" => Some(403), + "ChannelValueDateId" => Some(404), + "ChannelValueDateSub" => Some(405), + "ChannelValueDateType" => Some(406), + "ClearingAccount" => Some(407), + "ClearingAccountFlag" => Some(408), + "ClearingAdjustment" => Some(409), + "ClearingAdjustmentDate" => Some(410), + "ClearingAmountCount" => Some(411), + "ClearingAuthenticationDate" => Some(412), + "ClearingBeneficiaryId" => Some(413), + "ClearingBeneficiaryReport" => Some(414), + "ClearingClearingDate" => Some(415), + "ClearingClearingId" => Some(416), + "ClearingClearingReport" => Some(417), + "ClearingCollateral" => Some(418), + "ClearingCollateral2" => Some(419), + "ClearingCollateralCount" => Some(420), + "ClearingIdentificationType" => Some(421), + "ClearingInstructionCount" => Some(422), + "ClearingInstructionIndicator" => Some(423), + "ClearingMarginFlag" => Some(424), + "ClearingParty" => Some(425), + "ClearingPartyCount" => Some(426), + "ClearingPartyId" => Some(427), + "ClearingPartyIndicator" => Some(428), + "ClearingPosition" => Some(429), + "ClearingReconciliation" => Some(430), + "ClearingReconciliationSub" => Some(431), + "ClearingReferenceCode" => Some(432), + "ClearingReferenceSub" => Some(433), + "ClearingRemittanceReport" => Some(434), + "ClearingRouting" => Some(435), + "ClearingRoutingCount" => Some(436), + "ClearingSequenceCount" => Some(437), + "ClearingSequenceId" => Some(438), + "ClearingSettlement" => Some(439), + "ClearingStatementType" => Some(440), + "ClearingStatus2" => Some(441), + "ClearingStatusFlag" => Some(442), + "ClearingStatusId" => Some(443), + "ClearingTransactionIndicator" => Some(444), + "ClearingTransmissionCount" => Some(445), + "ClearingTransmissionReport" => Some(446), + "ClearingTransmissionType" => Some(447), + "ClearingValueDateCode" => Some(448), + "ClearingValueDateDate" => Some(449), + "CollateralAccount2" => Some(450), + "CollateralAccountCount" => Some(451), + "CollateralAdjustment2" => Some(452), + "CollateralAdjustmentDate" => Some(453), + "CollateralAdjustmentSub" => Some(454), + "CollateralAmountType" => Some(455), + "CollateralAuthentication2" => Some(456), + "CollateralAuthenticationCode" => Some(457), + "CollateralBeneficiary" => Some(458), + "CollateralBeneficiaryCode" => Some(459), + "CollateralBeneficiaryIndicator" => Some(460), + "CollateralCategory" => Some(461), + "CollateralCategory2" => Some(462), + "CollateralCategoryType" => Some(463), + "CollateralChannelCode" => Some(464), + "CollateralChannelCount" => Some(465), + "CollateralChannelDate" => Some(466), + "CollateralChannelReport" => Some(467), + "CollateralChannelSub" => Some(468), + "CollateralClearingDate" => Some(469), + "CollateralCollateralCode" => Some(470), + "CollateralCollateralCount" => Some(471), + "CollateralCollateralFlag" => Some(472), + "CollateralIdentification" => Some(473), + "CollateralInstructionCode" => Some(474), + "CollateralInstructionFlag" => Some(475), + "CollateralInstructionSub" => Some(476), + "CollateralMarginId" => Some(477), + "CollateralParty" => Some(478), + "CollateralPartyReport" => Some(479), + "CollateralPositionCount" => Some(480), + "CollateralPositionSub" => Some(481), + "CollateralPositionType" => Some(482), + "CollateralPriority2" => Some(483), + "CollateralPriorityDate" => Some(484), + "CollateralPriorityReport" => Some(485), + "CollateralReconciliation" => Some(486), + "CollateralReconciliation2" => Some(487), + "CollateralReconciliationCount" => Some(488), + "CollateralReconciliationDate" => Some(489), + "CollateralReference" => Some(490), + "CollateralReference2" => Some(491), + "CollateralReferenceType" => Some(492), + "CollateralRoutingReport" => Some(493), + "CollateralSequence2" => Some(494), + "CollateralSequenceCount" => Some(495), + "CollateralSettlementCode" => Some(496), + "CollateralSettlementDate" => Some(497), + "CollateralStatement" => Some(498), + "CollateralStatementReport" => Some(499), + "CollateralStatus" => Some(500), + "CollateralStatus2" => Some(501), + "CollateralStatusIndicator" => Some(502), + "CollateralTransaction2" => Some(503), + "CollateralTransmission2" => Some(504), + "CollateralValueDate2" => Some(505), + "CollateralValueDateCode" => Some(506), + "CollateralVersionIndicator" => Some(507), + "CollateralVersionType" => Some(508), + "CurrencyAccountId" => Some(509), + "CurrencyAccountSub" => Some(510), + "CurrencyAmountIndicator" => Some(511), + "CurrencyAuthentication" => Some(512), + "CurrencyBeneficiaryIndicator" => Some(513), + "CurrencyCategoryDate" => Some(514), + "CurrencyChannel2" => Some(515), + "CurrencyChannelCount" => Some(516), + "CurrencyChannelId" => Some(517), + "CurrencyClearing" => Some(518), + "CurrencyCollateralCount" => Some(519), + "CurrencyCollateralType" => Some(520), + "CurrencyCurrency" => Some(521), + "CurrencyCurrency1" => Some(522), + "CurrencyInstruction1" => Some(523), + "CurrencyInstructionSub" => Some(524), + "CurrencyMarginDate" => Some(525), + "CurrencyMarginId" => Some(526), + "CurrencyPartyCount" => Some(527), + "CurrencyPartyId" => Some(528), + "CurrencyPartySub" => Some(529), + "CurrencyPosition" => Some(530), + "CurrencyPositionCount" => Some(531), + "CurrencyPositionSub" => Some(532), + "CurrencyPriority" => Some(533), + "CurrencyReference2" => Some(534), + "CurrencyReferenceIndicator" => Some(535), + "CurrencyRouting" => Some(536), + "CurrencyRouting2" => Some(537), + "CurrencyRoutingId" => Some(538), + "CurrencySequenceCode" => Some(539), + "CurrencySequenceId" => Some(540), + "CurrencySequenceReport" => Some(541), + "CurrencySettlementType" => Some(542), + "CurrencyStatementSub" => Some(543), + "CurrencyStatusCode" => Some(544), + "CurrencyStatusType" => Some(545), + "CurrencyTransactionIndicator" => Some(546), + "CurrencyTransmissionDate" => Some(547), + "CurrencyValueDateIndicator" => Some(548), + "CurrencyVersionDate" => Some(549), + "IdentificationAccountIndicator" => Some(550), + "IdentificationAdjustmentCount" => Some(551), + "IdentificationAdjustmentIndicator" => Some(552), + "IdentificationAdjustmentReport" => Some(553), + "IdentificationAdjustmentType" => Some(554), + "IdentificationAmount" => Some(555), + "IdentificationAmountCount" => Some(556), + "IdentificationAmountId" => Some(557), + "IdentificationAmountSub" => Some(558), + "IdentificationAuthenticationId" => Some(559), + "IdentificationAuthenticationIndicator" => Some(560), + "IdentificationCategoryId" => Some(561), + "IdentificationClearing" => Some(562), + "IdentificationCollateral2" => Some(563), + "IdentificationCurrencyIndicator" => Some(564), + "IdentificationCurrencyType" => Some(565), + "IdentificationIdentificationCount" => Some(566), + "IdentificationIdentificationDate" => Some(567), + "IdentificationIdentificationSub" => Some(568), + "IdentificationInstructionDate" => Some(569), + "IdentificationInstructionId" => Some(570), + "IdentificationInstructionIndicator" => Some(571), + "IdentificationInstructionType" => Some(572), + "IdentificationMarginIndicator" => Some(573), + "IdentificationPartyFlag" => Some(574), + "IdentificationPartyType" => Some(575), + "IdentificationPositionType" => Some(576), + "IdentificationPriorityDate" => Some(577), + "IdentificationReconciliationCount" => Some(578), + "IdentificationReference1" => Some(579), + "IdentificationReferenceCode" => Some(580), + "IdentificationRouting" => Some(581), + "IdentificationSequence" => Some(582), + "IdentificationSequenceCode" => Some(583), + "IdentificationSequenceIndicator" => Some(584), + "IdentificationSettlement1" => Some(585), + "IdentificationSettlementId" => Some(586), + "IdentificationSettlementSub" => Some(587), + "IdentificationStatementFlag" => Some(588), + "IdentificationStatementIndicator" => Some(589), + "IdentificationStatementReport" => Some(590), + "IdentificationStatusCode" => Some(591), + "IdentificationStatusReport" => Some(592), + "IdentificationTransaction" => Some(593), + "IdentificationTransaction1" => Some(594), + "IdentificationTransactionType" => Some(595), + "IdentificationTransmission1" => Some(596), + "IdentificationValueDate" => Some(597), + "IdentificationValueDateCount" => Some(598), + "IdentificationValueDateDate" => Some(599), + "IdentificationValueDateId" => Some(600), + "IdentificationValueDateIndicator" => Some(601), + "IdentificationValueDateType" => Some(602), + "IdentificationVersion1" => Some(603), + "IdentificationVersionReport" => Some(604), + "InstructionAccount" => Some(605), + "InstructionAccountCode" => Some(606), + "InstructionAdjustment" => Some(607), + "InstructionAdjustmentId" => Some(608), + "InstructionAuthentication2" => Some(609), + "InstructionAuthenticationIndicator" => Some(610), + "InstructionBeneficiary" => Some(611), + "InstructionBeneficiaryDate" => Some(612), + "InstructionBeneficiaryReport" => Some(613), + "InstructionBeneficiaryType" => Some(614), + "InstructionCategoryCode" => Some(615), + "InstructionChannelIndicator" => Some(616), + "InstructionCurrency1" => Some(617), + "InstructionCurrencyCount" => Some(618), + "InstructionCurrencyId" => Some(619), + "InstructionIdentification" => Some(620), + "InstructionIdentificationType" => Some(621), + "InstructionInstruction2" => Some(622), + "InstructionInstructionIndicator" => Some(623), + "InstructionInstructionSub" => Some(624), + "InstructionMarginCode" => Some(625), + "InstructionParty" => Some(626), + "InstructionPositionCode" => Some(627), + "InstructionPositionType" => Some(628), + "InstructionPriority" => Some(629), + "InstructionPriorityFlag" => Some(630), + "InstructionPriorityId" => Some(631), + "InstructionReconciliation2" => Some(632), + "InstructionReconciliationType" => Some(633), + "InstructionReferenceIndicator" => Some(634), + "InstructionRemittanceCount" => Some(635), + "InstructionRemittanceId" => Some(636), + "InstructionSequenceFlag" => Some(637), + "InstructionSettlementType" => Some(638), + "InstructionStatusCode" => Some(639), + "InstructionStatusType" => Some(640), + "InstructionTransactionSub" => Some(641), + "InstructionTransmissionCode" => Some(642), + "InstructionTransmissionType" => Some(643), + "InstructionValueDate" => Some(644), + "InstructionValueDate1" => Some(645), + "InstructionValueDateIndicator" => Some(646), + "InstructionVersion2" => Some(647), + "MarginAccountIndicator" => Some(648), + "MarginAdjustmentDate" => Some(649), + "MarginAmount2" => Some(650), + "MarginAmountReport" => Some(651), + "MarginAuthentication2" => Some(652), + "MarginBeneficiary" => Some(653), + "MarginBeneficiaryReport" => Some(654), + "MarginBeneficiaryType" => Some(655), + "MarginCategory2" => Some(656), + "MarginChannel2" => Some(657), + "MarginClearing" => Some(658), + "MarginClearingCount" => Some(659), + "MarginCollateral1" => Some(660), + "MarginCurrencyCode" => Some(661), + "MarginIdentification" => Some(662), + "MarginIdentificationType" => Some(663), + "MarginInstruction1" => Some(664), + "MarginInstructionIndicator" => Some(665), + "MarginMarginDate" => Some(666), + "MarginMarginId" => Some(667), + "MarginPartyIndicator" => Some(668), + "MarginPositionCode" => Some(669), + "MarginPriorityDate" => Some(670), + "MarginPriorityFlag" => Some(671), + "MarginPrioritySub" => Some(672), + "MarginReconciliation" => Some(673), + "MarginReconciliation1" => Some(674), + "MarginReconciliationFlag" => Some(675), + "MarginReconciliationId" => Some(676), + "MarginReference1" => Some(677), + "MarginReferenceFlag" => Some(678), + "MarginReferenceId" => Some(679), + "MarginRemittance" => Some(680), + "MarginRemittanceType" => Some(681), + "MarginRouting" => Some(682), + "MarginRoutingType" => Some(683), + "MarginSequenceFlag" => Some(684), + "MarginSequenceId" => Some(685), + "MarginStatement" => Some(686), + "MarginStatementCount" => Some(687), + "MarginStatementReport" => Some(688), + "MarginStatusId" => Some(689), + "MarginTransactionCount" => Some(690), + "MarginTransactionFlag" => Some(691), + "MarginTransmissionId" => Some(692), + "MarginTransmissionSub" => Some(693), + "MarginValueDate2" => Some(694), + "PartyAccountReport" => Some(695), + "PartyAccountSub" => Some(696), + "PartyAccountType" => Some(697), + "PartyAdjustmentCode" => Some(698), + "PartyAdjustmentIndicator" => Some(699), + "PartyAdjustmentReport" => Some(700), + "PartyAmount1" => Some(701), + "PartyAmountDate" => Some(702), + "PartyAmountFlag" => Some(703), + "PartyAuthentication1" => Some(704), + "PartyCategoryCount" => Some(705), + "PartyChannel" => Some(706), + "PartyChannel2" => Some(707), + "PartyChannelSub" => Some(708), + "PartyCollateralReport" => Some(709), + "PartyCollateralSub" => Some(710), + "PartyCurrency" => Some(711), + "PartyCurrencyCode" => Some(712), + "PartyCurrencyType" => Some(713), + "PartyIdentificationIndicator" => Some(714), + "PartyInstructionDate" => Some(715), + "PartyInstructionId" => Some(716), + "PartyInstructionReport" => Some(717), + "PartyMarginType" => Some(718), + "PartyPosition2" => Some(719), + "PartyPositionId" => Some(720), + "PartyPositionType" => Some(721), + "PartyPriority" => Some(722), + "PartyPriority1" => Some(723), + "PartyPriorityCount" => Some(724), + "PartyReconciliationCode" => Some(725), + "PartyReconciliationFlag" => Some(726), + "PartyReconciliationId" => Some(727), + "PartyReferenceIndicator" => Some(728), + "PartyReferenceSub" => Some(729), + "PartyRemittanceType" => Some(730), + "PartyRouting1" => Some(731), + "PartyRouting2" => Some(732), + "PartyRoutingId" => Some(733), + "PartySequenceIndicator" => Some(734), + "PartySequenceReport" => Some(735), + "PartySettlement1" => Some(736), + "PartySettlementCount" => Some(737), + "PartyStatement" => Some(738), + "PartyStatementId" => Some(739), + "PartyStatementIndicator" => Some(740), + "PartyStatusId" => Some(741), + "PartyTransactionIndicator" => Some(742), + "PartyValueDateCount" => Some(743), + "PartyValueDateType" => Some(744), + "PartyVersion" => Some(745), + "PositionAccount" => Some(746), + "PositionAccount1" => Some(747), + "PositionAdjustmentCode" => Some(748), + "PositionAdjustmentDate" => Some(749), + "PositionAmount" => Some(750), + "PositionAmount1" => Some(751), + "PositionAmountSub" => Some(752), + "PositionAuthenticationFlag" => Some(753), + "PositionBeneficiaryFlag" => Some(754), + "PositionBeneficiaryReport" => Some(755), + "PositionCategorySub" => Some(756), + "PositionChannelFlag" => Some(757), + "PositionClearingReport" => Some(758), + "PositionClearingType" => Some(759), + "PositionCollateralCode" => Some(760), + "PositionCurrency2" => Some(761), + "PositionCurrencyReport" => Some(762), + "PositionCurrencySub" => Some(763), + "PositionIdentification1" => Some(764), + "PositionIdentificationCode" => Some(765), + "PositionInstruction" => Some(766), + "PositionInstructionReport" => Some(767), + "PositionMarginCode" => Some(768), + "PositionMarginCount" => Some(769), + "PositionMarginFlag" => Some(770), + "PositionMarginType" => Some(771), + "PositionPartyFlag" => Some(772), + "PositionPartyIndicator" => Some(773), + "PositionPositionReport" => Some(774), + "PositionPriorityFlag" => Some(775), + "PositionPriorityIndicator" => Some(776), + "PositionReconciliation1" => Some(777), + "PositionReconciliationReport" => Some(778), + "PositionReferenceSub" => Some(779), + "PositionRemittanceIndicator" => Some(780), + "PositionRoutingCount" => Some(781), + "PositionRoutingIndicator" => Some(782), + "PositionSequence" => Some(783), + "PositionSequenceCode" => Some(784), + "PositionSequenceDate" => Some(785), + "PositionSequenceIndicator" => Some(786), + "PositionStatement1" => Some(787), + "PositionStatementCount" => Some(788), + "PositionStatusSub" => Some(789), + "PositionTransmission" => Some(790), + "PositionTransmissionDate" => Some(791), + "PositionTransmissionType" => Some(792), + "PositionValueDate1" => Some(793), + "PositionValueDateFlag" => Some(794), + "PositionValueDateIndicator" => Some(795), + "PositionValueDateReport" => Some(796), + "PositionValueDateSub" => Some(797), + "PositionValueDateType" => Some(798), + "PriorityAccount1" => Some(799), + "PriorityAccountDate" => Some(800), + "PriorityAccountIndicator" => Some(801), + "PriorityAdjustment" => Some(802), + "PriorityAdjustment1" => Some(803), + "PriorityAdjustment2" => Some(804), + "PriorityAdjustmentCode" => Some(805), + "PriorityAdjustmentFlag" => Some(806), + "PriorityAdjustmentReport" => Some(807), + "PriorityAmount" => Some(808), + "PriorityAmountSub" => Some(809), + "PriorityAuthentication" => Some(810), + "PriorityAuthenticationDate" => Some(811), + "PriorityBeneficiaryCount" => Some(812), + "PriorityBeneficiaryIndicator" => Some(813), + "PriorityBeneficiaryType" => Some(814), + "PriorityChannelCount" => Some(815), + "PriorityChannelReport" => Some(816), + "PriorityChannelType" => Some(817), + "PriorityClearingType" => Some(818), + "PriorityCurrency" => Some(819), + "PriorityCurrencyId" => Some(820), + "PriorityCurrencyType" => Some(821), + "PriorityIdentificationDate" => Some(822), + "PriorityIdentificationId" => Some(823), + "PriorityInstruction2" => Some(824), + "PriorityInstructionCode" => Some(825), + "PriorityInstructionFlag" => Some(826), + "PriorityMargin2" => Some(827), + "PriorityMarginCount" => Some(828), + "PriorityParty" => Some(829), + "PriorityParty1" => Some(830), + "PriorityPartyDate" => Some(831), + "PriorityPartyFlag" => Some(832), + "PriorityPosition" => Some(833), + "PriorityPositionId" => Some(834), + "PriorityPositionType" => Some(835), + "PriorityPriority" => Some(836), + "PriorityReconciliation2" => Some(837), + "PriorityReconciliationCode" => Some(838), + "PriorityReconciliationFlag" => Some(839), + "PriorityReconciliationReport" => Some(840), + "PriorityReference1" => Some(841), + "PriorityReferenceCount" => Some(842), + "PriorityReferenceDate" => Some(843), + "PriorityReferenceId" => Some(844), + "PriorityRemittanceCode" => Some(845), + "PriorityRemittanceCount" => Some(846), + "PriorityRemittanceFlag" => Some(847), + "PriorityRoutingFlag" => Some(848), + "PrioritySequenceCount" => Some(849), + "PrioritySettlement" => Some(850), + "PriorityStatementCount" => Some(851), + "PriorityStatementFlag" => Some(852), + "PriorityStatementId" => Some(853), + "PriorityStatusId" => Some(854), + "PriorityStatusSub" => Some(855), + "PriorityTransactionCount" => Some(856), + "PriorityTransactionIndicator" => Some(857), + "PriorityTransmission" => Some(858), + "PriorityValueDate1" => Some(859), + "PriorityValueDate2" => Some(860), + "PriorityValueDateIndicator" => Some(861), + "PriorityVersion" => Some(862), + "PriorityVersionIndicator" => Some(863), + "PriorityVersionType" => Some(864), + "ReconciliationAccountReport" => Some(865), + "ReconciliationAdjustment" => Some(866), + "ReconciliationAmountCount" => Some(867), + "ReconciliationAuthenticationDate" => Some(868), + "ReconciliationBeneficiary1" => Some(869), + "ReconciliationBeneficiaryDate" => Some(870), + "ReconciliationChannel" => Some(871), + "ReconciliationChannelFlag" => Some(872), + "ReconciliationChannelSub" => Some(873), + "ReconciliationClearing1" => Some(874), + "ReconciliationClearingId" => Some(875), + "ReconciliationCurrency" => Some(876), + "ReconciliationIdentificationId" => Some(877), + "ReconciliationInstruction2" => Some(878), + "ReconciliationInstructionFlag" => Some(879), + "ReconciliationInstructionSub" => Some(880), + "ReconciliationMarginId" => Some(881), + "ReconciliationParty" => Some(882), + "ReconciliationPartyIndicator" => Some(883), + "ReconciliationPositionCount" => Some(884), + "ReconciliationPriority" => Some(885), + "ReconciliationPriority2" => Some(886), + "ReconciliationReconciliation1" => Some(887), + "ReconciliationReconciliationCount" => Some(888), + "ReconciliationReconciliationSub" => Some(889), + "ReconciliationReferenceCount" => Some(890), + "ReconciliationReferenceIndicator" => Some(891), + "ReconciliationRemittance" => Some(892), + "ReconciliationRemittanceId" => Some(893), + "ReconciliationRemittanceIndicator" => Some(894), + "ReconciliationRemittanceSub" => Some(895), + "ReconciliationSequenceCode" => Some(896), + "ReconciliationSequenceCount" => Some(897), + "ReconciliationStatusCount" => Some(898), + "ReconciliationStatusReport" => Some(899), + "ReconciliationTransactionDate" => Some(900), + "ReconciliationTransactionId" => Some(901), + "ReconciliationTransactionSub" => Some(902), + "ReconciliationTransactionType" => Some(903), + "ReconciliationTransmissionCount" => Some(904), + "ReconciliationTransmissionDate" => Some(905), + "ReconciliationValueDate2" => Some(906), + "ReconciliationValueDateType" => Some(907), + "ReconciliationVersion" => Some(908), + "ReconciliationVersionIndicator" => Some(909), + "ReconciliationVersionReport" => Some(910), + "ReferenceAccount" => Some(911), + "ReferenceAccount2" => Some(912), + "ReferenceAccountId" => Some(913), + "ReferenceAccountReport" => Some(914), + "ReferenceAmountCode" => Some(915), + "ReferenceAmountType" => Some(916), + "ReferenceAuthenticationSub" => Some(917), + "ReferenceBeneficiaryIndicator" => Some(918), + "ReferenceCategory2" => Some(919), + "ReferenceCategoryDate" => Some(920), + "ReferenceCategoryIndicator" => Some(921), + "ReferenceChannelReport" => Some(922), + "ReferenceClearing" => Some(923), + "ReferenceClearingId" => Some(924), + "ReferenceClearingType" => Some(925), + "ReferenceCollateral" => Some(926), + "ReferenceCollateral1" => Some(927), + "ReferenceCollateral2" => Some(928), + "ReferenceCurrency" => Some(929), + "ReferenceCurrencyCode" => Some(930), + "ReferenceIdentification" => Some(931), + "ReferenceIdentification1" => Some(932), + "ReferenceIdentificationCount" => Some(933), + "ReferenceIdentificationDate" => Some(934), + "ReferenceIdentificationReport" => Some(935), + "ReferenceInstructionDate" => Some(936), + "ReferenceInstructionFlag" => Some(937), + "ReferenceInstructionType" => Some(938), + "ReferenceMargin" => Some(939), + "ReferenceParty" => Some(940), + "ReferencePartySub" => Some(941), + "ReferencePositionId" => Some(942), + "ReferencePositionType" => Some(943), + "ReferencePriority" => Some(944), + "ReferencePriority1" => Some(945), + "ReferencePriorityReport" => Some(946), + "ReferenceReconciliation" => Some(947), + "ReferenceReconciliation1" => Some(948), + "ReferenceReconciliation2" => Some(949), + "ReferenceReferenceDate" => Some(950), + "ReferenceReferenceFlag" => Some(951), + "ReferenceRouting" => Some(952), + "ReferenceRoutingCount" => Some(953), + "ReferenceRoutingDate" => Some(954), + "ReferenceRoutingIndicator" => Some(955), + "ReferenceRoutingSub" => Some(956), + "ReferenceSequence" => Some(957), + "ReferenceSequenceId" => Some(958), + "ReferenceStatementDate" => Some(959), + "ReferenceTransactionDate" => Some(960), + "ReferenceTransactionSub" => Some(961), + "ReferenceTransmission" => Some(962), + "ReferenceTransmissionCode" => Some(963), + "ReferenceTransmissionCount" => Some(964), + "ReferenceValueDate2" => Some(965), + "ReferenceValueDateCode" => Some(966), + "ReferenceVersion1" => Some(967), + "ReferenceVersion2" => Some(968), + "RemittanceAccount" => Some(969), + "RemittanceAdjustmentDate" => Some(970), + "RemittanceAmountCode" => Some(971), + "RemittanceAmountDate" => Some(972), + "RemittanceAmountFlag" => Some(973), + "RemittanceAmountReport" => Some(974), + "RemittanceAuthentication" => Some(975), + "RemittanceAuthentication2" => Some(976), + "RemittanceAuthenticationCode" => Some(977), + "RemittanceAuthenticationDate" => Some(978), + "RemittanceBeneficiaryCount" => Some(979), + "RemittanceCategoryDate" => Some(980), + "RemittanceCategoryFlag" => Some(981), + "RemittanceChannel1" => Some(982), + "RemittanceClearing1" => Some(983), + "RemittanceClearingFlag" => Some(984), + "RemittanceClearingType" => Some(985), + "RemittanceCollateral" => Some(986), + "RemittanceCollateralCode" => Some(987), + "RemittanceCollateralSub" => Some(988), + "RemittanceCurrencyIndicator" => Some(989), + "RemittanceIdentificationReport" => Some(990), + "RemittanceIdentificationSub" => Some(991), + "RemittanceInstruction" => Some(992), + "RemittanceInstructionIndicator" => Some(993), + "RemittanceMarginId" => Some(994), + "RemittanceMarginSub" => Some(995), + "RemittancePositionId" => Some(996), + "RemittancePositionSub" => Some(997), + "RemittancePriorityReport" => Some(998), + "RemittanceReconciliation1" => Some(999), + "RemittanceReference" => Some(1000), + "RemittanceReference1" => Some(1001), + "RemittanceReferenceDate" => Some(1002), + "RemittanceRemittance1" => Some(1003), + "RemittanceRemittanceCount" => Some(1004), + "RemittanceRemittanceDate" => Some(1005), + "RemittanceRemittanceReport" => Some(1006), + "RemittanceRemittanceType" => Some(1007), + "RemittanceRouting" => Some(1008), + "RemittanceRouting1" => Some(1009), + "RemittanceRoutingSub" => Some(1010), + "RemittanceSequenceReport" => Some(1011), + "RemittanceSettlementFlag" => Some(1012), + "RemittanceSettlementReport" => Some(1013), + "RemittanceSettlementType" => Some(1014), + "RemittanceStatementSub" => Some(1015), + "RemittanceStatementType" => Some(1016), + "RemittanceStatus2" => Some(1017), + "RemittanceTransactionCount" => Some(1018), + "RemittanceTransactionReport" => Some(1019), + "RemittanceTransmission2" => Some(1020), + "RemittanceTransmissionCode" => Some(1021), + "RemittanceTransmissionFlag" => Some(1022), + "RemittanceValueDateCount" => Some(1023), + "RemittanceVersionDate" => Some(1024), + "RemittanceVersionFlag" => Some(1025), + "RemittanceVersionIndicator" => Some(1026), + "RemittanceVersionSub" => Some(1027), + "RoutingAccount" => Some(1028), + "RoutingAccountIndicator" => Some(1029), + "RoutingAdjustmentFlag" => Some(1030), + "RoutingAdjustmentSub" => Some(1031), + "RoutingAmountSub" => Some(1032), + "RoutingAmountType" => Some(1033), + "RoutingAuthentication" => Some(1034), + "RoutingAuthenticationFlag" => Some(1035), + "RoutingBeneficiaryDate" => Some(1036), + "RoutingBeneficiaryIndicator" => Some(1037), + "RoutingCategoryCount" => Some(1038), + "RoutingCategorySub" => Some(1039), + "RoutingChannel" => Some(1040), + "RoutingChannel2" => Some(1041), + "RoutingChannelId" => Some(1042), + "RoutingClearing" => Some(1043), + "RoutingClearingCode" => Some(1044), + "RoutingCollateralIndicator" => Some(1045), + "RoutingCurrency" => Some(1046), + "RoutingCurrencyCount" => Some(1047), + "RoutingCurrencyId" => Some(1048), + "RoutingCurrencyReport" => Some(1049), + "RoutingIdentificationSub" => Some(1050), + "RoutingInstruction" => Some(1051), + "RoutingInstructionId" => Some(1052), + "RoutingInstructionIndicator" => Some(1053), + "RoutingInstructionType" => Some(1054), + "RoutingMarginType" => Some(1055), + "RoutingParty2" => Some(1056), + "RoutingPartyCode" => Some(1057), + "RoutingPartyId" => Some(1058), + "RoutingPartyIndicator" => Some(1059), + "RoutingPartyReport" => Some(1060), + "RoutingPartyType" => Some(1061), + "RoutingPosition1" => Some(1062), + "RoutingPositionFlag" => Some(1063), + "RoutingPositionSub" => Some(1064), + "RoutingPriorityId" => Some(1065), + "RoutingReconciliation2" => Some(1066), + "RoutingReconciliationCode" => Some(1067), + "RoutingReconciliationId" => Some(1068), + "RoutingReferenceReport" => Some(1069), + "RoutingRouting" => Some(1070), + "RoutingRoutingCode" => Some(1071), + "RoutingSequenceReport" => Some(1072), + "RoutingSequenceSub" => Some(1073), + "RoutingSettlement1" => Some(1074), + "RoutingStatement2" => Some(1075), + "RoutingStatementCount" => Some(1076), + "RoutingStatementDate" => Some(1077), + "RoutingStatementId" => Some(1078), + "RoutingTransaction1" => Some(1079), + "RoutingTransactionDate" => Some(1080), + "RoutingTransactionIndicator" => Some(1081), + "RoutingTransmissionId" => Some(1082), + "RoutingTransmissionSub" => Some(1083), + "RoutingValueDate1" => Some(1084), + "RoutingVersionDate" => Some(1085), + "RoutingVersionReport" => Some(1086), + "SequenceAccount2" => Some(1087), + "SequenceAccountId" => Some(1088), + "SequenceAdjustment" => Some(1089), + "SequenceAmountCount" => Some(1090), + "SequenceAmountReport" => Some(1091), + "SequenceAuthentication2" => Some(1092), + "SequenceAuthenticationDate" => Some(1093), + "SequenceAuthenticationType" => Some(1094), + "SequenceCategoryDate" => Some(1095), + "SequenceCategorySub" => Some(1096), + "SequenceChannelDate" => Some(1097), + "SequenceClearingDate" => Some(1098), + "SequenceClearingId" => Some(1099), + "SequenceClearingIndicator" => Some(1100), + "SequenceCollateralReport" => Some(1101), + "SequenceCurrency" => Some(1102), + "SequenceIdentificationDate" => Some(1103), + "SequenceIdentificationId" => Some(1104), + "SequenceInstructionSub" => Some(1105), + "SequenceMargin" => Some(1106), + "SequenceMarginCount" => Some(1107), + "SequencePositionId" => Some(1108), + "SequencePriorityCount" => Some(1109), + "SequencePrioritySub" => Some(1110), + "SequenceReconciliationDate" => Some(1111), + "SequenceReference" => Some(1112), + "SequenceReferenceIndicator" => Some(1113), + "SequenceRemittanceCount" => Some(1114), + "SequenceRemittanceFlag" => Some(1115), + "SequenceRemittanceIndicator" => Some(1116), + "SequenceRouting2" => Some(1117), + "SequenceSettlement" => Some(1118), + "SequenceSettlementIndicator" => Some(1119), + "SequenceStatement" => Some(1120), + "SequenceStatementType" => Some(1121), + "SequenceStatusSub" => Some(1122), + "SequenceTransactionFlag" => Some(1123), + "SequenceTransactionSub" => Some(1124), + "SequenceTransactionType" => Some(1125), + "SequenceValueDateReport" => Some(1126), + "SequenceValueDateSub" => Some(1127), + "SequenceVersion2" => Some(1128), + "SequenceVersionIndicator" => Some(1129), + "SettlementAccount" => Some(1130), + "SettlementAdjustment1" => Some(1131), + "SettlementAdjustment2" => Some(1132), + "SettlementAdjustmentFlag" => Some(1133), + "SettlementAdjustmentReport" => Some(1134), + "SettlementAmount1" => Some(1135), + "SettlementAmount2" => Some(1136), + "SettlementAmountIndicator" => Some(1137), + "SettlementAuthentication2" => Some(1138), + "SettlementAuthenticationCount" => Some(1139), + "SettlementBeneficiaryFlag" => Some(1140), + "SettlementBeneficiaryType" => Some(1141), + "SettlementCategoryCode" => Some(1142), + "SettlementChannel" => Some(1143), + "SettlementChannelCode" => Some(1144), + "SettlementClearingReport" => Some(1145), + "SettlementCollateral2" => Some(1146), + "SettlementCollateralCode" => Some(1147), + "SettlementCollateralDate" => Some(1148), + "SettlementCollateralSub" => Some(1149), + "SettlementCurrency" => Some(1150), + "SettlementCurrency2" => Some(1151), + "SettlementIdentificationCount" => Some(1152), + "SettlementIdentificationIndicator" => Some(1153), + "SettlementInstruction" => Some(1154), + "SettlementInstructionDate" => Some(1155), + "SettlementMargin2" => Some(1156), + "SettlementMarginId" => Some(1157), + "SettlementMarginSub" => Some(1158), + "SettlementPartyCode" => Some(1159), + "SettlementPartyFlag" => Some(1160), + "SettlementPartyType" => Some(1161), + "SettlementPositionType" => Some(1162), + "SettlementPriorityCount" => Some(1163), + "SettlementPriorityFlag" => Some(1164), + "SettlementPriorityIndicator" => Some(1165), + "SettlementPriorityReport" => Some(1166), + "SettlementReconciliation1" => Some(1167), + "SettlementReconciliationSub" => Some(1168), + "SettlementReferenceReport" => Some(1169), + "SettlementRemittanceSub" => Some(1170), + "SettlementSequence2" => Some(1171), + "SettlementSequenceSub" => Some(1172), + "SettlementSequenceType" => Some(1173), + "SettlementSettlementId" => Some(1174), + "SettlementSettlementReport" => Some(1175), + "SettlementStatementCode" => Some(1176), + "SettlementStatus1" => Some(1177), + "SettlementTransaction" => Some(1178), + "SettlementTransactionReport" => Some(1179), + "SettlementTransmission" => Some(1180), + "SettlementValueDate" => Some(1181), + "SettlementValueDate2" => Some(1182), + "SettlementValueDateFlag" => Some(1183), + "SettlementValueDateSub" => Some(1184), + "StatementAccount2" => Some(1185), + "StatementAdjustmentDate" => Some(1186), + "StatementAdjustmentIndicator" => Some(1187), + "StatementAdjustmentReport" => Some(1188), + "StatementAdjustmentSub" => Some(1189), + "StatementAdjustmentType" => Some(1190), + "StatementAmount" => Some(1191), + "StatementAmount2" => Some(1192), + "StatementAmountCode" => Some(1193), + "StatementBeneficiaryCode" => Some(1194), + "StatementBeneficiaryFlag" => Some(1195), + "StatementBeneficiaryId" => Some(1196), + "StatementBeneficiaryType" => Some(1197), + "StatementCategory" => Some(1198), + "StatementChannel1" => Some(1199), + "StatementChannelCode" => Some(1200), + "StatementChannelId" => Some(1201), + "StatementCollateral" => Some(1202), + "StatementCollateralSub" => Some(1203), + "StatementCurrencyCode" => Some(1204), + "StatementCurrencyType" => Some(1205), + "StatementIdentification" => Some(1206), + "StatementIdentificationReport" => Some(1207), + "StatementInstructionReport" => Some(1208), + "StatementMargin1" => Some(1209), + "StatementMarginCount" => Some(1210), + "StatementMarginReport" => Some(1211), + "StatementMarginType" => Some(1212), + "StatementParty" => Some(1213), + "StatementPosition" => Some(1214), + "StatementPositionDate" => Some(1215), + "StatementReconciliationIndicator" => Some(1216), + "StatementReference2" => Some(1217), + "StatementReferenceCode" => Some(1218), + "StatementReferenceSub" => Some(1219), + "StatementRouting" => Some(1220), + "StatementRoutingCode" => Some(1221), + "StatementRoutingId" => Some(1222), + "StatementRoutingIndicator" => Some(1223), + "StatementRoutingType" => Some(1224), + "StatementSequenceCode" => Some(1225), + "StatementSequenceCount" => Some(1226), + "StatementSequenceSub" => Some(1227), + "StatementSettlementReport" => Some(1228), + "StatementStatement2" => Some(1229), + "StatementStatementId" => Some(1230), + "StatementStatus2" => Some(1231), + "StatementTransaction1" => Some(1232), + "StatementTransactionIndicator" => Some(1233), + "StatementTransactionSub" => Some(1234), + "StatementTransmissionReport" => Some(1235), + "StatementVersion1" => Some(1236), + "StatementVersionCount" => Some(1237), + "StatementVersionDate" => Some(1238), + "StatementVersionType" => Some(1239), + "StatusAccountId" => Some(1240), + "StatusAccountIndicator" => Some(1241), + "StatusAccountType" => Some(1242), + "StatusAdjustmentCode" => Some(1243), + "StatusAdjustmentDate" => Some(1244), + "StatusAdjustmentFlag" => Some(1245), + "StatusAmount" => Some(1246), + "StatusAmount1" => Some(1247), + "StatusAmountCode" => Some(1248), + "StatusAmountDate" => Some(1249), + "StatusAmountReport" => Some(1250), + "StatusAuthenticationFlag" => Some(1251), + "StatusAuthenticationReport" => Some(1252), + "StatusBeneficiary" => Some(1253), + "StatusCategory" => Some(1254), + "StatusCategory2" => Some(1255), + "StatusChannel" => Some(1256), + "StatusChannel1" => Some(1257), + "StatusChannelFlag" => Some(1258), + "StatusChannelType" => Some(1259), + "StatusClearing" => Some(1260), + "StatusCollateral2" => Some(1261), + "StatusCollateralCount" => Some(1262), + "StatusCollateralDate" => Some(1263), + "StatusCollateralFlag" => Some(1264), + "StatusCollateralSub" => Some(1265), + "StatusCurrencyDate" => Some(1266), + "StatusCurrencySub" => Some(1267), + "StatusCurrencyType" => Some(1268), + "StatusIdentificationId" => Some(1269), + "StatusInstructionCount" => Some(1270), + "StatusInstructionIndicator" => Some(1271), + "StatusMargin2" => Some(1272), + "StatusMarginCode" => Some(1273), + "StatusMarginId" => Some(1274), + "StatusPartyReport" => Some(1275), + "StatusPositionCount" => Some(1276), + "StatusPositionFlag" => Some(1277), + "StatusPositionIndicator" => Some(1278), + "StatusPriority" => Some(1279), + "StatusPriorityReport" => Some(1280), + "StatusReconciliationDate" => Some(1281), + "StatusReconciliationId" => Some(1282), + "StatusReconciliationType" => Some(1283), + "StatusRemittanceCount" => Some(1284), + "StatusRemittanceDate" => Some(1285), + "StatusRouting" => Some(1286), + "StatusSequenceCode" => Some(1287), + "StatusSettlementIndicator" => Some(1288), + "StatusTransactionCode" => Some(1289), + "StatusTransactionCount" => Some(1290), + "StatusTransactionFlag" => Some(1291), + "StatusTransactionReport" => Some(1292), + "StatusTransmissionCount" => Some(1293), + "StatusTransmissionType" => Some(1294), + "StatusValueDate2" => Some(1295), + "StatusValueDateCode" => Some(1296), + "StatusValueDateCount" => Some(1297), + "StatusVersion1" => Some(1298), + "TransactionAccount" => Some(1299), + "TransactionAccountCount" => Some(1300), + "TransactionAdjustment1" => Some(1301), + "TransactionAdjustmentCode" => Some(1302), + "TransactionAdjustmentFlag" => Some(1303), + "TransactionAmount" => Some(1304), + "TransactionAmountCount" => Some(1305), + "TransactionAmountIndicator" => Some(1306), + "TransactionAuthenticationIndicator" => Some(1307), + "TransactionBeneficiary2" => Some(1308), + "TransactionBeneficiarySub" => Some(1309), + "TransactionCategoryDate" => Some(1310), + "TransactionCategoryFlag" => Some(1311), + "TransactionChannelSub" => Some(1312), + "TransactionChannelType" => Some(1313), + "TransactionClearing" => Some(1314), + "TransactionClearingReport" => Some(1315), + "TransactionClearingSub" => Some(1316), + "TransactionCollateral" => Some(1317), + "TransactionCollateralFlag" => Some(1318), + "TransactionCollateralId" => Some(1319), + "TransactionIdentificationIndicator" => Some(1320), + "TransactionInstruction" => Some(1321), + "TransactionInstructionCode" => Some(1322), + "TransactionInstructionSub" => Some(1323), + "TransactionMargin2" => Some(1324), + "TransactionMarginCount" => Some(1325), + "TransactionPosition2" => Some(1326), + "TransactionPositionIndicator" => Some(1327), + "TransactionPriorityId" => Some(1328), + "TransactionReconciliationCount" => Some(1329), + "TransactionRemittanceFlag" => Some(1330), + "TransactionRemittanceReport" => Some(1331), + "TransactionRemittanceType" => Some(1332), + "TransactionRoutingFlag" => Some(1333), + "TransactionSequenceFlag" => Some(1334), + "TransactionSequenceId" => Some(1335), + "TransactionSequenceSub" => Some(1336), + "TransactionSequenceType" => Some(1337), + "TransactionSettlementDate" => Some(1338), + "TransactionSettlementFlag" => Some(1339), + "TransactionStatement" => Some(1340), + "TransactionStatementIndicator" => Some(1341), + "TransactionStatusType" => Some(1342), + "TransactionTransactionDate" => Some(1343), + "TransactionTransmissionCount" => Some(1344), + "TransactionVersionCode" => Some(1345), + "TransactionVersionType" => Some(1346), + "TransmissionAdjustment1" => Some(1347), + "TransmissionAdjustmentDate" => Some(1348), + "TransmissionAdjustmentFlag" => Some(1349), + "TransmissionAdjustmentType" => Some(1350), + "TransmissionCategory" => Some(1351), + "TransmissionCategoryFlag" => Some(1352), + "TransmissionCategoryId" => Some(1353), + "TransmissionChannel" => Some(1354), + "TransmissionClearingId" => Some(1355), + "TransmissionCollateralDate" => Some(1356), + "TransmissionCurrencyIndicator" => Some(1357), + "TransmissionCurrencyReport" => Some(1358), + "TransmissionIdentification1" => Some(1359), + "TransmissionInstruction" => Some(1360), + "TransmissionInstruction2" => Some(1361), + "TransmissionInstructionFlag" => Some(1362), + "TransmissionInstructionType" => Some(1363), + "TransmissionMargin2" => Some(1364), + "TransmissionMarginDate" => Some(1365), + "TransmissionPartyCode" => Some(1366), + "TransmissionPartyDate" => Some(1367), + "TransmissionPositionReport" => Some(1368), + "TransmissionReconciliation2" => Some(1369), + "TransmissionReconciliationSub" => Some(1370), + "TransmissionReference1" => Some(1371), + "TransmissionReferenceIndicator" => Some(1372), + "TransmissionRemittanceDate" => Some(1373), + "TransmissionRemittanceReport" => Some(1374), + "TransmissionRoutingCount" => Some(1375), + "TransmissionRoutingSub" => Some(1376), + "TransmissionSequenceDate" => Some(1377), + "TransmissionSettlementCount" => Some(1378), + "TransmissionSettlementFlag" => Some(1379), + "TransmissionStatement" => Some(1380), + "TransmissionStatement2" => Some(1381), + "TransmissionStatementCount" => Some(1382), + "TransmissionStatusCode" => Some(1383), + "TransmissionTransaction" => Some(1384), + "TransmissionTransmissionDate" => Some(1385), + "TransmissionValueDate" => Some(1386), + "TransmissionValueDateId" => Some(1387), + "TransmissionValueDateIndicator" => Some(1388), + "ValueDateAccount2" => Some(1389), + "ValueDateAccountCount" => Some(1390), + "ValueDateAdjustment" => Some(1391), + "ValueDateAdjustmentCount" => Some(1392), + "ValueDateAdjustmentId" => Some(1393), + "ValueDateAdjustmentReport" => Some(1394), + "ValueDateAdjustmentType" => Some(1395), + "ValueDateAmountCode" => Some(1396), + "ValueDateAmountCount" => Some(1397), + "ValueDateBeneficiary" => Some(1398), + "ValueDateBeneficiarySub" => Some(1399), + "ValueDateClearingDate" => Some(1400), + "ValueDateCurrency" => Some(1401), + "ValueDateIdentification2" => Some(1402), + "ValueDateIdentificationFlag" => Some(1403), + "ValueDateIdentificationIndicator" => Some(1404), + "ValueDateIdentificationReport" => Some(1405), + "ValueDateMargin" => Some(1406), + "ValueDatePartyId" => Some(1407), + "ValueDatePosition" => Some(1408), + "ValueDatePositionType" => Some(1409), + "ValueDatePriority" => Some(1410), + "ValueDatePriority2" => Some(1411), + "ValueDatePriorityCount" => Some(1412), + "ValueDatePriorityReport" => Some(1413), + "ValueDateReconciliationIndicator" => Some(1414), + "ValueDateReferenceType" => Some(1415), + "ValueDateRemittance2" => Some(1416), + "ValueDateRemittanceReport" => Some(1417), + "ValueDateRoutingReport" => Some(1418), + "ValueDateSequence" => Some(1419), + "ValueDateSequence1" => Some(1420), + "ValueDateSequenceCount" => Some(1421), + "ValueDateSequenceType" => Some(1422), + "ValueDateSettlement2" => Some(1423), + "ValueDateSettlementCount" => Some(1424), + "ValueDateStatementCode" => Some(1425), + "ValueDateStatementReport" => Some(1426), + "ValueDateStatementSub" => Some(1427), + "ValueDateStatusCode" => Some(1428), + "ValueDateStatusType" => Some(1429), + "ValueDateTransaction2" => Some(1430), + "ValueDateTransactionCount" => Some(1431), + "ValueDateTransactionId" => Some(1432), + "ValueDateTransactionReport" => Some(1433), + "ValueDateValueDateCode" => Some(1434), + "ValueDateValueDateIndicator" => Some(1435), + "ValueDateValueDateReport" => Some(1436), + "ValueDateVersionFlag" => Some(1437), + "ValueDateVersionSub" => Some(1438), + "VersionAccount" => Some(1439), + "VersionAccountCode" => Some(1440), + "VersionAccountCount" => Some(1441), + "VersionAccountIndicator" => Some(1442), + "VersionAdjustmentIndicator" => Some(1443), + "VersionAdjustmentSub" => Some(1444), + "VersionAmount" => Some(1445), + "VersionAmount2" => Some(1446), + "VersionAmountCode" => Some(1447), + "VersionAuthentication1" => Some(1448), + "VersionAuthenticationCode" => Some(1449), + "VersionBeneficiary1" => Some(1450), + "VersionBeneficiary2" => Some(1451), + "VersionBeneficiaryIndicator" => Some(1452), + "VersionCategory2" => Some(1453), + "VersionChannel" => Some(1454), + "VersionChannelDate" => Some(1455), + "VersionChannelFlag" => Some(1456), + "VersionClearing" => Some(1457), + "VersionClearingCode" => Some(1458), + "VersionClearingCount" => Some(1459), + "VersionClearingFlag" => Some(1460), + "VersionClearingSub" => Some(1461), + "VersionCollateralFlag" => Some(1462), + "VersionCurrencyCode" => Some(1463), + "VersionCurrencyFlag" => Some(1464), + "VersionCurrencyIndicator" => Some(1465), + "VersionIdentificationCount" => Some(1466), + "VersionIdentificationDate" => Some(1467), + "VersionIdentificationFlag" => Some(1468), + "VersionIdentificationIndicator" => Some(1469), + "VersionInstructionFlag" => Some(1470), + "VersionMargin1" => Some(1471), + "VersionMarginId" => Some(1472), + "VersionPartyId" => Some(1473), + "VersionPosition1" => Some(1474), + "VersionPosition2" => Some(1475), + "VersionPositionReport" => Some(1476), + "VersionPriority2" => Some(1477), + "VersionPriorityCode" => Some(1478), + "VersionReconciliationCount" => Some(1479), + "VersionReconciliationId" => Some(1480), + "VersionReconciliationSub" => Some(1481), + "VersionReference2" => Some(1482), + "VersionReferenceCode" => Some(1483), + "VersionRemittanceIndicator" => Some(1484), + "VersionRoutingReport" => Some(1485), + "VersionSequence2" => Some(1486), + "VersionSequenceFlag" => Some(1487), + "VersionSequenceType" => Some(1488), + "VersionSettlement" => Some(1489), + "VersionSettlementType" => Some(1490), + "VersionStatement" => Some(1491), + "VersionStatementFlag" => Some(1492), + "VersionStatementId" => Some(1493), + "VersionStatusIndicator" => Some(1494), + "VersionTransactionCode" => Some(1495), + "VersionTransactionCount" => Some(1496), + "VersionTransmissionType" => Some(1497), + "VersionValueDateCount" => Some(1498), + "VersionValueDateFlag" => Some(1499), + _ => None, + } +} + +pub static PHF_1500: phf::Map<&'static str, u32> = phf::phf_map! { + "AccountAccount" => 0u32, + "AccountAccount1" => 1u32, + "AccountAdjustment2" => 2u32, + "AccountAmountDate" => 3u32, + "AccountAmountFlag" => 4u32, + "AccountAmountType" => 5u32, + "AccountAuthenticationType" => 6u32, + "AccountBeneficiaryCode" => 7u32, + "AccountBeneficiaryCount" => 8u32, + "AccountBeneficiaryFlag" => 9u32, + "AccountCategory" => 10u32, + "AccountCategoryDate" => 11u32, + "AccountCategoryFlag" => 12u32, + "AccountCategoryIndicator" => 13u32, + "AccountChannel" => 14u32, + "AccountChannelCount" => 15u32, + "AccountChannelId" => 16u32, + "AccountChannelIndicator" => 17u32, + "AccountClearing1" => 18u32, + "AccountCollateral2" => 19u32, + "AccountCollateralFlag" => 20u32, + "AccountCollateralReport" => 21u32, + "AccountCurrency1" => 22u32, + "AccountCurrencyFlag" => 23u32, + "AccountCurrencyId" => 24u32, + "AccountCurrencyIndicator" => 25u32, + "AccountCurrencySub" => 26u32, + "AccountIdentification" => 27u32, + "AccountIdentification1" => 28u32, + "AccountInstructionReport" => 29u32, + "AccountMarginFlag" => 30u32, + "AccountMarginType" => 31u32, + "AccountParty1" => 32u32, + "AccountPartyCount" => 33u32, + "AccountPartyFlag" => 34u32, + "AccountPartyIndicator" => 35u32, + "AccountPartyType" => 36u32, + "AccountPosition" => 37u32, + "AccountPriorityDate" => 38u32, + "AccountReconciliation" => 39u32, + "AccountReconciliationType" => 40u32, + "AccountReferenceId" => 41u32, + "AccountReferenceIndicator" => 42u32, + "AccountReferenceType" => 43u32, + "AccountRemittanceId" => 44u32, + "AccountRemittanceIndicator" => 45u32, + "AccountRoutingCount" => 46u32, + "AccountRoutingIndicator" => 47u32, + "AccountRoutingType" => 48u32, + "AccountSequence" => 49u32, + "AccountSequence1" => 50u32, + "AccountSequence2" => 51u32, + "AccountSettlementDate" => 52u32, + "AccountSettlementType" => 53u32, + "AccountStatement" => 54u32, + "AccountStatementCount" => 55u32, + "AccountStatusFlag" => 56u32, + "AccountStatusType" => 57u32, + "AccountTransaction" => 58u32, + "AccountTransactionCode" => 59u32, + "AccountTransactionSub" => 60u32, + "AccountTransactionType" => 61u32, + "AccountTransmission" => 62u32, + "AccountTransmission1" => 63u32, + "AccountVersion" => 64u32, + "AccountVersionSub" => 65u32, + "AccountVersionType" => 66u32, + "AdjustmentAccount" => 67u32, + "AdjustmentAccount1" => 68u32, + "AdjustmentAccountCode" => 69u32, + "AdjustmentAdjustment2" => 70u32, + "AdjustmentAdjustmentIndicator" => 71u32, + "AdjustmentAmount" => 72u32, + "AdjustmentAuthentication" => 73u32, + "AdjustmentAuthenticationFlag" => 74u32, + "AdjustmentBeneficiaryCount" => 75u32, + "AdjustmentBeneficiaryId" => 76u32, + "AdjustmentCategory1" => 77u32, + "AdjustmentChannelFlag" => 78u32, + "AdjustmentChannelType" => 79u32, + "AdjustmentClearingDate" => 80u32, + "AdjustmentClearingId" => 81u32, + "AdjustmentCollateralIndicator" => 82u32, + "AdjustmentCurrency" => 83u32, + "AdjustmentCurrencyId" => 84u32, + "AdjustmentCurrencyReport" => 85u32, + "AdjustmentIdentificationReport" => 86u32, + "AdjustmentInstructionDate" => 87u32, + "AdjustmentInstructionType" => 88u32, + "AdjustmentMarginCode" => 89u32, + "AdjustmentMarginReport" => 90u32, + "AdjustmentPosition2" => 91u32, + "AdjustmentPositionCount" => 92u32, + "AdjustmentPositionDate" => 93u32, + "AdjustmentReconciliation" => 94u32, + "AdjustmentReconciliationCount" => 95u32, + "AdjustmentReference1" => 96u32, + "AdjustmentRemittance" => 97u32, + "AdjustmentRemittance1" => 98u32, + "AdjustmentRemittanceFlag" => 99u32, + "AdjustmentRoutingIndicator" => 100u32, + "AdjustmentSequence" => 101u32, + "AdjustmentSequenceReport" => 102u32, + "AdjustmentSettlementFlag" => 103u32, + "AdjustmentStatement" => 104u32, + "AdjustmentStatementCode" => 105u32, + "AdjustmentStatementFlag" => 106u32, + "AdjustmentStatementId" => 107u32, + "AdjustmentStatementIndicator" => 108u32, + "AdjustmentStatusDate" => 109u32, + "AdjustmentStatusType" => 110u32, + "AdjustmentTransactionDate" => 111u32, + "AdjustmentTransmissionCode" => 112u32, + "AdjustmentTransmissionCount" => 113u32, + "AdjustmentTransmissionId" => 114u32, + "AdjustmentTransmissionSub" => 115u32, + "AdjustmentValueDateFlag" => 116u32, + "AdjustmentVersionType" => 117u32, + "AmountAccount1" => 118u32, + "AmountAccountCode" => 119u32, + "AmountAccountFlag" => 120u32, + "AmountAdjustment" => 121u32, + "AmountAdjustmentCode" => 122u32, + "AmountAdjustmentFlag" => 123u32, + "AmountAuthentication" => 124u32, + "AmountAuthenticationId" => 125u32, + "AmountAuthenticationIndicator" => 126u32, + "AmountChannel" => 127u32, + "AmountChannelCode" => 128u32, + "AmountChannelDate" => 129u32, + "AmountCollateralFlag" => 130u32, + "AmountCurrency" => 131u32, + "AmountCurrencyCount" => 132u32, + "AmountIdentificationIndicator" => 133u32, + "AmountIdentificationSub" => 134u32, + "AmountInstructionFlag" => 135u32, + "AmountInstructionReport" => 136u32, + "AmountPartyId" => 137u32, + "AmountPartyIndicator" => 138u32, + "AmountPriorityReport" => 139u32, + "AmountReconciliationIndicator" => 140u32, + "AmountReconciliationSub" => 141u32, + "AmountReference" => 142u32, + "AmountReferenceIndicator" => 143u32, + "AmountRemittance" => 144u32, + "AmountRemittance1" => 145u32, + "AmountRemittance2" => 146u32, + "AmountRemittanceFlag" => 147u32, + "AmountRemittanceReport" => 148u32, + "AmountSequenceDate" => 149u32, + "AmountSequenceReport" => 150u32, + "AmountSettlementDate" => 151u32, + "AmountStatementIndicator" => 152u32, + "AmountStatusFlag" => 153u32, + "AmountStatusIndicator" => 154u32, + "AmountTransaction" => 155u32, + "AmountTransactionCount" => 156u32, + "AmountTransmission2" => 157u32, + "AmountTransmissionCode" => 158u32, + "AmountTransmissionIndicator" => 159u32, + "AmountValueDate" => 160u32, + "AmountValueDate1" => 161u32, + "AmountVersionDate" => 162u32, + "AuthenticationAccountDate" => 163u32, + "AuthenticationAccountId" => 164u32, + "AuthenticationAccountType" => 165u32, + "AuthenticationAdjustmentType" => 166u32, + "AuthenticationAmountFlag" => 167u32, + "AuthenticationAuthentication1" => 168u32, + "AuthenticationBeneficiaryCode" => 169u32, + "AuthenticationBeneficiaryDate" => 170u32, + "AuthenticationBeneficiaryId" => 171u32, + "AuthenticationBeneficiaryIndicator" => 172u32, + "AuthenticationBeneficiarySub" => 173u32, + "AuthenticationCategory1" => 174u32, + "AuthenticationCategoryDate" => 175u32, + "AuthenticationCategoryFlag" => 176u32, + "AuthenticationChannel1" => 177u32, + "AuthenticationChannel2" => 178u32, + "AuthenticationChannelFlag" => 179u32, + "AuthenticationClearing" => 180u32, + "AuthenticationClearingIndicator" => 181u32, + "AuthenticationClearingReport" => 182u32, + "AuthenticationCollateralType" => 183u32, + "AuthenticationCurrencyIndicator" => 184u32, + "AuthenticationCurrencyType" => 185u32, + "AuthenticationIdentificationFlag" => 186u32, + "AuthenticationIdentificationReport" => 187u32, + "AuthenticationInstruction" => 188u32, + "AuthenticationInstructionId" => 189u32, + "AuthenticationMarginCount" => 190u32, + "AuthenticationPartyCode" => 191u32, + "AuthenticationPosition1" => 192u32, + "AuthenticationPositionFlag" => 193u32, + "AuthenticationPositionReport" => 194u32, + "AuthenticationPriorityCode" => 195u32, + "AuthenticationPriorityIndicator" => 196u32, + "AuthenticationReconciliationIndicator" => 197u32, + "AuthenticationReference2" => 198u32, + "AuthenticationReferenceFlag" => 199u32, + "AuthenticationReferenceId" => 200u32, + "AuthenticationRemittance1" => 201u32, + "AuthenticationRemittanceFlag" => 202u32, + "AuthenticationRemittanceIndicator" => 203u32, + "AuthenticationRemittanceType" => 204u32, + "AuthenticationRouting" => 205u32, + "AuthenticationRouting1" => 206u32, + "AuthenticationSequenceCode" => 207u32, + "AuthenticationSequenceId" => 208u32, + "AuthenticationSequenceIndicator" => 209u32, + "AuthenticationSequenceSub" => 210u32, + "AuthenticationSettlement" => 211u32, + "AuthenticationSettlement1" => 212u32, + "AuthenticationSettlementId" => 213u32, + "AuthenticationSettlementSub" => 214u32, + "AuthenticationStatementCount" => 215u32, + "AuthenticationStatementId" => 216u32, + "AuthenticationStatusId" => 217u32, + "AuthenticationTransaction1" => 218u32, + "AuthenticationTransactionDate" => 219u32, + "AuthenticationTransactionSub" => 220u32, + "AuthenticationTransactionType" => 221u32, + "AuthenticationTransmissionCode" => 222u32, + "AuthenticationTransmissionIndicator" => 223u32, + "AuthenticationTransmissionReport" => 224u32, + "AuthenticationValueDate2" => 225u32, + "AuthenticationVersion1" => 226u32, + "BeneficiaryAccount1" => 227u32, + "BeneficiaryAccountReport" => 228u32, + "BeneficiaryAccountType" => 229u32, + "BeneficiaryAdjustmentCount" => 230u32, + "BeneficiaryAdjustmentDate" => 231u32, + "BeneficiaryAdjustmentType" => 232u32, + "BeneficiaryAmountType" => 233u32, + "BeneficiaryAuthenticationCode" => 234u32, + "BeneficiaryAuthenticationSub" => 235u32, + "BeneficiaryAuthenticationType" => 236u32, + "BeneficiaryBeneficiary" => 237u32, + "BeneficiaryBeneficiary2" => 238u32, + "BeneficiaryBeneficiaryCode" => 239u32, + "BeneficiaryBeneficiaryCount" => 240u32, + "BeneficiaryBeneficiaryIndicator" => 241u32, + "BeneficiaryBeneficiaryReport" => 242u32, + "BeneficiaryCategory" => 243u32, + "BeneficiaryCategoryCode" => 244u32, + "BeneficiaryChannel2" => 245u32, + "BeneficiaryChannelCount" => 246u32, + "BeneficiaryChannelId" => 247u32, + "BeneficiaryCollateralCount" => 248u32, + "BeneficiaryCollateralFlag" => 249u32, + "BeneficiaryCollateralIndicator" => 250u32, + "BeneficiaryCurrencyType" => 251u32, + "BeneficiaryIdentification1" => 252u32, + "BeneficiaryIdentification2" => 253u32, + "BeneficiaryIdentificationReport" => 254u32, + "BeneficiaryInstructionReport" => 255u32, + "BeneficiaryInstructionSub" => 256u32, + "BeneficiaryMarginId" => 257u32, + "BeneficiaryPartyCode" => 258u32, + "BeneficiaryPartyFlag" => 259u32, + "BeneficiaryPartyIndicator" => 260u32, + "BeneficiaryPosition" => 261u32, + "BeneficiaryPriorityType" => 262u32, + "BeneficiaryReference1" => 263u32, + "BeneficiaryReferenceDate" => 264u32, + "BeneficiaryReferenceFlag" => 265u32, + "BeneficiaryReferenceReport" => 266u32, + "BeneficiaryRemittance" => 267u32, + "BeneficiaryRemittance2" => 268u32, + "BeneficiaryRemittanceCount" => 269u32, + "BeneficiaryRemittanceSub" => 270u32, + "BeneficiaryRoutingIndicator" => 271u32, + "BeneficiarySequenceId" => 272u32, + "BeneficiarySettlementIndicator" => 273u32, + "BeneficiaryStatus2" => 274u32, + "BeneficiaryStatusCount" => 275u32, + "BeneficiaryStatusDate" => 276u32, + "BeneficiaryTransactionSub" => 277u32, + "BeneficiaryTransmission" => 278u32, + "BeneficiaryTransmission1" => 279u32, + "BeneficiaryTransmission2" => 280u32, + "BeneficiaryTransmissionFlag" => 281u32, + "BeneficiaryTransmissionIndicator" => 282u32, + "BeneficiaryValueDate" => 283u32, + "BeneficiaryValueDate1" => 284u32, + "BeneficiaryVersionCount" => 285u32, + "BeneficiaryVersionFlag" => 286u32, + "CategoryAccount1" => 287u32, + "CategoryAccountDate" => 288u32, + "CategoryAccountFlag" => 289u32, + "CategoryAccountType" => 290u32, + "CategoryAdjustment1" => 291u32, + "CategoryAdjustmentDate" => 292u32, + "CategoryAmount" => 293u32, + "CategoryAmountIndicator" => 294u32, + "CategoryAuthentication1" => 295u32, + "CategoryAuthentication2" => 296u32, + "CategoryAuthenticationIndicator" => 297u32, + "CategoryAuthenticationType" => 298u32, + "CategoryBeneficiaryCode" => 299u32, + "CategoryBeneficiaryIndicator" => 300u32, + "CategoryChannel" => 301u32, + "CategoryChannelIndicator" => 302u32, + "CategoryClearingFlag" => 303u32, + "CategoryClearingType" => 304u32, + "CategoryCollateral1" => 305u32, + "CategoryCollateralFlag" => 306u32, + "CategoryCollateralType" => 307u32, + "CategoryCurrency" => 308u32, + "CategoryCurrency1" => 309u32, + "CategoryCurrencyReport" => 310u32, + "CategoryCurrencySub" => 311u32, + "CategoryCurrencyType" => 312u32, + "CategoryIdentification1" => 313u32, + "CategoryIdentificationSub" => 314u32, + "CategoryIdentificationType" => 315u32, + "CategoryInstruction" => 316u32, + "CategoryInstructionCount" => 317u32, + "CategoryInstructionIndicator" => 318u32, + "CategoryMargin" => 319u32, + "CategoryPartySub" => 320u32, + "CategoryPositionId" => 321u32, + "CategoryPositionIndicator" => 322u32, + "CategoryPositionReport" => 323u32, + "CategoryPriority1" => 324u32, + "CategoryPriorityId" => 325u32, + "CategoryReconciliation1" => 326u32, + "CategoryReconciliation2" => 327u32, + "CategoryReconciliationId" => 328u32, + "CategoryReconciliationSub" => 329u32, + "CategoryReferenceDate" => 330u32, + "CategoryRemittance1" => 331u32, + "CategoryRemittanceDate" => 332u32, + "CategoryRemittanceFlag" => 333u32, + "CategoryRemittanceReport" => 334u32, + "CategoryRouting" => 335u32, + "CategoryRoutingIndicator" => 336u32, + "CategorySequenceCount" => 337u32, + "CategorySequenceIndicator" => 338u32, + "CategorySettlement" => 339u32, + "CategoryStatusCount" => 340u32, + "CategoryStatusReport" => 341u32, + "CategoryTransaction" => 342u32, + "CategoryTransactionDate" => 343u32, + "CategoryTransmission" => 344u32, + "CategoryTransmissionCode" => 345u32, + "CategoryValueDateIndicator" => 346u32, + "CategoryVersion" => 347u32, + "CategoryVersion1" => 348u32, + "ChannelAccount2" => 349u32, + "ChannelAccountCode" => 350u32, + "ChannelAccountFlag" => 351u32, + "ChannelAdjustment" => 352u32, + "ChannelAdjustmentIndicator" => 353u32, + "ChannelAmount" => 354u32, + "ChannelAmountIndicator" => 355u32, + "ChannelAmountReport" => 356u32, + "ChannelAuthentication" => 357u32, + "ChannelAuthenticationReport" => 358u32, + "ChannelAuthenticationType" => 359u32, + "ChannelBeneficiary1" => 360u32, + "ChannelBeneficiaryDate" => 361u32, + "ChannelCategoryId" => 362u32, + "ChannelCategoryType" => 363u32, + "ChannelChannelCode" => 364u32, + "ChannelClearingDate" => 365u32, + "ChannelCollateralCount" => 366u32, + "ChannelCollateralReport" => 367u32, + "ChannelCollateralSub" => 368u32, + "ChannelIdentification" => 369u32, + "ChannelIdentificationFlag" => 370u32, + "ChannelIdentificationId" => 371u32, + "ChannelInstructionFlag" => 372u32, + "ChannelMargin2" => 373u32, + "ChannelMarginCount" => 374u32, + "ChannelMarginDate" => 375u32, + "ChannelMarginIndicator" => 376u32, + "ChannelParty" => 377u32, + "ChannelParty2" => 378u32, + "ChannelPosition" => 379u32, + "ChannelPositionId" => 380u32, + "ChannelPositionIndicator" => 381u32, + "ChannelPriority" => 382u32, + "ChannelPriority2" => 383u32, + "ChannelPriorityCount" => 384u32, + "ChannelReconciliation" => 385u32, + "ChannelReference" => 386u32, + "ChannelReferenceCode" => 387u32, + "ChannelReferenceFlag" => 388u32, + "ChannelRemittance" => 389u32, + "ChannelRemittanceFlag" => 390u32, + "ChannelRemittanceId" => 391u32, + "ChannelRoutingId" => 392u32, + "ChannelRoutingReport" => 393u32, + "ChannelRoutingSub" => 394u32, + "ChannelSettlement" => 395u32, + "ChannelSettlementSub" => 396u32, + "ChannelStatement1" => 397u32, + "ChannelStatementId" => 398u32, + "ChannelStatus1" => 399u32, + "ChannelTransactionCount" => 400u32, + "ChannelTransactionFlag" => 401u32, + "ChannelTransmissionIndicator" => 402u32, + "ChannelValueDate1" => 403u32, + "ChannelValueDateId" => 404u32, + "ChannelValueDateSub" => 405u32, + "ChannelValueDateType" => 406u32, + "ClearingAccount" => 407u32, + "ClearingAccountFlag" => 408u32, + "ClearingAdjustment" => 409u32, + "ClearingAdjustmentDate" => 410u32, + "ClearingAmountCount" => 411u32, + "ClearingAuthenticationDate" => 412u32, + "ClearingBeneficiaryId" => 413u32, + "ClearingBeneficiaryReport" => 414u32, + "ClearingClearingDate" => 415u32, + "ClearingClearingId" => 416u32, + "ClearingClearingReport" => 417u32, + "ClearingCollateral" => 418u32, + "ClearingCollateral2" => 419u32, + "ClearingCollateralCount" => 420u32, + "ClearingIdentificationType" => 421u32, + "ClearingInstructionCount" => 422u32, + "ClearingInstructionIndicator" => 423u32, + "ClearingMarginFlag" => 424u32, + "ClearingParty" => 425u32, + "ClearingPartyCount" => 426u32, + "ClearingPartyId" => 427u32, + "ClearingPartyIndicator" => 428u32, + "ClearingPosition" => 429u32, + "ClearingReconciliation" => 430u32, + "ClearingReconciliationSub" => 431u32, + "ClearingReferenceCode" => 432u32, + "ClearingReferenceSub" => 433u32, + "ClearingRemittanceReport" => 434u32, + "ClearingRouting" => 435u32, + "ClearingRoutingCount" => 436u32, + "ClearingSequenceCount" => 437u32, + "ClearingSequenceId" => 438u32, + "ClearingSettlement" => 439u32, + "ClearingStatementType" => 440u32, + "ClearingStatus2" => 441u32, + "ClearingStatusFlag" => 442u32, + "ClearingStatusId" => 443u32, + "ClearingTransactionIndicator" => 444u32, + "ClearingTransmissionCount" => 445u32, + "ClearingTransmissionReport" => 446u32, + "ClearingTransmissionType" => 447u32, + "ClearingValueDateCode" => 448u32, + "ClearingValueDateDate" => 449u32, + "CollateralAccount2" => 450u32, + "CollateralAccountCount" => 451u32, + "CollateralAdjustment2" => 452u32, + "CollateralAdjustmentDate" => 453u32, + "CollateralAdjustmentSub" => 454u32, + "CollateralAmountType" => 455u32, + "CollateralAuthentication2" => 456u32, + "CollateralAuthenticationCode" => 457u32, + "CollateralBeneficiary" => 458u32, + "CollateralBeneficiaryCode" => 459u32, + "CollateralBeneficiaryIndicator" => 460u32, + "CollateralCategory" => 461u32, + "CollateralCategory2" => 462u32, + "CollateralCategoryType" => 463u32, + "CollateralChannelCode" => 464u32, + "CollateralChannelCount" => 465u32, + "CollateralChannelDate" => 466u32, + "CollateralChannelReport" => 467u32, + "CollateralChannelSub" => 468u32, + "CollateralClearingDate" => 469u32, + "CollateralCollateralCode" => 470u32, + "CollateralCollateralCount" => 471u32, + "CollateralCollateralFlag" => 472u32, + "CollateralIdentification" => 473u32, + "CollateralInstructionCode" => 474u32, + "CollateralInstructionFlag" => 475u32, + "CollateralInstructionSub" => 476u32, + "CollateralMarginId" => 477u32, + "CollateralParty" => 478u32, + "CollateralPartyReport" => 479u32, + "CollateralPositionCount" => 480u32, + "CollateralPositionSub" => 481u32, + "CollateralPositionType" => 482u32, + "CollateralPriority2" => 483u32, + "CollateralPriorityDate" => 484u32, + "CollateralPriorityReport" => 485u32, + "CollateralReconciliation" => 486u32, + "CollateralReconciliation2" => 487u32, + "CollateralReconciliationCount" => 488u32, + "CollateralReconciliationDate" => 489u32, + "CollateralReference" => 490u32, + "CollateralReference2" => 491u32, + "CollateralReferenceType" => 492u32, + "CollateralRoutingReport" => 493u32, + "CollateralSequence2" => 494u32, + "CollateralSequenceCount" => 495u32, + "CollateralSettlementCode" => 496u32, + "CollateralSettlementDate" => 497u32, + "CollateralStatement" => 498u32, + "CollateralStatementReport" => 499u32, + "CollateralStatus" => 500u32, + "CollateralStatus2" => 501u32, + "CollateralStatusIndicator" => 502u32, + "CollateralTransaction2" => 503u32, + "CollateralTransmission2" => 504u32, + "CollateralValueDate2" => 505u32, + "CollateralValueDateCode" => 506u32, + "CollateralVersionIndicator" => 507u32, + "CollateralVersionType" => 508u32, + "CurrencyAccountId" => 509u32, + "CurrencyAccountSub" => 510u32, + "CurrencyAmountIndicator" => 511u32, + "CurrencyAuthentication" => 512u32, + "CurrencyBeneficiaryIndicator" => 513u32, + "CurrencyCategoryDate" => 514u32, + "CurrencyChannel2" => 515u32, + "CurrencyChannelCount" => 516u32, + "CurrencyChannelId" => 517u32, + "CurrencyClearing" => 518u32, + "CurrencyCollateralCount" => 519u32, + "CurrencyCollateralType" => 520u32, + "CurrencyCurrency" => 521u32, + "CurrencyCurrency1" => 522u32, + "CurrencyInstruction1" => 523u32, + "CurrencyInstructionSub" => 524u32, + "CurrencyMarginDate" => 525u32, + "CurrencyMarginId" => 526u32, + "CurrencyPartyCount" => 527u32, + "CurrencyPartyId" => 528u32, + "CurrencyPartySub" => 529u32, + "CurrencyPosition" => 530u32, + "CurrencyPositionCount" => 531u32, + "CurrencyPositionSub" => 532u32, + "CurrencyPriority" => 533u32, + "CurrencyReference2" => 534u32, + "CurrencyReferenceIndicator" => 535u32, + "CurrencyRouting" => 536u32, + "CurrencyRouting2" => 537u32, + "CurrencyRoutingId" => 538u32, + "CurrencySequenceCode" => 539u32, + "CurrencySequenceId" => 540u32, + "CurrencySequenceReport" => 541u32, + "CurrencySettlementType" => 542u32, + "CurrencyStatementSub" => 543u32, + "CurrencyStatusCode" => 544u32, + "CurrencyStatusType" => 545u32, + "CurrencyTransactionIndicator" => 546u32, + "CurrencyTransmissionDate" => 547u32, + "CurrencyValueDateIndicator" => 548u32, + "CurrencyVersionDate" => 549u32, + "IdentificationAccountIndicator" => 550u32, + "IdentificationAdjustmentCount" => 551u32, + "IdentificationAdjustmentIndicator" => 552u32, + "IdentificationAdjustmentReport" => 553u32, + "IdentificationAdjustmentType" => 554u32, + "IdentificationAmount" => 555u32, + "IdentificationAmountCount" => 556u32, + "IdentificationAmountId" => 557u32, + "IdentificationAmountSub" => 558u32, + "IdentificationAuthenticationId" => 559u32, + "IdentificationAuthenticationIndicator" => 560u32, + "IdentificationCategoryId" => 561u32, + "IdentificationClearing" => 562u32, + "IdentificationCollateral2" => 563u32, + "IdentificationCurrencyIndicator" => 564u32, + "IdentificationCurrencyType" => 565u32, + "IdentificationIdentificationCount" => 566u32, + "IdentificationIdentificationDate" => 567u32, + "IdentificationIdentificationSub" => 568u32, + "IdentificationInstructionDate" => 569u32, + "IdentificationInstructionId" => 570u32, + "IdentificationInstructionIndicator" => 571u32, + "IdentificationInstructionType" => 572u32, + "IdentificationMarginIndicator" => 573u32, + "IdentificationPartyFlag" => 574u32, + "IdentificationPartyType" => 575u32, + "IdentificationPositionType" => 576u32, + "IdentificationPriorityDate" => 577u32, + "IdentificationReconciliationCount" => 578u32, + "IdentificationReference1" => 579u32, + "IdentificationReferenceCode" => 580u32, + "IdentificationRouting" => 581u32, + "IdentificationSequence" => 582u32, + "IdentificationSequenceCode" => 583u32, + "IdentificationSequenceIndicator" => 584u32, + "IdentificationSettlement1" => 585u32, + "IdentificationSettlementId" => 586u32, + "IdentificationSettlementSub" => 587u32, + "IdentificationStatementFlag" => 588u32, + "IdentificationStatementIndicator" => 589u32, + "IdentificationStatementReport" => 590u32, + "IdentificationStatusCode" => 591u32, + "IdentificationStatusReport" => 592u32, + "IdentificationTransaction" => 593u32, + "IdentificationTransaction1" => 594u32, + "IdentificationTransactionType" => 595u32, + "IdentificationTransmission1" => 596u32, + "IdentificationValueDate" => 597u32, + "IdentificationValueDateCount" => 598u32, + "IdentificationValueDateDate" => 599u32, + "IdentificationValueDateId" => 600u32, + "IdentificationValueDateIndicator" => 601u32, + "IdentificationValueDateType" => 602u32, + "IdentificationVersion1" => 603u32, + "IdentificationVersionReport" => 604u32, + "InstructionAccount" => 605u32, + "InstructionAccountCode" => 606u32, + "InstructionAdjustment" => 607u32, + "InstructionAdjustmentId" => 608u32, + "InstructionAuthentication2" => 609u32, + "InstructionAuthenticationIndicator" => 610u32, + "InstructionBeneficiary" => 611u32, + "InstructionBeneficiaryDate" => 612u32, + "InstructionBeneficiaryReport" => 613u32, + "InstructionBeneficiaryType" => 614u32, + "InstructionCategoryCode" => 615u32, + "InstructionChannelIndicator" => 616u32, + "InstructionCurrency1" => 617u32, + "InstructionCurrencyCount" => 618u32, + "InstructionCurrencyId" => 619u32, + "InstructionIdentification" => 620u32, + "InstructionIdentificationType" => 621u32, + "InstructionInstruction2" => 622u32, + "InstructionInstructionIndicator" => 623u32, + "InstructionInstructionSub" => 624u32, + "InstructionMarginCode" => 625u32, + "InstructionParty" => 626u32, + "InstructionPositionCode" => 627u32, + "InstructionPositionType" => 628u32, + "InstructionPriority" => 629u32, + "InstructionPriorityFlag" => 630u32, + "InstructionPriorityId" => 631u32, + "InstructionReconciliation2" => 632u32, + "InstructionReconciliationType" => 633u32, + "InstructionReferenceIndicator" => 634u32, + "InstructionRemittanceCount" => 635u32, + "InstructionRemittanceId" => 636u32, + "InstructionSequenceFlag" => 637u32, + "InstructionSettlementType" => 638u32, + "InstructionStatusCode" => 639u32, + "InstructionStatusType" => 640u32, + "InstructionTransactionSub" => 641u32, + "InstructionTransmissionCode" => 642u32, + "InstructionTransmissionType" => 643u32, + "InstructionValueDate" => 644u32, + "InstructionValueDate1" => 645u32, + "InstructionValueDateIndicator" => 646u32, + "InstructionVersion2" => 647u32, + "MarginAccountIndicator" => 648u32, + "MarginAdjustmentDate" => 649u32, + "MarginAmount2" => 650u32, + "MarginAmountReport" => 651u32, + "MarginAuthentication2" => 652u32, + "MarginBeneficiary" => 653u32, + "MarginBeneficiaryReport" => 654u32, + "MarginBeneficiaryType" => 655u32, + "MarginCategory2" => 656u32, + "MarginChannel2" => 657u32, + "MarginClearing" => 658u32, + "MarginClearingCount" => 659u32, + "MarginCollateral1" => 660u32, + "MarginCurrencyCode" => 661u32, + "MarginIdentification" => 662u32, + "MarginIdentificationType" => 663u32, + "MarginInstruction1" => 664u32, + "MarginInstructionIndicator" => 665u32, + "MarginMarginDate" => 666u32, + "MarginMarginId" => 667u32, + "MarginPartyIndicator" => 668u32, + "MarginPositionCode" => 669u32, + "MarginPriorityDate" => 670u32, + "MarginPriorityFlag" => 671u32, + "MarginPrioritySub" => 672u32, + "MarginReconciliation" => 673u32, + "MarginReconciliation1" => 674u32, + "MarginReconciliationFlag" => 675u32, + "MarginReconciliationId" => 676u32, + "MarginReference1" => 677u32, + "MarginReferenceFlag" => 678u32, + "MarginReferenceId" => 679u32, + "MarginRemittance" => 680u32, + "MarginRemittanceType" => 681u32, + "MarginRouting" => 682u32, + "MarginRoutingType" => 683u32, + "MarginSequenceFlag" => 684u32, + "MarginSequenceId" => 685u32, + "MarginStatement" => 686u32, + "MarginStatementCount" => 687u32, + "MarginStatementReport" => 688u32, + "MarginStatusId" => 689u32, + "MarginTransactionCount" => 690u32, + "MarginTransactionFlag" => 691u32, + "MarginTransmissionId" => 692u32, + "MarginTransmissionSub" => 693u32, + "MarginValueDate2" => 694u32, + "PartyAccountReport" => 695u32, + "PartyAccountSub" => 696u32, + "PartyAccountType" => 697u32, + "PartyAdjustmentCode" => 698u32, + "PartyAdjustmentIndicator" => 699u32, + "PartyAdjustmentReport" => 700u32, + "PartyAmount1" => 701u32, + "PartyAmountDate" => 702u32, + "PartyAmountFlag" => 703u32, + "PartyAuthentication1" => 704u32, + "PartyCategoryCount" => 705u32, + "PartyChannel" => 706u32, + "PartyChannel2" => 707u32, + "PartyChannelSub" => 708u32, + "PartyCollateralReport" => 709u32, + "PartyCollateralSub" => 710u32, + "PartyCurrency" => 711u32, + "PartyCurrencyCode" => 712u32, + "PartyCurrencyType" => 713u32, + "PartyIdentificationIndicator" => 714u32, + "PartyInstructionDate" => 715u32, + "PartyInstructionId" => 716u32, + "PartyInstructionReport" => 717u32, + "PartyMarginType" => 718u32, + "PartyPosition2" => 719u32, + "PartyPositionId" => 720u32, + "PartyPositionType" => 721u32, + "PartyPriority" => 722u32, + "PartyPriority1" => 723u32, + "PartyPriorityCount" => 724u32, + "PartyReconciliationCode" => 725u32, + "PartyReconciliationFlag" => 726u32, + "PartyReconciliationId" => 727u32, + "PartyReferenceIndicator" => 728u32, + "PartyReferenceSub" => 729u32, + "PartyRemittanceType" => 730u32, + "PartyRouting1" => 731u32, + "PartyRouting2" => 732u32, + "PartyRoutingId" => 733u32, + "PartySequenceIndicator" => 734u32, + "PartySequenceReport" => 735u32, + "PartySettlement1" => 736u32, + "PartySettlementCount" => 737u32, + "PartyStatement" => 738u32, + "PartyStatementId" => 739u32, + "PartyStatementIndicator" => 740u32, + "PartyStatusId" => 741u32, + "PartyTransactionIndicator" => 742u32, + "PartyValueDateCount" => 743u32, + "PartyValueDateType" => 744u32, + "PartyVersion" => 745u32, + "PositionAccount" => 746u32, + "PositionAccount1" => 747u32, + "PositionAdjustmentCode" => 748u32, + "PositionAdjustmentDate" => 749u32, + "PositionAmount" => 750u32, + "PositionAmount1" => 751u32, + "PositionAmountSub" => 752u32, + "PositionAuthenticationFlag" => 753u32, + "PositionBeneficiaryFlag" => 754u32, + "PositionBeneficiaryReport" => 755u32, + "PositionCategorySub" => 756u32, + "PositionChannelFlag" => 757u32, + "PositionClearingReport" => 758u32, + "PositionClearingType" => 759u32, + "PositionCollateralCode" => 760u32, + "PositionCurrency2" => 761u32, + "PositionCurrencyReport" => 762u32, + "PositionCurrencySub" => 763u32, + "PositionIdentification1" => 764u32, + "PositionIdentificationCode" => 765u32, + "PositionInstruction" => 766u32, + "PositionInstructionReport" => 767u32, + "PositionMarginCode" => 768u32, + "PositionMarginCount" => 769u32, + "PositionMarginFlag" => 770u32, + "PositionMarginType" => 771u32, + "PositionPartyFlag" => 772u32, + "PositionPartyIndicator" => 773u32, + "PositionPositionReport" => 774u32, + "PositionPriorityFlag" => 775u32, + "PositionPriorityIndicator" => 776u32, + "PositionReconciliation1" => 777u32, + "PositionReconciliationReport" => 778u32, + "PositionReferenceSub" => 779u32, + "PositionRemittanceIndicator" => 780u32, + "PositionRoutingCount" => 781u32, + "PositionRoutingIndicator" => 782u32, + "PositionSequence" => 783u32, + "PositionSequenceCode" => 784u32, + "PositionSequenceDate" => 785u32, + "PositionSequenceIndicator" => 786u32, + "PositionStatement1" => 787u32, + "PositionStatementCount" => 788u32, + "PositionStatusSub" => 789u32, + "PositionTransmission" => 790u32, + "PositionTransmissionDate" => 791u32, + "PositionTransmissionType" => 792u32, + "PositionValueDate1" => 793u32, + "PositionValueDateFlag" => 794u32, + "PositionValueDateIndicator" => 795u32, + "PositionValueDateReport" => 796u32, + "PositionValueDateSub" => 797u32, + "PositionValueDateType" => 798u32, + "PriorityAccount1" => 799u32, + "PriorityAccountDate" => 800u32, + "PriorityAccountIndicator" => 801u32, + "PriorityAdjustment" => 802u32, + "PriorityAdjustment1" => 803u32, + "PriorityAdjustment2" => 804u32, + "PriorityAdjustmentCode" => 805u32, + "PriorityAdjustmentFlag" => 806u32, + "PriorityAdjustmentReport" => 807u32, + "PriorityAmount" => 808u32, + "PriorityAmountSub" => 809u32, + "PriorityAuthentication" => 810u32, + "PriorityAuthenticationDate" => 811u32, + "PriorityBeneficiaryCount" => 812u32, + "PriorityBeneficiaryIndicator" => 813u32, + "PriorityBeneficiaryType" => 814u32, + "PriorityChannelCount" => 815u32, + "PriorityChannelReport" => 816u32, + "PriorityChannelType" => 817u32, + "PriorityClearingType" => 818u32, + "PriorityCurrency" => 819u32, + "PriorityCurrencyId" => 820u32, + "PriorityCurrencyType" => 821u32, + "PriorityIdentificationDate" => 822u32, + "PriorityIdentificationId" => 823u32, + "PriorityInstruction2" => 824u32, + "PriorityInstructionCode" => 825u32, + "PriorityInstructionFlag" => 826u32, + "PriorityMargin2" => 827u32, + "PriorityMarginCount" => 828u32, + "PriorityParty" => 829u32, + "PriorityParty1" => 830u32, + "PriorityPartyDate" => 831u32, + "PriorityPartyFlag" => 832u32, + "PriorityPosition" => 833u32, + "PriorityPositionId" => 834u32, + "PriorityPositionType" => 835u32, + "PriorityPriority" => 836u32, + "PriorityReconciliation2" => 837u32, + "PriorityReconciliationCode" => 838u32, + "PriorityReconciliationFlag" => 839u32, + "PriorityReconciliationReport" => 840u32, + "PriorityReference1" => 841u32, + "PriorityReferenceCount" => 842u32, + "PriorityReferenceDate" => 843u32, + "PriorityReferenceId" => 844u32, + "PriorityRemittanceCode" => 845u32, + "PriorityRemittanceCount" => 846u32, + "PriorityRemittanceFlag" => 847u32, + "PriorityRoutingFlag" => 848u32, + "PrioritySequenceCount" => 849u32, + "PrioritySettlement" => 850u32, + "PriorityStatementCount" => 851u32, + "PriorityStatementFlag" => 852u32, + "PriorityStatementId" => 853u32, + "PriorityStatusId" => 854u32, + "PriorityStatusSub" => 855u32, + "PriorityTransactionCount" => 856u32, + "PriorityTransactionIndicator" => 857u32, + "PriorityTransmission" => 858u32, + "PriorityValueDate1" => 859u32, + "PriorityValueDate2" => 860u32, + "PriorityValueDateIndicator" => 861u32, + "PriorityVersion" => 862u32, + "PriorityVersionIndicator" => 863u32, + "PriorityVersionType" => 864u32, + "ReconciliationAccountReport" => 865u32, + "ReconciliationAdjustment" => 866u32, + "ReconciliationAmountCount" => 867u32, + "ReconciliationAuthenticationDate" => 868u32, + "ReconciliationBeneficiary1" => 869u32, + "ReconciliationBeneficiaryDate" => 870u32, + "ReconciliationChannel" => 871u32, + "ReconciliationChannelFlag" => 872u32, + "ReconciliationChannelSub" => 873u32, + "ReconciliationClearing1" => 874u32, + "ReconciliationClearingId" => 875u32, + "ReconciliationCurrency" => 876u32, + "ReconciliationIdentificationId" => 877u32, + "ReconciliationInstruction2" => 878u32, + "ReconciliationInstructionFlag" => 879u32, + "ReconciliationInstructionSub" => 880u32, + "ReconciliationMarginId" => 881u32, + "ReconciliationParty" => 882u32, + "ReconciliationPartyIndicator" => 883u32, + "ReconciliationPositionCount" => 884u32, + "ReconciliationPriority" => 885u32, + "ReconciliationPriority2" => 886u32, + "ReconciliationReconciliation1" => 887u32, + "ReconciliationReconciliationCount" => 888u32, + "ReconciliationReconciliationSub" => 889u32, + "ReconciliationReferenceCount" => 890u32, + "ReconciliationReferenceIndicator" => 891u32, + "ReconciliationRemittance" => 892u32, + "ReconciliationRemittanceId" => 893u32, + "ReconciliationRemittanceIndicator" => 894u32, + "ReconciliationRemittanceSub" => 895u32, + "ReconciliationSequenceCode" => 896u32, + "ReconciliationSequenceCount" => 897u32, + "ReconciliationStatusCount" => 898u32, + "ReconciliationStatusReport" => 899u32, + "ReconciliationTransactionDate" => 900u32, + "ReconciliationTransactionId" => 901u32, + "ReconciliationTransactionSub" => 902u32, + "ReconciliationTransactionType" => 903u32, + "ReconciliationTransmissionCount" => 904u32, + "ReconciliationTransmissionDate" => 905u32, + "ReconciliationValueDate2" => 906u32, + "ReconciliationValueDateType" => 907u32, + "ReconciliationVersion" => 908u32, + "ReconciliationVersionIndicator" => 909u32, + "ReconciliationVersionReport" => 910u32, + "ReferenceAccount" => 911u32, + "ReferenceAccount2" => 912u32, + "ReferenceAccountId" => 913u32, + "ReferenceAccountReport" => 914u32, + "ReferenceAmountCode" => 915u32, + "ReferenceAmountType" => 916u32, + "ReferenceAuthenticationSub" => 917u32, + "ReferenceBeneficiaryIndicator" => 918u32, + "ReferenceCategory2" => 919u32, + "ReferenceCategoryDate" => 920u32, + "ReferenceCategoryIndicator" => 921u32, + "ReferenceChannelReport" => 922u32, + "ReferenceClearing" => 923u32, + "ReferenceClearingId" => 924u32, + "ReferenceClearingType" => 925u32, + "ReferenceCollateral" => 926u32, + "ReferenceCollateral1" => 927u32, + "ReferenceCollateral2" => 928u32, + "ReferenceCurrency" => 929u32, + "ReferenceCurrencyCode" => 930u32, + "ReferenceIdentification" => 931u32, + "ReferenceIdentification1" => 932u32, + "ReferenceIdentificationCount" => 933u32, + "ReferenceIdentificationDate" => 934u32, + "ReferenceIdentificationReport" => 935u32, + "ReferenceInstructionDate" => 936u32, + "ReferenceInstructionFlag" => 937u32, + "ReferenceInstructionType" => 938u32, + "ReferenceMargin" => 939u32, + "ReferenceParty" => 940u32, + "ReferencePartySub" => 941u32, + "ReferencePositionId" => 942u32, + "ReferencePositionType" => 943u32, + "ReferencePriority" => 944u32, + "ReferencePriority1" => 945u32, + "ReferencePriorityReport" => 946u32, + "ReferenceReconciliation" => 947u32, + "ReferenceReconciliation1" => 948u32, + "ReferenceReconciliation2" => 949u32, + "ReferenceReferenceDate" => 950u32, + "ReferenceReferenceFlag" => 951u32, + "ReferenceRouting" => 952u32, + "ReferenceRoutingCount" => 953u32, + "ReferenceRoutingDate" => 954u32, + "ReferenceRoutingIndicator" => 955u32, + "ReferenceRoutingSub" => 956u32, + "ReferenceSequence" => 957u32, + "ReferenceSequenceId" => 958u32, + "ReferenceStatementDate" => 959u32, + "ReferenceTransactionDate" => 960u32, + "ReferenceTransactionSub" => 961u32, + "ReferenceTransmission" => 962u32, + "ReferenceTransmissionCode" => 963u32, + "ReferenceTransmissionCount" => 964u32, + "ReferenceValueDate2" => 965u32, + "ReferenceValueDateCode" => 966u32, + "ReferenceVersion1" => 967u32, + "ReferenceVersion2" => 968u32, + "RemittanceAccount" => 969u32, + "RemittanceAdjustmentDate" => 970u32, + "RemittanceAmountCode" => 971u32, + "RemittanceAmountDate" => 972u32, + "RemittanceAmountFlag" => 973u32, + "RemittanceAmountReport" => 974u32, + "RemittanceAuthentication" => 975u32, + "RemittanceAuthentication2" => 976u32, + "RemittanceAuthenticationCode" => 977u32, + "RemittanceAuthenticationDate" => 978u32, + "RemittanceBeneficiaryCount" => 979u32, + "RemittanceCategoryDate" => 980u32, + "RemittanceCategoryFlag" => 981u32, + "RemittanceChannel1" => 982u32, + "RemittanceClearing1" => 983u32, + "RemittanceClearingFlag" => 984u32, + "RemittanceClearingType" => 985u32, + "RemittanceCollateral" => 986u32, + "RemittanceCollateralCode" => 987u32, + "RemittanceCollateralSub" => 988u32, + "RemittanceCurrencyIndicator" => 989u32, + "RemittanceIdentificationReport" => 990u32, + "RemittanceIdentificationSub" => 991u32, + "RemittanceInstruction" => 992u32, + "RemittanceInstructionIndicator" => 993u32, + "RemittanceMarginId" => 994u32, + "RemittanceMarginSub" => 995u32, + "RemittancePositionId" => 996u32, + "RemittancePositionSub" => 997u32, + "RemittancePriorityReport" => 998u32, + "RemittanceReconciliation1" => 999u32, + "RemittanceReference" => 1000u32, + "RemittanceReference1" => 1001u32, + "RemittanceReferenceDate" => 1002u32, + "RemittanceRemittance1" => 1003u32, + "RemittanceRemittanceCount" => 1004u32, + "RemittanceRemittanceDate" => 1005u32, + "RemittanceRemittanceReport" => 1006u32, + "RemittanceRemittanceType" => 1007u32, + "RemittanceRouting" => 1008u32, + "RemittanceRouting1" => 1009u32, + "RemittanceRoutingSub" => 1010u32, + "RemittanceSequenceReport" => 1011u32, + "RemittanceSettlementFlag" => 1012u32, + "RemittanceSettlementReport" => 1013u32, + "RemittanceSettlementType" => 1014u32, + "RemittanceStatementSub" => 1015u32, + "RemittanceStatementType" => 1016u32, + "RemittanceStatus2" => 1017u32, + "RemittanceTransactionCount" => 1018u32, + "RemittanceTransactionReport" => 1019u32, + "RemittanceTransmission2" => 1020u32, + "RemittanceTransmissionCode" => 1021u32, + "RemittanceTransmissionFlag" => 1022u32, + "RemittanceValueDateCount" => 1023u32, + "RemittanceVersionDate" => 1024u32, + "RemittanceVersionFlag" => 1025u32, + "RemittanceVersionIndicator" => 1026u32, + "RemittanceVersionSub" => 1027u32, + "RoutingAccount" => 1028u32, + "RoutingAccountIndicator" => 1029u32, + "RoutingAdjustmentFlag" => 1030u32, + "RoutingAdjustmentSub" => 1031u32, + "RoutingAmountSub" => 1032u32, + "RoutingAmountType" => 1033u32, + "RoutingAuthentication" => 1034u32, + "RoutingAuthenticationFlag" => 1035u32, + "RoutingBeneficiaryDate" => 1036u32, + "RoutingBeneficiaryIndicator" => 1037u32, + "RoutingCategoryCount" => 1038u32, + "RoutingCategorySub" => 1039u32, + "RoutingChannel" => 1040u32, + "RoutingChannel2" => 1041u32, + "RoutingChannelId" => 1042u32, + "RoutingClearing" => 1043u32, + "RoutingClearingCode" => 1044u32, + "RoutingCollateralIndicator" => 1045u32, + "RoutingCurrency" => 1046u32, + "RoutingCurrencyCount" => 1047u32, + "RoutingCurrencyId" => 1048u32, + "RoutingCurrencyReport" => 1049u32, + "RoutingIdentificationSub" => 1050u32, + "RoutingInstruction" => 1051u32, + "RoutingInstructionId" => 1052u32, + "RoutingInstructionIndicator" => 1053u32, + "RoutingInstructionType" => 1054u32, + "RoutingMarginType" => 1055u32, + "RoutingParty2" => 1056u32, + "RoutingPartyCode" => 1057u32, + "RoutingPartyId" => 1058u32, + "RoutingPartyIndicator" => 1059u32, + "RoutingPartyReport" => 1060u32, + "RoutingPartyType" => 1061u32, + "RoutingPosition1" => 1062u32, + "RoutingPositionFlag" => 1063u32, + "RoutingPositionSub" => 1064u32, + "RoutingPriorityId" => 1065u32, + "RoutingReconciliation2" => 1066u32, + "RoutingReconciliationCode" => 1067u32, + "RoutingReconciliationId" => 1068u32, + "RoutingReferenceReport" => 1069u32, + "RoutingRouting" => 1070u32, + "RoutingRoutingCode" => 1071u32, + "RoutingSequenceReport" => 1072u32, + "RoutingSequenceSub" => 1073u32, + "RoutingSettlement1" => 1074u32, + "RoutingStatement2" => 1075u32, + "RoutingStatementCount" => 1076u32, + "RoutingStatementDate" => 1077u32, + "RoutingStatementId" => 1078u32, + "RoutingTransaction1" => 1079u32, + "RoutingTransactionDate" => 1080u32, + "RoutingTransactionIndicator" => 1081u32, + "RoutingTransmissionId" => 1082u32, + "RoutingTransmissionSub" => 1083u32, + "RoutingValueDate1" => 1084u32, + "RoutingVersionDate" => 1085u32, + "RoutingVersionReport" => 1086u32, + "SequenceAccount2" => 1087u32, + "SequenceAccountId" => 1088u32, + "SequenceAdjustment" => 1089u32, + "SequenceAmountCount" => 1090u32, + "SequenceAmountReport" => 1091u32, + "SequenceAuthentication2" => 1092u32, + "SequenceAuthenticationDate" => 1093u32, + "SequenceAuthenticationType" => 1094u32, + "SequenceCategoryDate" => 1095u32, + "SequenceCategorySub" => 1096u32, + "SequenceChannelDate" => 1097u32, + "SequenceClearingDate" => 1098u32, + "SequenceClearingId" => 1099u32, + "SequenceClearingIndicator" => 1100u32, + "SequenceCollateralReport" => 1101u32, + "SequenceCurrency" => 1102u32, + "SequenceIdentificationDate" => 1103u32, + "SequenceIdentificationId" => 1104u32, + "SequenceInstructionSub" => 1105u32, + "SequenceMargin" => 1106u32, + "SequenceMarginCount" => 1107u32, + "SequencePositionId" => 1108u32, + "SequencePriorityCount" => 1109u32, + "SequencePrioritySub" => 1110u32, + "SequenceReconciliationDate" => 1111u32, + "SequenceReference" => 1112u32, + "SequenceReferenceIndicator" => 1113u32, + "SequenceRemittanceCount" => 1114u32, + "SequenceRemittanceFlag" => 1115u32, + "SequenceRemittanceIndicator" => 1116u32, + "SequenceRouting2" => 1117u32, + "SequenceSettlement" => 1118u32, + "SequenceSettlementIndicator" => 1119u32, + "SequenceStatement" => 1120u32, + "SequenceStatementType" => 1121u32, + "SequenceStatusSub" => 1122u32, + "SequenceTransactionFlag" => 1123u32, + "SequenceTransactionSub" => 1124u32, + "SequenceTransactionType" => 1125u32, + "SequenceValueDateReport" => 1126u32, + "SequenceValueDateSub" => 1127u32, + "SequenceVersion2" => 1128u32, + "SequenceVersionIndicator" => 1129u32, + "SettlementAccount" => 1130u32, + "SettlementAdjustment1" => 1131u32, + "SettlementAdjustment2" => 1132u32, + "SettlementAdjustmentFlag" => 1133u32, + "SettlementAdjustmentReport" => 1134u32, + "SettlementAmount1" => 1135u32, + "SettlementAmount2" => 1136u32, + "SettlementAmountIndicator" => 1137u32, + "SettlementAuthentication2" => 1138u32, + "SettlementAuthenticationCount" => 1139u32, + "SettlementBeneficiaryFlag" => 1140u32, + "SettlementBeneficiaryType" => 1141u32, + "SettlementCategoryCode" => 1142u32, + "SettlementChannel" => 1143u32, + "SettlementChannelCode" => 1144u32, + "SettlementClearingReport" => 1145u32, + "SettlementCollateral2" => 1146u32, + "SettlementCollateralCode" => 1147u32, + "SettlementCollateralDate" => 1148u32, + "SettlementCollateralSub" => 1149u32, + "SettlementCurrency" => 1150u32, + "SettlementCurrency2" => 1151u32, + "SettlementIdentificationCount" => 1152u32, + "SettlementIdentificationIndicator" => 1153u32, + "SettlementInstruction" => 1154u32, + "SettlementInstructionDate" => 1155u32, + "SettlementMargin2" => 1156u32, + "SettlementMarginId" => 1157u32, + "SettlementMarginSub" => 1158u32, + "SettlementPartyCode" => 1159u32, + "SettlementPartyFlag" => 1160u32, + "SettlementPartyType" => 1161u32, + "SettlementPositionType" => 1162u32, + "SettlementPriorityCount" => 1163u32, + "SettlementPriorityFlag" => 1164u32, + "SettlementPriorityIndicator" => 1165u32, + "SettlementPriorityReport" => 1166u32, + "SettlementReconciliation1" => 1167u32, + "SettlementReconciliationSub" => 1168u32, + "SettlementReferenceReport" => 1169u32, + "SettlementRemittanceSub" => 1170u32, + "SettlementSequence2" => 1171u32, + "SettlementSequenceSub" => 1172u32, + "SettlementSequenceType" => 1173u32, + "SettlementSettlementId" => 1174u32, + "SettlementSettlementReport" => 1175u32, + "SettlementStatementCode" => 1176u32, + "SettlementStatus1" => 1177u32, + "SettlementTransaction" => 1178u32, + "SettlementTransactionReport" => 1179u32, + "SettlementTransmission" => 1180u32, + "SettlementValueDate" => 1181u32, + "SettlementValueDate2" => 1182u32, + "SettlementValueDateFlag" => 1183u32, + "SettlementValueDateSub" => 1184u32, + "StatementAccount2" => 1185u32, + "StatementAdjustmentDate" => 1186u32, + "StatementAdjustmentIndicator" => 1187u32, + "StatementAdjustmentReport" => 1188u32, + "StatementAdjustmentSub" => 1189u32, + "StatementAdjustmentType" => 1190u32, + "StatementAmount" => 1191u32, + "StatementAmount2" => 1192u32, + "StatementAmountCode" => 1193u32, + "StatementBeneficiaryCode" => 1194u32, + "StatementBeneficiaryFlag" => 1195u32, + "StatementBeneficiaryId" => 1196u32, + "StatementBeneficiaryType" => 1197u32, + "StatementCategory" => 1198u32, + "StatementChannel1" => 1199u32, + "StatementChannelCode" => 1200u32, + "StatementChannelId" => 1201u32, + "StatementCollateral" => 1202u32, + "StatementCollateralSub" => 1203u32, + "StatementCurrencyCode" => 1204u32, + "StatementCurrencyType" => 1205u32, + "StatementIdentification" => 1206u32, + "StatementIdentificationReport" => 1207u32, + "StatementInstructionReport" => 1208u32, + "StatementMargin1" => 1209u32, + "StatementMarginCount" => 1210u32, + "StatementMarginReport" => 1211u32, + "StatementMarginType" => 1212u32, + "StatementParty" => 1213u32, + "StatementPosition" => 1214u32, + "StatementPositionDate" => 1215u32, + "StatementReconciliationIndicator" => 1216u32, + "StatementReference2" => 1217u32, + "StatementReferenceCode" => 1218u32, + "StatementReferenceSub" => 1219u32, + "StatementRouting" => 1220u32, + "StatementRoutingCode" => 1221u32, + "StatementRoutingId" => 1222u32, + "StatementRoutingIndicator" => 1223u32, + "StatementRoutingType" => 1224u32, + "StatementSequenceCode" => 1225u32, + "StatementSequenceCount" => 1226u32, + "StatementSequenceSub" => 1227u32, + "StatementSettlementReport" => 1228u32, + "StatementStatement2" => 1229u32, + "StatementStatementId" => 1230u32, + "StatementStatus2" => 1231u32, + "StatementTransaction1" => 1232u32, + "StatementTransactionIndicator" => 1233u32, + "StatementTransactionSub" => 1234u32, + "StatementTransmissionReport" => 1235u32, + "StatementVersion1" => 1236u32, + "StatementVersionCount" => 1237u32, + "StatementVersionDate" => 1238u32, + "StatementVersionType" => 1239u32, + "StatusAccountId" => 1240u32, + "StatusAccountIndicator" => 1241u32, + "StatusAccountType" => 1242u32, + "StatusAdjustmentCode" => 1243u32, + "StatusAdjustmentDate" => 1244u32, + "StatusAdjustmentFlag" => 1245u32, + "StatusAmount" => 1246u32, + "StatusAmount1" => 1247u32, + "StatusAmountCode" => 1248u32, + "StatusAmountDate" => 1249u32, + "StatusAmountReport" => 1250u32, + "StatusAuthenticationFlag" => 1251u32, + "StatusAuthenticationReport" => 1252u32, + "StatusBeneficiary" => 1253u32, + "StatusCategory" => 1254u32, + "StatusCategory2" => 1255u32, + "StatusChannel" => 1256u32, + "StatusChannel1" => 1257u32, + "StatusChannelFlag" => 1258u32, + "StatusChannelType" => 1259u32, + "StatusClearing" => 1260u32, + "StatusCollateral2" => 1261u32, + "StatusCollateralCount" => 1262u32, + "StatusCollateralDate" => 1263u32, + "StatusCollateralFlag" => 1264u32, + "StatusCollateralSub" => 1265u32, + "StatusCurrencyDate" => 1266u32, + "StatusCurrencySub" => 1267u32, + "StatusCurrencyType" => 1268u32, + "StatusIdentificationId" => 1269u32, + "StatusInstructionCount" => 1270u32, + "StatusInstructionIndicator" => 1271u32, + "StatusMargin2" => 1272u32, + "StatusMarginCode" => 1273u32, + "StatusMarginId" => 1274u32, + "StatusPartyReport" => 1275u32, + "StatusPositionCount" => 1276u32, + "StatusPositionFlag" => 1277u32, + "StatusPositionIndicator" => 1278u32, + "StatusPriority" => 1279u32, + "StatusPriorityReport" => 1280u32, + "StatusReconciliationDate" => 1281u32, + "StatusReconciliationId" => 1282u32, + "StatusReconciliationType" => 1283u32, + "StatusRemittanceCount" => 1284u32, + "StatusRemittanceDate" => 1285u32, + "StatusRouting" => 1286u32, + "StatusSequenceCode" => 1287u32, + "StatusSettlementIndicator" => 1288u32, + "StatusTransactionCode" => 1289u32, + "StatusTransactionCount" => 1290u32, + "StatusTransactionFlag" => 1291u32, + "StatusTransactionReport" => 1292u32, + "StatusTransmissionCount" => 1293u32, + "StatusTransmissionType" => 1294u32, + "StatusValueDate2" => 1295u32, + "StatusValueDateCode" => 1296u32, + "StatusValueDateCount" => 1297u32, + "StatusVersion1" => 1298u32, + "TransactionAccount" => 1299u32, + "TransactionAccountCount" => 1300u32, + "TransactionAdjustment1" => 1301u32, + "TransactionAdjustmentCode" => 1302u32, + "TransactionAdjustmentFlag" => 1303u32, + "TransactionAmount" => 1304u32, + "TransactionAmountCount" => 1305u32, + "TransactionAmountIndicator" => 1306u32, + "TransactionAuthenticationIndicator" => 1307u32, + "TransactionBeneficiary2" => 1308u32, + "TransactionBeneficiarySub" => 1309u32, + "TransactionCategoryDate" => 1310u32, + "TransactionCategoryFlag" => 1311u32, + "TransactionChannelSub" => 1312u32, + "TransactionChannelType" => 1313u32, + "TransactionClearing" => 1314u32, + "TransactionClearingReport" => 1315u32, + "TransactionClearingSub" => 1316u32, + "TransactionCollateral" => 1317u32, + "TransactionCollateralFlag" => 1318u32, + "TransactionCollateralId" => 1319u32, + "TransactionIdentificationIndicator" => 1320u32, + "TransactionInstruction" => 1321u32, + "TransactionInstructionCode" => 1322u32, + "TransactionInstructionSub" => 1323u32, + "TransactionMargin2" => 1324u32, + "TransactionMarginCount" => 1325u32, + "TransactionPosition2" => 1326u32, + "TransactionPositionIndicator" => 1327u32, + "TransactionPriorityId" => 1328u32, + "TransactionReconciliationCount" => 1329u32, + "TransactionRemittanceFlag" => 1330u32, + "TransactionRemittanceReport" => 1331u32, + "TransactionRemittanceType" => 1332u32, + "TransactionRoutingFlag" => 1333u32, + "TransactionSequenceFlag" => 1334u32, + "TransactionSequenceId" => 1335u32, + "TransactionSequenceSub" => 1336u32, + "TransactionSequenceType" => 1337u32, + "TransactionSettlementDate" => 1338u32, + "TransactionSettlementFlag" => 1339u32, + "TransactionStatement" => 1340u32, + "TransactionStatementIndicator" => 1341u32, + "TransactionStatusType" => 1342u32, + "TransactionTransactionDate" => 1343u32, + "TransactionTransmissionCount" => 1344u32, + "TransactionVersionCode" => 1345u32, + "TransactionVersionType" => 1346u32, + "TransmissionAdjustment1" => 1347u32, + "TransmissionAdjustmentDate" => 1348u32, + "TransmissionAdjustmentFlag" => 1349u32, + "TransmissionAdjustmentType" => 1350u32, + "TransmissionCategory" => 1351u32, + "TransmissionCategoryFlag" => 1352u32, + "TransmissionCategoryId" => 1353u32, + "TransmissionChannel" => 1354u32, + "TransmissionClearingId" => 1355u32, + "TransmissionCollateralDate" => 1356u32, + "TransmissionCurrencyIndicator" => 1357u32, + "TransmissionCurrencyReport" => 1358u32, + "TransmissionIdentification1" => 1359u32, + "TransmissionInstruction" => 1360u32, + "TransmissionInstruction2" => 1361u32, + "TransmissionInstructionFlag" => 1362u32, + "TransmissionInstructionType" => 1363u32, + "TransmissionMargin2" => 1364u32, + "TransmissionMarginDate" => 1365u32, + "TransmissionPartyCode" => 1366u32, + "TransmissionPartyDate" => 1367u32, + "TransmissionPositionReport" => 1368u32, + "TransmissionReconciliation2" => 1369u32, + "TransmissionReconciliationSub" => 1370u32, + "TransmissionReference1" => 1371u32, + "TransmissionReferenceIndicator" => 1372u32, + "TransmissionRemittanceDate" => 1373u32, + "TransmissionRemittanceReport" => 1374u32, + "TransmissionRoutingCount" => 1375u32, + "TransmissionRoutingSub" => 1376u32, + "TransmissionSequenceDate" => 1377u32, + "TransmissionSettlementCount" => 1378u32, + "TransmissionSettlementFlag" => 1379u32, + "TransmissionStatement" => 1380u32, + "TransmissionStatement2" => 1381u32, + "TransmissionStatementCount" => 1382u32, + "TransmissionStatusCode" => 1383u32, + "TransmissionTransaction" => 1384u32, + "TransmissionTransmissionDate" => 1385u32, + "TransmissionValueDate" => 1386u32, + "TransmissionValueDateId" => 1387u32, + "TransmissionValueDateIndicator" => 1388u32, + "ValueDateAccount2" => 1389u32, + "ValueDateAccountCount" => 1390u32, + "ValueDateAdjustment" => 1391u32, + "ValueDateAdjustmentCount" => 1392u32, + "ValueDateAdjustmentId" => 1393u32, + "ValueDateAdjustmentReport" => 1394u32, + "ValueDateAdjustmentType" => 1395u32, + "ValueDateAmountCode" => 1396u32, + "ValueDateAmountCount" => 1397u32, + "ValueDateBeneficiary" => 1398u32, + "ValueDateBeneficiarySub" => 1399u32, + "ValueDateClearingDate" => 1400u32, + "ValueDateCurrency" => 1401u32, + "ValueDateIdentification2" => 1402u32, + "ValueDateIdentificationFlag" => 1403u32, + "ValueDateIdentificationIndicator" => 1404u32, + "ValueDateIdentificationReport" => 1405u32, + "ValueDateMargin" => 1406u32, + "ValueDatePartyId" => 1407u32, + "ValueDatePosition" => 1408u32, + "ValueDatePositionType" => 1409u32, + "ValueDatePriority" => 1410u32, + "ValueDatePriority2" => 1411u32, + "ValueDatePriorityCount" => 1412u32, + "ValueDatePriorityReport" => 1413u32, + "ValueDateReconciliationIndicator" => 1414u32, + "ValueDateReferenceType" => 1415u32, + "ValueDateRemittance2" => 1416u32, + "ValueDateRemittanceReport" => 1417u32, + "ValueDateRoutingReport" => 1418u32, + "ValueDateSequence" => 1419u32, + "ValueDateSequence1" => 1420u32, + "ValueDateSequenceCount" => 1421u32, + "ValueDateSequenceType" => 1422u32, + "ValueDateSettlement2" => 1423u32, + "ValueDateSettlementCount" => 1424u32, + "ValueDateStatementCode" => 1425u32, + "ValueDateStatementReport" => 1426u32, + "ValueDateStatementSub" => 1427u32, + "ValueDateStatusCode" => 1428u32, + "ValueDateStatusType" => 1429u32, + "ValueDateTransaction2" => 1430u32, + "ValueDateTransactionCount" => 1431u32, + "ValueDateTransactionId" => 1432u32, + "ValueDateTransactionReport" => 1433u32, + "ValueDateValueDateCode" => 1434u32, + "ValueDateValueDateIndicator" => 1435u32, + "ValueDateValueDateReport" => 1436u32, + "ValueDateVersionFlag" => 1437u32, + "ValueDateVersionSub" => 1438u32, + "VersionAccount" => 1439u32, + "VersionAccountCode" => 1440u32, + "VersionAccountCount" => 1441u32, + "VersionAccountIndicator" => 1442u32, + "VersionAdjustmentIndicator" => 1443u32, + "VersionAdjustmentSub" => 1444u32, + "VersionAmount" => 1445u32, + "VersionAmount2" => 1446u32, + "VersionAmountCode" => 1447u32, + "VersionAuthentication1" => 1448u32, + "VersionAuthenticationCode" => 1449u32, + "VersionBeneficiary1" => 1450u32, + "VersionBeneficiary2" => 1451u32, + "VersionBeneficiaryIndicator" => 1452u32, + "VersionCategory2" => 1453u32, + "VersionChannel" => 1454u32, + "VersionChannelDate" => 1455u32, + "VersionChannelFlag" => 1456u32, + "VersionClearing" => 1457u32, + "VersionClearingCode" => 1458u32, + "VersionClearingCount" => 1459u32, + "VersionClearingFlag" => 1460u32, + "VersionClearingSub" => 1461u32, + "VersionCollateralFlag" => 1462u32, + "VersionCurrencyCode" => 1463u32, + "VersionCurrencyFlag" => 1464u32, + "VersionCurrencyIndicator" => 1465u32, + "VersionIdentificationCount" => 1466u32, + "VersionIdentificationDate" => 1467u32, + "VersionIdentificationFlag" => 1468u32, + "VersionIdentificationIndicator" => 1469u32, + "VersionInstructionFlag" => 1470u32, + "VersionMargin1" => 1471u32, + "VersionMarginId" => 1472u32, + "VersionPartyId" => 1473u32, + "VersionPosition1" => 1474u32, + "VersionPosition2" => 1475u32, + "VersionPositionReport" => 1476u32, + "VersionPriority2" => 1477u32, + "VersionPriorityCode" => 1478u32, + "VersionReconciliationCount" => 1479u32, + "VersionReconciliationId" => 1480u32, + "VersionReconciliationSub" => 1481u32, + "VersionReference2" => 1482u32, + "VersionReferenceCode" => 1483u32, + "VersionRemittanceIndicator" => 1484u32, + "VersionRoutingReport" => 1485u32, + "VersionSequence2" => 1486u32, + "VersionSequenceFlag" => 1487u32, + "VersionSequenceType" => 1488u32, + "VersionSettlement" => 1489u32, + "VersionSettlementType" => 1490u32, + "VersionStatement" => 1491u32, + "VersionStatementFlag" => 1492u32, + "VersionStatementId" => 1493u32, + "VersionStatusIndicator" => 1494u32, + "VersionTransactionCode" => 1495u32, + "VersionTransactionCount" => 1496u32, + "VersionTransmissionType" => 1497u32, + "VersionValueDateCount" => 1498u32, + "VersionValueDateFlag" => 1499u32, +}; diff --git a/crates/polyxml-core/benches/tag_dispatch.rs b/crates/polyxml-core/benches/tag_dispatch.rs new file mode 100644 index 00000000..f9b56339 --- /dev/null +++ b/crates/polyxml-core/benches/tag_dispatch.rs @@ -0,0 +1,119 @@ +//! Issue #49: tag-dispatch strategy comparison at schema scale tiers. +//! +//! Baselines per the issue's benchmark setup: +//! 1. Standard Rust `match` on string literals (LLVM bucketed memcmp chains) +//! 2. Runtime `HashMap<&str, u32>` +//! 3. Compile-time `phf::Map<&str, u32>` +//! +//! Each iteration sweeps the full tier (hit path for every tag); Criterion's +//! `Throughput::Elements` reports tags/second directly. Tokenization is +//! intentionally isolated OUT of the bench: it is identical across dispatch +//! strategies, so only the lookup is measured (see +//! docs/benchmarks/rust-phf-dispatch.md). +//! +//! Regenerate the tag tables with scripts/gen_tag_dispatch_fixtures.py. + +use std::collections::HashMap; +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, Criterion, Throughput}; + +#[path = "support/tag_dispatch_fixtures.rs"] +mod fixtures; + +fn bench_tier( + c: &mut Criterion, + tier: &str, + tags: &[&'static str], + match_fn: fn(&str) -> Option, + phf_map: &'static phf::Map<&'static str, u32>, +) { + let hashmap: HashMap<&'static str, u32> = tags + .iter() + .enumerate() + .map(|(i, tag)| (*tag, i as u32)) + .collect(); + // A tag present in no strategy: the miss path matters for unknown children. + let miss = format!("Unknown{tier}Sentinel"); + + let mut group = c.benchmark_group(format!("tag_dispatch/{tier}")); + group.throughput(Throughput::Elements(tags.len() as u64)); + + group.bench_function("match_hit", |b| { + b.iter(|| { + let mut acc = 0u64; + for tag in tags { + acc += u64::from(match_fn(black_box(tag)).unwrap_or(u32::MAX)); + } + black_box(acc); + }); + }); + group.bench_function("match_miss", |b| { + let miss = black_box(miss.as_str()); + b.iter(|| black_box(match_fn(miss))); + }); + + group.bench_function("hashmap_hit", |b| { + b.iter(|| { + let mut acc = 0u64; + for tag in tags { + acc += u64::from(*hashmap.get(black_box(tag)).unwrap_or(&u32::MAX)); + } + black_box(acc); + }); + }); + group.bench_function("hashmap_miss", |b| { + let miss = black_box(miss.as_str()); + b.iter(|| black_box(hashmap.get(miss))); + }); + + group.bench_function("phf_hit", |b| { + b.iter(|| { + let mut acc = 0u64; + for tag in tags { + acc += u64::from(*phf_map.get(black_box(tag)).unwrap_or(&u32::MAX)); + } + black_box(acc); + }); + }); + group.bench_function("phf_miss", |b| { + let miss = black_box(miss.as_str()); + b.iter(|| black_box(phf_map.get(miss))); + }); + + group.finish(); +} + +fn tag_dispatch(c: &mut Criterion) { + bench_tier( + c, + "small_16", + &fixtures::TAGS_16, + fixtures::match_dispatch_16, + &fixtures::PHF_16, + ); + bench_tier( + c, + "medium_120", + &fixtures::TAGS_120, + fixtures::match_dispatch_120, + &fixtures::PHF_120, + ); + bench_tier( + c, + "large_600", + &fixtures::TAGS_600, + fixtures::match_dispatch_600, + &fixtures::PHF_600, + ); + bench_tier( + c, + "large_1500", + &fixtures::TAGS_1500, + fixtures::match_dispatch_1500, + &fixtures::PHF_1500, + ); +} + +criterion_group!(benches, tag_dispatch); +criterion_main!(benches); diff --git a/crates/polyxml-core/src/codegen/rust/mod.rs b/crates/polyxml-core/src/codegen/rust/mod.rs index 51fe6341..eb2ca984 100644 --- a/crates/polyxml-core/src/codegen/rust/mod.rs +++ b/crates/polyxml-core/src/codegen/rust/mod.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeSet, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use std::fmt::Write as FmtWrite; use heck::{AsPascalCase, AsSnakeCase}; @@ -30,6 +30,10 @@ pub struct RustOptions { pub emit_codecs: bool, /// Derive rkyv::{Archive, Serialize, Deserialize} zero-copy wire format serialization (default: false). pub emit_rkyv: bool, + /// Emit compile-time perfect-hash (phf) element-tag dispatch instead of a + /// string `match` (default: false). Generated crates must depend on `phf`. + #[serde(default)] + pub phf: bool, /// Custom header text to prepend to generated files (default: None). pub custom_header: Option, } @@ -44,6 +48,7 @@ impl Default for RustOptions { emit_root_aliases: true, emit_codecs: true, emit_rkyv: false, + phf: false, custom_header: None, } } @@ -1056,6 +1061,72 @@ impl RustCodegen { format!("impl {}", struct_name) }; + // --feature phf: emit a module-level perfect-hash dispatch table (enum + // + static, built with phf_codegen) ahead of the impl and route both + // child-event match sites through it. Fully gated so default output + // stays byte-identical. + let phf_enum_name = format!("__{}ElementId", struct_name); + let phf_static_name = format!("__{}_ELEMENT_DISPATCH", struct_name.to_uppercase()); + let mut phf_arms: Vec<(String, String, Vec)> = Vec::new(); + if self.options.phf { + let mut seen_fields = HashSet::new(); + let mut seen_variants = HashSet::new(); + for field in flatten_fields(s, ir) { + // Uniquify over EVERY field (attrs/text included) so names stay + // in lockstep with field_metas and struct-field emission; a + // non-element sharing a name with a later element must still + // consume its slot or dispatch lookups miss and fall back to a + // bare-string arm inside the Option match (won't compile). + let rust_name = self.unique_rust_field_name(&field.name, &mut seen_fields); + if field.kind != FieldKind::Element { + continue; + } + let tags: Vec = match self.resolve_union_def(&field.type_ref, ir) { + Some(u) => u.branches.iter().map(|b| b.xml_name.clone()).collect(), + None => vec![field.xml_name.clone()], + }; + let base_variant = to_rust_variant_identifier(rust_name.trim_start_matches("r#")); + let mut variant = base_variant.clone(); + let mut n = 2; + while !seen_variants.insert(variant.clone()) { + variant = format!("{}{}", base_variant, n); + n += 1; + } + phf_arms.push((rust_name, variant, tags)); + } + if !phf_arms.is_empty() { + out.push('\n'); + let _ = writeln!( + out, + "/// Perfect-hash element dispatch id for `{struct_name}` (`--feature phf`)." + ); + out.push_str("#[derive(Clone, Copy)]\n"); + let _ = writeln!(out, "enum {} {{", phf_enum_name); + for (_, variant, _) in &phf_arms { + let _ = writeln!(out, " {},", variant); + } + out.push_str("}\n\n"); + let mut map = phf_codegen::Map::new(); + for (_, variant, tags) in &phf_arms { + for tag in tags { + map.entry(tag.as_str(), format!("{}::{}", phf_enum_name, variant)); + } + } + let _ = writeln!( + out, + "static {}: ::phf::Map<&'static str, {}> = {};", + phf_static_name, + phf_enum_name, + map.build() + ); + out.push('\n'); + } + } + let phf_variant_of: HashMap<&str, &str> = phf_arms + .iter() + .map(|(rust_name, variant, _)| (rust_name.as_str(), variant.as_str())) + .collect(); + out.push('\n'); let _ = writeln!(out, "{} {{", impl_header); @@ -1154,7 +1225,17 @@ impl RustCodegen { if !elem_fields.is_empty() { out.push_str("\n loop {\n"); out.push_str(" match reader.read_event()? {\n"); - out.push_str(" Event::Start(e) => match e.local_name().as_ref() {\n"); + if phf_variant_of.is_empty() { + out.push_str( + " Event::Start(e) => match e.local_name().as_ref() {\n", + ); + } else { + let _ = writeln!( + out, + " Event::Start(e) => match {}.get(e.local_name().as_ref()) {{", + phf_static_name + ); + } for meta in &elem_fields { if let Some(union_def) = self.resolve_union_def(&meta.field.type_ref, ir) { @@ -1163,7 +1244,16 @@ impl RustCodegen { .iter() .map(|b| format!("\"{}\"", b.xml_name)) .collect(); - let _ = writeln!(out, " {} => {{", branch_tags.join(" | ")); + if let Some(variant) = phf_variant_of.get(meta.rust_name.as_str()) { + let _ = writeln!( + out, + " Some(&{}::{}) => {{", + phf_enum_name, variant + ); + } else { + let _ = + writeln!(out, " {} => {{", branch_tags.join(" | ")); + } let union_name = type_ident(&union_def.qname); let _ = writeln!( out, @@ -1186,7 +1276,16 @@ impl RustCodegen { } out.push_str(" }\n"); } else { - let _ = writeln!(out, " \"{}\" => {{", meta.field.xml_name); + if let Some(variant) = phf_variant_of.get(meta.rust_name.as_str()) { + let _ = writeln!( + out, + " Some(&{}::{}) => {{", + phf_enum_name, variant + ); + } else { + let _ = + writeln!(out, " \"{}\" => {{", meta.field.xml_name); + } self.emit_element_parse( out, &meta.field, @@ -1203,7 +1302,17 @@ impl RustCodegen { out.push_str(" }\n"); out.push_str(" },\n"); - out.push_str(" Event::Empty(e) => match e.local_name().as_ref() {\n"); + if phf_variant_of.is_empty() { + out.push_str( + " Event::Empty(e) => match e.local_name().as_ref() {\n", + ); + } else { + let _ = writeln!( + out, + " Event::Empty(e) => match {}.get(e.local_name().as_ref()) {{", + phf_static_name + ); + } for meta in &elem_fields { if let Some(union_def) = self.resolve_union_def(&meta.field.type_ref, ir) { let branch_tags: Vec<_> = union_def @@ -1211,7 +1320,16 @@ impl RustCodegen { .iter() .map(|b| format!("\"{}\"", b.xml_name)) .collect(); - let _ = writeln!(out, " {} => {{", branch_tags.join(" | ")); + if let Some(variant) = phf_variant_of.get(meta.rust_name.as_str()) { + let _ = writeln!( + out, + " Some(&{}::{}) => {{", + phf_enum_name, variant + ); + } else { + let _ = + writeln!(out, " {} => {{", branch_tags.join(" | ")); + } let union_name = type_ident(&union_def.qname); let _ = writeln!( out, @@ -1234,7 +1352,16 @@ impl RustCodegen { } out.push_str(" }\n"); } else { - let _ = writeln!(out, " \"{}\" => {{", meta.field.xml_name); + if let Some(variant) = phf_variant_of.get(meta.rust_name.as_str()) { + let _ = writeln!( + out, + " Some(&{}::{}) => {{", + phf_enum_name, variant + ); + } else { + let _ = + writeln!(out, " \"{}\" => {{", meta.field.xml_name); + } self.emit_empty_element_parse( out, &meta.field, diff --git a/crates/polyxml-core/tests/test_rust_codegen.rs b/crates/polyxml-core/tests/test_rust_codegen.rs index 57b55b03..c02e8469 100644 --- a/crates/polyxml-core/tests/test_rust_codegen.rs +++ b/crates/polyxml-core/tests/test_rust_codegen.rs @@ -149,6 +149,7 @@ fn test_rust_zero_copy_codegen() { emit_root_aliases: true, emit_codecs: false, emit_rkyv: false, + phf: false, custom_header: None, }); @@ -201,6 +202,7 @@ fn test_rust_owned_codegen() { emit_root_aliases: true, emit_codecs: false, emit_rkyv: false, + phf: false, custom_header: None, }); @@ -956,3 +958,175 @@ fn test_rust_read_element_text_accumulates_refs_and_cdata() { } } } + +/// `--feature phf` swaps both child-event match sites for a phf_codegen-built +/// perfect-hash table plus a per-struct id enum; the default must stay a plain +/// string `match` (preserve-defaults invariant). +#[test] +fn test_rust_phf_dispatch_emission() { + let ir = XsdParser::new() + .parse_str(extension_xsd()) + .expect("parse extension schema"); + let code = RustCodegen::new(RustOptions { + phf: true, + ..Default::default() + }) + .generate_module(&ir); + + // Per-struct id enum + phf_codegen table ahead of each impl. + let base_enum = section(&code, "enum __BaseElementId"); + assert!(base_enum.contains("Label,"), "{base_enum}"); + assert!(code.contains( + "static __BASE_ELEMENT_DISPATCH: ::phf::Map<&'static str, __BaseElementId> = ::phf::Map {" + )); + assert!(code.contains("(\"label\", __BaseElementId::Label)")); + + // Inherited element fields land in the derived struct's table too. + assert!(code.contains( + "static __LEAF_ELEMENT_DISPATCH: ::phf::Map<&'static str, __LeafElementId> = ::phf::Map {" + )); + assert!(code.contains("(\"rank\", __LeafElementId::Rank)")); + assert!(code.contains("(\"note\", __LeafElementId::Note)")); + + // Both event sites route through the table: 3 structs x (Start + Empty). + assert_eq!(code.matches(".get(e.local_name().as_ref())").count(), 6); + assert!(code.contains("Some(&__BaseElementId::Label) => {")); + assert!(code.contains("Some(&__LeafElementId::Note) => {")); + + // Default output is untouched by the feature. + let default_code = RustCodegen::new(RustOptions::default()).generate_module(&ir); + assert!(!default_code.contains("ELEMENT_DISPATCH")); + assert!(!default_code.contains("::phf::")); + assert!(default_code.contains("Event::Start(e) => match e.local_name().as_ref() {")); +} + +/// Union-typed fields group every branch tag onto ONE dispatch variant. +#[test] +fn test_rust_phf_dispatch_union_groups_branch_tags() { + let mut ir = SchemaIR::new(); + + ir.add_type(TypeDef::Struct(StructDef { + qname: QName::local("Card"), + base_type: None, + is_abstract: false, + fields: vec![FieldDef::new( + "number", + "number", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::String), + )], + documentation: None, + })); + ir.add_type(TypeDef::Struct(StructDef { + qname: QName::local("Cash"), + base_type: None, + is_abstract: false, + fields: vec![FieldDef::new( + "amount", + "amount", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::Decimal), + )], + documentation: None, + })); + ir.add_type(TypeDef::Union(UnionDef { + qname: QName::local("PaymentMethod"), + branches: vec![ + UnionBranch { + variant_name: "Card".into(), + xml_name: "card".into(), + namespace: None, + type_ref: TypeRef::Named(QName::local("Card")), + documentation: None, + }, + UnionBranch { + variant_name: "Cash".into(), + xml_name: "cash".into(), + namespace: None, + type_ref: TypeRef::Named(QName::local("Cash")), + documentation: None, + }, + ], + documentation: None, + })); + ir.add_type(TypeDef::Struct(StructDef { + qname: QName::local("Order"), + base_type: None, + is_abstract: false, + fields: vec![FieldDef::new( + "method", + "method", + FieldKind::Element, + TypeRef::Named(QName::local("PaymentMethod")), + )], + documentation: None, + })); + + let code = RustCodegen::new(RustOptions { + phf: true, + ..Default::default() + }) + .generate_module(&ir); + + assert!(code.contains("(\"card\", __OrderElementId::Method)")); + assert!(code.contains("(\"cash\", __OrderElementId::Method)")); + assert_eq!( + code.matches("Some(&__OrderElementId::Method) => {").count(), + 2 + ); +} + +/// Attribute and element sharing one XML name: the element's rust name +/// uniquifies to `code_2` everywhere (struct decl, `var_` bindings, dispatch +/// table entry, and match arm must all agree — a bare-string arm inside the +/// `Option` scrutinee would not compile). +#[test] +fn test_rust_phf_dispatch_uniquifies_like_field_metas() { + let mut ir = SchemaIR::new(); + ir.add_type(TypeDef::Struct(StructDef { + qname: QName::local("Collision"), + base_type: None, + is_abstract: false, + fields: vec![ + FieldDef::new( + "code", + "code", + FieldKind::Attribute, + TypeRef::Primitive(PrimitiveType::String), + ), + FieldDef::new( + "code", + "code", + FieldKind::Element, + TypeRef::Primitive(PrimitiveType::String), + ), + ], + documentation: None, + })); + + let code = RustCodegen::new(RustOptions { + phf: true, + ..Default::default() + }) + .generate_module(&ir); + + assert!( + code.contains("pub code_2:"), + "element code_2 missing from struct:\n{code}" + ); + assert!( + code.contains("(\"code\", __CollisionElementId::Code2)"), + "table entry not keyed on the uniquified name:\n{code}" + ); + assert!( + code.contains("Some(&__CollisionElementId::Code2) => {"), + "arm missing or mismatched with the table:\n{code}" + ); + let start = code + .find("Event::Start(e) => match __COLLISION_ELEMENT_DISPATCH") + .expect("phf scrutinee missing"); + assert!( + !code[start..].contains(" \"code\" => {"), + "bare-string arm leaked into the Option match:\n{code}" + ); +} diff --git a/docs/benchmarks.md b/docs/benchmarks.md index 4df776bf..cd25a60b 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -99,6 +99,14 @@ When storing XML dataclasses and Pydantic models in embedded transactional key-v The benchmark suite is reusable and version-controlled. +> 🛡️ **Memory safety**: the suite entry points below run under +> [`scripts/memcap.sh`](https://github.com/nth-bailey/PolyXML/blob/main/scripts/memcap.sh), +> which caps each run at 60% of available RAM in an isolated cgroup (kernel +> OOM-kills only the runaway build; the host stays responsive — `ulimit` +> fallback where systemd is unavailable). Wrap any ad-hoc +> `cargo bench --release` the same way; tune with `POLYXML_MEMCAP_PCT`, opt +> out with `POLYXML_MEMCAP_DISABLE=1`. + ### Run All Benchmarks (Rust + Python) ```bash @@ -109,6 +117,7 @@ The benchmark suite is reusable and version-controlled. ```bash cargo bench --bench core_benchmarks +cargo bench --bench tag_dispatch # dispatch strategy suite: docs/benchmarks/rust-phf-dispatch.md ``` ### Run Python Comparative Benchmarks CLI diff --git a/docs/benchmarks/rust-phf-dispatch.md b/docs/benchmarks/rust-phf-dispatch.md new file mode 100644 index 00000000..cd566997 --- /dev/null +++ b/docs/benchmarks/rust-phf-dispatch.md @@ -0,0 +1,282 @@ +# Rust Tag Dispatch: `match` vs `HashMap` vs `phf` (issue #49) + +> Status: benchmark results below are produced on a Linux x86-64 host with a +> functional PMU (`perf_event_paranoid = 2`). Commands are reproducible with +> stock tooling; see [Reproducing](#reproducing). + +## What changed + +Rust codegen gained an opt-in **`--feature phf`** flag that swaps the generated +child-element dispatch from a linear string `match` to a compile-time perfect +hash table built with `phf_codegen`: + +```bash +polyxml generate schema.xsd --lang rust --feature phf +# or in polyxml.toml: +# [[generate]] +# target = "rust" +# features = ["phf"] +``` + +> **Note on flag shape**: issue #49 originally proposed a standalone `--phf` +> flag; per the issue #50 unification decision (see the +> `polyxml-codegen-workflow` skill §9), all Rust opt-ins are exposed through the +> repeatable `--feature` array instead. `--feature phf` is the supported form. + +For every struct with element children the generator emits: + +```rust +/// Perfect-hash element dispatch id for `Address` (`--feature phf`). +#[derive(Clone, Copy)] +enum __AddressElementId { Street, City, ... } + +static __ADDRESS_ELEMENT_DISPATCH: ::phf::Map<&'static str, __AddressElementId> = ::phf::Map { + key: ..., disps: &[...], entries: &[("city", __AddressElementId::City), ...], +}; +``` + +and both `Event::Start` and `Event::Empty` arm dispatches become +`__ADDRESS_ELEMENT_DISPATCH.get(e.local_name().as_ref())` lookups. Union-typed +fields map every branch tag onto one variant. Default output (no feature) is +byte-identical to before — locked by `test_rust_phf_dispatch_emission`. + +Consuming crates must add `phf = "0.14"` to their `Cargo.toml`. + +## Methodology + +* **Framework**: Criterion.rs (`benches/tag_dispatch.rs`, issue acceptance + criterion #2). +* **Scale tiers** (issue-defined): small **16**, medium **120**, large **600** + elements, plus an extra-large **1500** tier added here so the + `match`→`phf` crossover is *measured* directly rather than extrapolated + (the issue's hypothesis put the break-even at 50 elements — see + [Auto-threshold](#auto-threshold-recommendation)). Tags are seeded + two-word pseudo-ISO-20022 vocabulary + (`scripts/gen_tag_dispatch_fixtures.py`, 10-45 byte names) so LLVM's + length-bucketed memcmp chains are neither favored nor gamed. +* **Baselines** (issue-defined): + 1. `match tag { "..." => ... }` — LLVM lowers these to length-bucketed + vectorized/memcmp chains, not jump tables (byte-array keys can't jump-table). + 2. Runtime `HashMap<&str, u32>` (SipHash-1-3, std default). + 3. Compile-time `phf::Map<&str, u32>`. +* **Hit and miss paths** are measured separately per strategy + (`*_hit` = full-tier sweep, throughput reported in tags/s; + `*_miss` = lookup of an unknown sentinel tag). +* **Tokenization is intentionally excluded**: XML tokenization cost is + identical across dispatch strategies, so only the lookup — the part the + feature changes — is measured. Issue metric "1,000,000 tags" maps to + tags/s × elapsed time below. + +## Results — throughput (Criterion) + +Host: AMD Ryzen 5 4500, Linux x86-64 (WSL2), rustc 1.98.1, Criterion 0.8. +All times are per full-tier sweep unless noted; throughput is tags/second as +reported by `Throughput::Elements`. + +### Hit path (lookup of a tag present in the tier) + +| Tier | `match` hit | hit thrpt | `HashMap` hit | hit thrpt | `phf` hit | hit thrpt | +| :--- | ---: | ---: | ---: | ---: | ---: | ---: | +| small (16) | **52.1 ns** | 307.1 M tags/s | 275.0 ns | 58.2 M tags/s | 440.3 ns | 36.3 M tags/s | +| medium (120) | **658.3 ns** | 182.3 M tags/s | 2.063 µs | 58.2 M tags/s | 3.232 µs | 37.1 M tags/s | +| large (600) | 11.141 µs | 53.9 M tags/s | **10.613 µs** | 56.5 M tags/s | 15.843 µs | 37.9 M tags/s | +| large (1500) | 75.590 µs | 19.8 M tags/s | **27.044 µs** | 55.5 M tags/s | 40.352 µs | 37.2 M tags/s | + +### Miss path (lookup of an unknown sentinel tag) + +| Tier | `match` miss | `HashMap` miss | `phf` miss | +| :--- | ---: | ---: | ---: | +| small (16) | **5.36 ns** | 18.67 ns | 21.40 ns | +| medium (120) | **7.07 ns** | 17.69 ns | 22.02 ns | +| large (600) | 26.77 ns | **17.14 ns** | 21.06 ns | +| large (1500) | 46.19 ns | **17.64 ns** | 21.39 ns | + +Interpretation: + +* **Small (16) & medium (120)**: LLVM's length-bucketed `memcmp` chain wins + decisively — `match` is 5.3× faster than `HashMap` and 8.4× faster than + `phf` at 16 tags (52 ns vs 440 ns per sweep). This inverts issue #49's + hypothesis that `phf` would already win at ≥50 elements: on this + toolchain it does not, because `phf` pays a fixed ~27 ns/tag (SipHash-1-3 + + displacement probe) while small `memcmp` chains are a handful of + predictable vectorized compares. +* **Large (600)**: the crossover happens between tiers. `HashMap` edges + ahead of `match` (10.6 µs vs 11.1 µs) while `phf` (15.8 µs) is still + behind both — the linear `memcmp` chain cost has grown super-linearly + with the number of distinct string lengths, but not yet past `phf`'s + constant per-tag cost. +* **Large (1500)**: `match` collapses to 19.8 M tags/s (75.6 µs — 3.8× + slower than at 600 tags scaled, the classic i-cache/branch-predictor + blow-up of hundreds of `memcmp` arms). `phf` (40.4 µs) finally beats it + by 1.87× while staying perfectly flat at ~37 M tags/s across **all** + tiers. `HashMap` stays fastest on hits (27.0 µs) — it hashes once and + probes a SIMD-friendly control-byte table — but it is a *runtime* build, + i.e. only a baseline, not a shippable static-dispatch option. +* **Misses**: `match` degrades linearly with tier size (5.4 ns → 46.2 ns, + since a miss walks the whole chain) while both hash structures are flat + (`HashMap` ~17–19 ns, `phf` ~21–22 ns constant). XML deserialization + only dispatches tags that exist in the schema, so the miss path matters + mainly for schema-validation workloads. + +## Results — hardware counters (`perf stat`, issue acceptance criterion #3) + +The canonical command (requires the `perf` binary): + +```bash +perf stat -e cycles,instructions,branches,branch-misses,L1-icache-load-misses \ + ./target/release/deps/tag_dispatch- --bench '' +``` + +This host lacks the `perf` binary, so counters are read through the same PMU +via `perf_event_open` using the committed harness (same events, same +semantics, userspace-only as required by `perf_event_paranoid = 2`): + +```bash +scripts/perf_stat.sh -- ./target/release/deps/tag_dispatch- --bench '' +``` + +Counters are for a **full sweep of the tier** (warm process, summed over +Criterion's iterations by the harness — compare rows within a tier, not +across tiers). IPC = instructions / cycles; miss% = branch-misses / branches. + +| Tier | Strategy | cycles | instructions | branches | branch-misses | L1I misses | IPC | miss% | +| :--- | :--- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| 16 | match | 36.0 B | 93.2 B | 20.9 B | 13.0 M | 74.9 K | 2.587 | 0.062% | +| 16 | hashmap | 41.8 B | 126.4 B | 11.5 B | 12.9 M | 109 K | 3.024 | 0.112% | +| 16 | phf | 37.6 B | 94.7 B | 7.8 B | 13.0 M | 103 K | 2.519 | 0.168% | +| 120 | match | 44.8 B | 124.1 B | 21.2 B | 17.1 M | 332 K | 2.770 | 0.081% | +| 120 | hashmap | 40.5 B | 122.6 B | 11.1 B | 13.1 M | 124 K | 3.030 | 0.118% | +| 120 | phf | 36.4 B | 92.3 B | 7.5 B | 14.8 M | 106 K | 2.539 | 0.196% | +| 600 | match | 46.3 B | 121.7 B | 16.6 B | 87.1 M | 12.5 M | 2.630 | 0.526% | +| 600 | hashmap | 45.4 B | 135.0 B | 12.2 B | 13.8 M | 123 K | 2.974 | 0.113% | +| 600 | phf | 39.7 B | 102.5 B | 8.3 B | 13.3 M | 106 K | 2.582 | 0.160% | +| 1500 | match | 44.1 B | 97.1 B | 12.0 B | 205.1 M | 92.1 M | 2.203 | 1.704% | +| 1500 | hashmap | 37.4 B | 107.0 B | 9.7 B | 24.9 M | 134 K | 2.860 | 0.257% | +| 1500 | phf | 44.3 B | 111.2 B | 9.0 B | 35.0 M | 130 K | 2.508 | 0.388% | + +Interpretation: + +* **The issue's two predicted failure modes are confirmed for `match`, but + only at scale.** From 120 → 1500 tags, `match`'s branch-miss rate grows + 21× (0.081% → 1.704%) and its L1 instruction-cache misses explode + **277×** (332 K → 92.1 M) as hundreds of string-literal `memcmp` arms + stop fitting in the 32 KiB I-cache and the predictor saturates — exactly + the "branch misprediction & i-cache thrash" mechanism #49 predicted for + large tag counts. Both hash structures stay ~flat by comparison + (`phf` L1I: 103 K → 130 K over the same range). +* **`phf` is the lowest-work dispatch of the three**: fewest branches at + every tier (7.5–9.0 B vs 16–21 B for `match` at small/medium — one hash + instead of a compare chain), and instruction counts *fall* as tiers grow + because loop overhead amortizes. Its disadvantage is raw latency per + lookup (SipHash + probe ≈ 27 ns/tag), not scalability — visible as the + lowest-but-stable IPC (≈2.5, load-dependent probe stalls). +* **`HashMap` runs at the highest IPC (≈2.9–3.0)** thanks to hashbrown's + branch-poor SIMD control-byte scan, but pays the most instructions at + small tiers (126.4 B vs 93.2 B for `match` at 16 tags). +* At 1500 tags `match` executes **1.7 branch-misses per 100 branches and + ~710× more I-cache misses than `phf`** (92.1 M vs 130 K) — the wall-clock + collapse in the throughput table (19.8 M vs 37.2 M tags/s) is these + counters, not extra work. + +## Compilation time & binary size + +**Small schema (completed).** Same standalone consumer crate (detached from +the workspace, `--release`), rebuilt after `touch`-ing only the generated +file; section sizes via `size`/`size -A` on the example binary: + +| Variant | gen-rebuild time | first build incl. `phf` dep | `.text` | `.rodata` | `.data` | `.bss` | +| :--- | ---: | ---: | ---: | ---: | ---: | ---: | +| `match` (default) | 2.51–2.54 s | — | 2 590 640 | 367 768 | 271 608 | 3 048 | +| `phf` | 2.53–2.55 s | 2.75 s | 2 591 728 | 367 912 | 271 928 | 1 640 | +| **Δ** | +0.02 s (noise) | +0.2 s one-time | **+1 088 (+0.04 %)** | **+144 (+0.04 %)** | +320 | −1 408 | + +**Large tiers (600 / 1500 elements): not measured — deferred to a +follow-up issue.** The encounter, for the record: + +* Compiling the generated consumer crate at **600 *and* 1500 elements — + both the `match` *and* the `phf` variant — exceeds ~3.2 GiB for a single + `rustc`** (observed in-cgroup anon-RSS at the OOM kill: 3.19–3.21 GiB, + right at the cap of 60 % of 5.4 GiB available on the 7.7 GiB reference + host). Uncapped attempts swap-thrashed the host into freezing (two WSL + restarts) before `scripts/memcap.sh` existed; every capped attempt since + was kernel-OOM-killed *inside its own cgroup* with the host untouched — + exit 143, by design. +* Because the memory blow-up hits **both variants at the same tier**, it + scales with field count (type-checking/monomorphizing the 600- and + 1500-field `decode_xml` paths), **not** with `match`-arm count — so no + `match`-vs-`phf` compile-memory conclusion can be drawn from it either + way. Re-establishing the compile-time and `.text`/`.rodata` growth curves + at these tiers needs a host where one `rustc` may safely use ≥ 4 GiB and + is tracked as a follow-up to this work. + +Interpretation (small schema): + +* **`phf` dispatch is effectively free at build scale**: +0.04 % `.text`, + +144 bytes `.rodata` (the perfect-hash tables live in read-only data), + and a rebuild delta within run-to-run noise. The only real build cost is + a one-time ~0.2 s to compile the `phf` dependency. +* This matches the runtime story: the table itself is tiny — what differs + between the strategies is *how each lookup walks it*, not how much code + or data gets emitted for a typical schema. + +## Auto-threshold recommendation + +**Enable `--feature phf` for schemas with ≥ ~900 element tags on a struct; +keep the default `match` below that.** + +Derivation (per-tag hit cost, linear interpolation between the measured +tiers): `match` costs 18.6 ns/tag at 600 tags and 50.4 ns/tag at 1500 +(tags/s × tier size), while `phf` is flat at ≈26.9 ns/tag at every tier. +Solving `18.6 + (50.4−18.6)·(N−600)/900 = 26.9` gives the `match`→`phf` +hit crossover at **N ≈ 835**; we round up to **900** for measurement noise +and schema drift. Notes: + +* This is a *recommendation*, not an auto-enable: the feature stays + opt-in (`--feature phf` / `features = ["phf"]`) per the issue #50 + unification decision — PolyXML does not silently change emitted code. +* The threshold is **~18× higher than issue #49's hypothesized 50-element + break-even** on this toolchain (rustc 1.98.1 / LLVM): small and medium + tiers keep winning for `match` at 5–8× margin. Trust the measurement, + not the hypothesis — re-run `cargo bench --bench tag_dispatch` on your + toolchain before adopting a different number. +* `phf`'s real win at ≥900 tags is **predictability**: flat ~37 M tags/s + and flat counters from 16 → 1500 tags, whereas `match` degrades 3.8× + (and its counters, catastrophically) with no upper bound in sight. +* Miss-heavy workloads (schema validation of unknown tags) cross over much + earlier: `match` misses degrade linearly (5.4 → 46.2 ns) while `phf` + misses are flat (~21 ns), intersecting around **≈380 tags**. If your + workload dispatches unknown tags on a hot path, prefer `phf` from ~400. +* The runtime `HashMap` baseline beats `phf` on hits at every tier, but it + cannot ship as static dispatch (it requires a runtime build of the table + per deserializer instance); it is reported only as a reference point. + +## Reproducing + +Every command below can be wrapped in `scripts/memcap.sh` (default: cap the +run at 60% of available RAM in an isolated cgroup, swap off) so a large-tier +build or bench cannot freeze a small host — the benchmark entry points +(`benchmarks/run_all.sh`, `benchmarks/cli/benchmark.sh`, +`scripts/perf_stat.sh`) already re-exec through it. Set +`POLYXML_MEMCAP_DISABLE=1` to opt out. + +```bash +# 1. Throughput (all tiers/strategies, ~3 minutes) +scripts/memcap.sh cargo bench -p polyxml --bench tag_dispatch + +# 2. Hardware counters for one strategy/tier (perf_stat.sh caps itself; +# pass --bench or Criterion runs test mode and you measure startup) +BIN=$(ls -t target/release/deps/tag_dispatch-* | grep -v '\.d$' | head -1) +scripts/perf_stat.sh -- "$BIN" --bench 'tag_dispatch/large_600/phf_hit' + +# 3. Regenerate tag fixtures (deterministic seed) +python3 scripts/gen_tag_dispatch_fixtures.py + +# 4. Generated-crate compile time / size comparison: build the same consumer +# crate twice (default output vs --feature phf + phf dep), timed after +# touching only the generated file; inspect with `size` (.text/.rodata). +# Run each build through scripts/memcap.sh: at 600 and 1500 elements +# EVERY variant's rustc needs >=3.2 GiB and will be OOM-killed at the +# default cap (by design — the host must stay responsive). Those tiers +# are deferred to a follow-up issue; the small-schema numbers above are +# the ones measured within the cap. +``` diff --git a/scripts/gen_tag_dispatch_fixtures.py b/scripts/gen_tag_dispatch_fixtures.py new file mode 100755 index 00000000..8efb51ef --- /dev/null +++ b/scripts/gen_tag_dispatch_fixtures.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Generate benches/support/tag_dispatch_fixtures.rs (issue #49). + +The Criterion suite in benches/tag_dispatch.rs compares three tag-dispatch +strategies (string `match`, runtime HashMap, compile-time phf::Map) at the +schema scale tiers defined in the issue. The per-tier tag lists and match +functions are too large to hand-maintain, so they are generated here with a +fixed seed and committed alongside the bench. + +Usage: + python3 scripts/gen_tag_dispatch_fixtures.py +""" + +from __future__ import annotations + +import random +from pathlib import Path + +OUT = ( + Path(__file__).resolve().parents[1] + / "crates" + / "polyxml-core" + / "benches" + / "support" + / "tag_dispatch_fixtures.rs" +) + +# Issue #49 scale tiers: small (10-25), medium (80-150), large (500-1500). +# Both ends of the large range are sampled so the match->phf crossover (if any) +# is measured rather than extrapolated. +TIERS = [16, 120, 600, 1500] +SEED = 490490 + +# Realistic enterprise-tag vocabulary; lengths ~12-45 bytes so LLVM's +# length-bucketed memcmp chains are not artificially favored. +WORDS = [ + "Settlement", + "Instruction", + "Party", + "Account", + "Amount", + "Currency", + "ValueDate", + "Reference", + "Status", + "Identification", + "Transmission", + "Reconciliation", + "Clearing", + "Margin", + "Collateral", + "Beneficiary", + "Remittance", + "Routing", + "Authentication", + "Sequence", + "Version", + "Channel", + "Priority", + "Category", + "Adjustment", + "Position", + "Statement", + "Transaction", +] +SUFFIXES = [ + "", + "", + "Code", + "Type", + "Flag", + "Indicator", + "Date", + "Id", + "Count", + "1", + "2", + "Sub", + "Report", +] + + +def make_tags(n: int, seed: int) -> list[str]: + rng = random.Random(seed) + # Two-word composition keeps the space (28*28*13 = 10k+) well above the + # largest tier; the guard turns any future space shortfall into a clear + # error instead of an infinite loop. + attempts = 0 + tags: set[str] = set() + while len(tags) < n: + tags.add(rng.choice(WORDS) + rng.choice(WORDS) + rng.choice(SUFFIXES)) + attempts += 1 + if attempts > n * 100: + raise RuntimeError(f"tag space too small for tier {n} ({len(tags)} unique)") + return sorted(tags) + + +def emit(tier: int, tags: list[str], out: list[str]) -> None: + n = len(tags) + out.append(f"// --- Tier {n} ({'small' if n <= 25 else 'medium' if n <= 150 else 'large'}) ---") + out.append(f"pub const TAGS_{n}: [&str; {n}] = [") + for t in tags: + out.append(f' "{t}",') + out.append("];") + out.append("") + out.append(f"pub fn match_dispatch_{n}(tag: &str) -> Option {{") + out.append(" match tag {") + for i, t in enumerate(tags): + out.append(f' "{t}" => Some({i}),') + out.append(" _ => None,") + out.append(" }") + out.append("}") + out.append("") + out.append(f"pub static PHF_{n}: phf::Map<&'static str, u32> = phf::phf_map! {{") + for i, t in enumerate(tags): + out.append(f' "{t}" => {i}u32,') + out.append("};") + out.append("") + + +def main() -> None: + header = [ + "// GENERATED by scripts/gen_tag_dispatch_fixtures.py — do not edit.", + "// Tag lists, string-match baselines, and phf tables for the issue #49", + "// tag_dispatch Criterion bench. Regenerate with:", + "// python3 scripts/gen_tag_dispatch_fixtures.py", + "", + "#![allow(clippy::all)]", + "", + ] + out = list(header) + for tier in TIERS: + emit(tier, make_tags(tier, SEED + tier), out) + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text("\n".join(out)) + print(f"wrote {OUT} ({len(out)} lines)") + + +if __name__ == "__main__": + main() diff --git a/scripts/memcap.sh b/scripts/memcap.sh new file mode 100755 index 00000000..06cd4280 --- /dev/null +++ b/scripts/memcap.sh @@ -0,0 +1,140 @@ +#!/bin/sh +# memcap.sh - run a command under a hard RAM cap. +# +# Benchmarks and codegen smoke tests build large generated crates; on small +# dev machines (e.g. a 7.7-GiB WSL box) an uncapped `cargo build --release` +# can exhaust RAM, drive the host into swap-thrash and freeze it. This +# wrapper caps the command's whole process tree so an over-limit build is +# OOM-killed *inside its own cgroup* (exit 137) while the host stays up. +# +# Usage: scripts/memcap.sh [args...] +# +# Environment: +# POLYXML_MEMCAP_PCT Percentage of currently-available RAM the command +# may use (default: 60). Integer 1-100. +# POLYXML_MEMCAP_BACKEND auto (default) | systemd | ulimit | none +# auto - Linux with a systemd --user manager: +# kernel-enforced MemoryMax (preferred); +# otherwise fall back to ulimit. +# systemd - require a systemd --user scope or fail +# (useful for asserting the hard cap). +# ulimit - force the address-space fallback +# (no systemd, containers, macOS). +# none - run uncapped. +# POLYXML_MEMCAP_DISABLE Set to 1 to run uncapped (same as backend=none). +# POLYXML_MEMCAP_LEVEL Nesting counter; set automatically so wrappers +# (e.g. perf_stat.sh) can re-enter safely without +# double-capping. +# +# Notes: +# * The percentage is recomputed against *available* RAM at each invocation. +# * The systemd path sets MemorySwapMax=0 so capped work can never page the +# host into thrash: hitting the cap fails fast (kernel OOM kill) instead +# of grinding to a halt. +# * The ulimit path caps address space (RLIMIT_AS), which is best-effort: +# it reliably stops runaway allocations but counts reserved -not- resident +# pages, so it may be stricter than the systemd cap. +set -eu + +usage() { + echo "usage: $0 [args...]" >&2 + exit 2 +} + +[ "$#" -gt 0 ] || usage + +pct="${POLYXML_MEMCAP_PCT:-60}" +backend="${POLYXML_MEMCAP_BACKEND:-auto}" + +case "$pct" in + '' | *[!0-9]*) + echo "memcap: POLYXML_MEMCAP_PCT must be an integer (got '$pct')" >&2 + exit 2 + ;; +esac +if [ "$pct" -lt 1 ] || [ "$pct" -gt 100 ]; then + echo "memcap: POLYXML_MEMCAP_PCT must be 1-100 (got '$pct')" >&2 + exit 2 +fi + +# Mark ourselves as "inside a cap" BEFORE any exec-out below: wrapped +# scripts re-exec through memcap when POLYXML_MEMCAP_LEVEL is unset, so the +# disable/fallback paths must still set it or they would loop forever. +level=$((${POLYXML_MEMCAP_LEVEL:-0} + 1)) +export POLYXML_MEMCAP_LEVEL="$level" + +if [ "${POLYXML_MEMCAP_DISABLE:-0}" = "1" ] || [ "$backend" = "none" ]; then + exec "$@" +fi + +# --- How much RAM may we take? ------------------------------------------- +os="$(uname -s)" +avail_kb="" + +if [ "$os" = "Linux" ] && [ -r /proc/meminfo ]; then + avail_kb="$(awk '/^MemAvailable:/ {print $2; exit}' /proc/meminfo)" +fi +if [ -z "$avail_kb" ] && [ "$os" = "Darwin" ]; then + # Best effort: free + inactive + speculative pages (macOS reclaims the + # latter two on demand, so they are effectively available). + page_size="$(sysctl -n hw.pagesize 2>/dev/null || echo 4096)" + avail_kb="$(vm_stat 2>/dev/null | awk -v ps="$page_size" ' + /Pages free:/ {gsub(/\./, "", $3); free = $3} + /Pages inactive:/ {gsub(/\./, "", $3); inact = $3} + /Pages speculative:/{gsub(/\./, "", $3); spec = $3} + END {if (free == "") exit 1; printf "%d", (free + inact + spec) * ps / 1024}')" || avail_kb="" +fi +if [ -z "$avail_kb" ] || [ "$avail_kb" -le 0 ] 2>/dev/null; then + # Last resort: total RAM. + if [ "$os" = "Darwin" ]; then + total_bytes="$(sysctl -n hw.memsize 2>/dev/null || echo 0)" + avail_kb=$((total_bytes / 1024)) + elif [ -r /proc/meminfo ]; then + avail_kb="$(awk '/^MemTotal:/ {print $2; exit}' /proc/meminfo)" + fi +fi +if [ -z "$avail_kb" ] || [ "$avail_kb" -le 0 ] 2>/dev/null; then + echo "memcap: cannot determine available memory; running uncapped" >&2 + exec "$@" +fi + +cap_kb=$((avail_kb * pct / 100)) +cap_mib=$((cap_kb / 1024)) +avail_mib=$((avail_kb / 1024)) + +# --- Backend: kernel-enforced cgroup (preferred) -------------------------- +use_systemd=0 +if [ "$backend" = "systemd" ] || + { [ "$backend" = "auto" ] && [ "$os" = "Linux" ] && command -v systemd-run >/dev/null 2>&1; }; then + # Probe the *user* manager once. Works in systemd distros and CI images + # with a session bus; fails in bare containers/chroots -> fall back. + if systemd-run --user --scope --quiet -p MemoryMax=64M -- /bin/true \ + /dev/null 2>&1; then + use_systemd=1 + elif [ "$backend" = "systemd" ]; then + echo "memcap: systemd --user scope unavailable but POLYXML_MEMCAP_BACKEND=systemd was requested" >&2 + exit 2 + fi +fi + +if [ "$use_systemd" = 1 ]; then + echo "memcap: <=${cap_mib} MiB (${pct}% of ${avail_mib} MiB available) [systemd MemoryMax, swap off]" >&2 + exec systemd-run --user --scope --quiet \ + --setenv=POLYXML_MEMCAP_LEVEL="$level" \ + -p "MemoryMax=${cap_kb}K" \ + -p MemorySwapMax=0 \ + -- "$@" +fi + +# --- Fallback: RLIMIT_AS -------------------------------------------------- +if [ "$backend" = "auto" ] || [ "$backend" = "ulimit" ]; then + echo "memcap: <=${cap_mib} MiB (${pct}% of ${avail_mib} MiB available) [ulimit -v fallback]" >&2 + if ulimit -v "$cap_kb" 2>/dev/null; then + exec "$@" + fi + echo "memcap: could not lower RLIMIT_AS (hard limit too low?); running uncapped" >&2 + exec "$@" +fi + +echo "memcap: unknown POLYXML_MEMCAP_BACKEND '$backend'" >&2 +exit 2 diff --git a/scripts/perf_stat.c b/scripts/perf_stat.c new file mode 100644 index 00000000..a1b8cfac --- /dev/null +++ b/scripts/perf_stat.c @@ -0,0 +1,150 @@ +/* perf_stat: perf_event_open-based hardware counter harness for hosts without + * the `perf` binary (issue #49: "Profile branch misses and IPC using perf + * stat"). Counts for a command: cycles, instructions, branches, branch-misses, + * and L1-icache-load-misses — the same events + * `perf stat -e cycles,instructions,branches,branch-misses,L1-icache-load-misses` + * would report. Userspace-only (perf_event_paranoid >= 2 compatible). + * + * Events are attached directly to the forked child pid (perf-stat style) + * rather than via inherit=1: inherited child counts do not reliably + * propagate to the parent's read() on this kernel, which silently reported + * only parent noise (~2.5M cycles regardless of workload). + * + * Build & run: + * gcc -O2 -o /tmp/perf_stat scripts/perf_stat.c + * /tmp/perf_stat -- ./target/release/deps/tag_dispatch- + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static long open_ev(pid_t pid, __u64 config, __u32 type) { + struct perf_event_attr pe; + memset(&pe, 0, sizeof(pe)); + pe.type = type; + pe.size = sizeof(pe); + pe.config = config; + pe.disabled = 1; /* enabled together right before child release */ + pe.exclude_kernel = 1; /* required at perf_event_paranoid >= 2 */ + return syscall(__NR_perf_event_open, &pe, pid, -1, -1, 0); +} + +struct ev { + const char *name; + long fd; +}; + +int main(int argc, char **argv) { + if (argc < 3 || strcmp(argv[1], "--") != 0) { + fprintf(stderr, "usage: perf_stat -- [args...]\n"); + return 2; + } + + int sync[2]; + if (pipe(sync) != 0) { + perror("pipe"); + return 4; + } + + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return 4; + } + if (pid == 0) { + /* Block until the parent has the counters armed on this pid. */ + close(sync[1]); + char c; + if (read(sync[0], &c, 1) != 1) { + _exit(4); + } + close(sync[0]); + execvp(argv[2], &argv[2]); + perror("exec"); + _exit(127); + } + + close(sync[0]); + + const __u64 l1i_read_miss = + (__u64)PERF_COUNT_HW_CACHE_L1I | + ((__u64)PERF_COUNT_HW_CACHE_OP_READ << 8) | + ((__u64)PERF_COUNT_HW_CACHE_RESULT_MISS << 16); + + struct ev evs[] = { + {"cycles", open_ev(pid, PERF_COUNT_HW_CPU_CYCLES, PERF_TYPE_HARDWARE)}, + {"instructions", open_ev(pid, PERF_COUNT_HW_INSTRUCTIONS, PERF_TYPE_HARDWARE)}, + {"branches", open_ev(pid, PERF_COUNT_HW_BRANCH_INSTRUCTIONS, PERF_TYPE_HARDWARE)}, + {"branch-misses", open_ev(pid, PERF_COUNT_HW_BRANCH_MISSES, PERF_TYPE_HARDWARE)}, + {"L1-icache-load-misses", open_ev(pid, l1i_read_miss, PERF_TYPE_HW_CACHE)}, + }; + const int n = (int)(sizeof(evs) / sizeof(evs[0])); + + int avail = 0; + for (int i = 0; i < n; i++) { + if (evs[i].fd < 0) { + fprintf(stderr, "perf_stat: %s unavailable (%s)\n", evs[i].name, + strerror(errno)); + } else { + avail++; + ioctl(evs[i].fd, PERF_EVENT_IOC_ENABLE, 0); + } + } + if (avail == 0) { + fprintf(stderr, "perf_stat: no PMU counters available\n"); + /* Don't strand the child. */ + (void)!write(sync[1], "x", 1); + close(sync[1]); + int st = 0; + waitpid(pid, &st, 0); + return 3; + } + + /* Release the child: counters are attached and enabled. */ + if (write(sync[1], "x", 1) != 1) { + perror("write"); + return 4; + } + close(sync[1]); + + int status = 0; + waitpid(pid, &status, 0); + for (int i = 0; i < n; i++) { + if (evs[i].fd >= 0) { + ioctl(evs[i].fd, PERF_EVENT_IOC_DISABLE, 0); + } + } + + unsigned long long vals[5] = {0, 0, 0, 0, 0}; + for (int i = 0; i < n; i++) { + if (evs[i].fd >= 0 && + read(evs[i].fd, &vals[i], sizeof(vals[i])) != (ssize_t)sizeof(vals[i])) { + vals[i] = 0; + } + } + + fprintf(stderr, "\n"); + for (int i = 0; i < n; i++) { + if (evs[i].fd >= 0) { + fprintf(stderr, "%18llu %s\n", vals[i], evs[i].name); + } + } + if (vals[0] > 0) { + fprintf(stderr, "\nIPC: %.3f", (double)vals[1] / (double)vals[0]); + if (vals[2] > 0) { + fprintf(stderr, " branch-miss rate: %.4f%%", + 100.0 * (double)vals[3] / (double)vals[2]); + } + fprintf(stderr, "\n"); + } + + return WIFEXITED(status) ? WEXITSTATUS(status) : 128 + WTERMSIG(status); +} diff --git a/scripts/perf_stat.sh b/scripts/perf_stat.sh new file mode 100755 index 00000000..2717d722 --- /dev/null +++ b/scripts/perf_stat.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Compile-and-run wrapper for scripts/perf_stat.c (see that file for usage). +# Usage: scripts/perf_stat.sh -- [args...] +set -eu +here="$(cd "$(dirname "$0")" && pwd)" +# Run the countered command under a RAM cap (scripts/memcap.sh) so runaway +# workloads can't freeze small hosts; the LEVEL guard makes the re-exec +# idempotent. POLYXML_MEMCAP_DISABLE=1 opts out. Note: pass --bench to +# Criterion binaries, otherwise they run test mode (~0.2s) and the counters +# only measure process startup. +if [ -z "${POLYXML_MEMCAP_LEVEL:-}" ]; then + exec "$here/memcap.sh" "$here/$(basename "$0")" "$@" +fi +bin="${TMPDIR:-/tmp}/polyxml_perf_stat.$$" +trap 'rm -f "$bin"' EXIT +gcc -O2 -o "$bin" "$here/perf_stat.c" +exec "$bin" "$@" diff --git a/scripts/verify_codegen.sh b/scripts/verify_codegen.sh index 4dec66f7..ff5d35ce 100755 --- a/scripts/verify_codegen.sh +++ b/scripts/verify_codegen.sh @@ -57,10 +57,11 @@ EOF echo -e "${BLUE}==> Testing code generation across all 7 target ecosystems...${NC}" -# 1. Rust (zero-copy + rkyv) -echo -n " • Rust (--feature zero-copy,rkyv)... " -"${POLYXML_BIN}" generate --lang rust --feature zero-copy,rkyv --out "${TMP_DIR}/rs" "${SCHEMA_FILE}" >/dev/null +# 1. Rust (zero-copy + rkyv + perfect-hash dispatch) +echo -n " • Rust (--feature zero-copy,rkyv,phf)... " +"${POLYXML_BIN}" generate --lang rust --feature zero-copy,rkyv,phf --out "${TMP_DIR}/rs" "${SCHEMA_FILE}" >/dev/null test -f "${TMP_DIR}/rs/test_schema.rs" +grep -q "ELEMENT_DISPATCH" "${TMP_DIR}/rs/test_schema.rs" echo -e "${GREEN}OK${NC}" # 2. Python (dataclass & pydantic) From 020794fedb6a0d7f14447faaac2f457161afce44 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 09:20:00 -0500 Subject: [PATCH 09/10] docs(benchmarks): link deferred large-schema compile measurement to #55 The size section's "deferred to a follow-up issue" caveat now points at the concrete issue. Also fixes a garbled memcap sentence in SKILL.md (POLYXML_MEMCAP_LEVEL is set automatically, not set by the caller) and drops an unverifiable exit-code claim from the OOM encounter record. --- .agents/skills/polyxml-codegen-workflow/SKILL.md | 5 +++-- docs/benchmarks/rust-phf-dispatch.md | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 7ac43647..1e0da732 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -312,12 +312,13 @@ derived types inherit their base's patterns at parse time): (exit 137) instead of freezing the host — uncapped 1500-element builds froze a 7.7-GiB WSL box repeatedly before this guard existed. Wrap measure/compile-size scripts per build step (own scope each) so one - OOM kill doesn't abort the rest; set `POLYXML_MEMCAP_LEVEL` is automatic, + OOM kill doesn't abort the rest; `POLYXML_MEMCAP_LEVEL` (nesting depth) + is set automatically so wrapped scripts can't re-exec in a loop, and `POLYXML_MEMCAP_DISABLE=1` opts out. Known datum: generated consumer crates at **600 and 1500 elements need >=3.2 GiB for a single `rustc`** (both the `match` and `phf` variants — it scales with field count, not match-arm count), so compile-time/size measurements at those tiers are - deferred to a follow-up issue and must not be retried uncapped on + deferred to #55 and must not be retried uncapped on 8-GiB-class hosts. - Shared CLI options apply to every `--lang`, not just the preceding one. Use per-target manifest entries for heterogeneous configurations. Schema-less diff --git a/docs/benchmarks/rust-phf-dispatch.md b/docs/benchmarks/rust-phf-dispatch.md index cd566997..62a2c1f0 100644 --- a/docs/benchmarks/rust-phf-dispatch.md +++ b/docs/benchmarks/rust-phf-dispatch.md @@ -190,8 +190,8 @@ file; section sizes via `size`/`size -A` on the example binary: | `phf` | 2.53–2.55 s | 2.75 s | 2 591 728 | 367 912 | 271 928 | 1 640 | | **Δ** | +0.02 s (noise) | +0.2 s one-time | **+1 088 (+0.04 %)** | **+144 (+0.04 %)** | +320 | −1 408 | -**Large tiers (600 / 1500 elements): not measured — deferred to a -follow-up issue.** The encounter, for the record: +**Large tiers (600 / 1500 elements): not measured — deferred to +#55.** The encounter, for the record: * Compiling the generated consumer crate at **600 *and* 1500 elements — both the `match` *and* the `phf` variant — exceeds ~3.2 GiB for a single @@ -200,14 +200,14 @@ follow-up issue.** The encounter, for the record: host). Uncapped attempts swap-thrashed the host into freezing (two WSL restarts) before `scripts/memcap.sh` existed; every capped attempt since was kernel-OOM-killed *inside its own cgroup* with the host untouched — - exit 143, by design. + by design. * Because the memory blow-up hits **both variants at the same tier**, it scales with field count (type-checking/monomorphizing the 600- and 1500-field `decode_xml` paths), **not** with `match`-arm count — so no `match`-vs-`phf` compile-memory conclusion can be drawn from it either way. Re-establishing the compile-time and `.text`/`.rodata` growth curves at these tiers needs a host where one `rustc` may safely use ≥ 4 GiB and - is tracked as a follow-up to this work. + is tracked in #55. Interpretation (small schema): @@ -277,6 +277,6 @@ python3 scripts/gen_tag_dispatch_fixtures.py # Run each build through scripts/memcap.sh: at 600 and 1500 elements # EVERY variant's rustc needs >=3.2 GiB and will be OOM-killed at the # default cap (by design — the host must stay responsive). Those tiers -# are deferred to a follow-up issue; the small-schema numbers above are +# are deferred to #55; the small-schema numbers above are # the ones measured within the cap. ``` From d6f567d148d4f0ca46786fdf755f580bd72b4f54 Mon Sep 17 00:00:00 2001 From: Bailey Nguyen Date: Tue, 22 Sep 2026 22:33:23 -0500 Subject: [PATCH 10/10] fix: enforce pattern and xsi:type semantics across runtimes --- .../skills/polyxml-codegen-workflow/SKILL.md | 13 +- .agents/skills/polyxml-core-engine/SKILL.md | 30 ++-- crates/polyxml-core/src/codegen/cpp/mod.rs | 4 +- crates/polyxml-core/src/codegen/csharp/mod.rs | 4 +- crates/polyxml-core/src/codegen/go/mod.rs | 4 +- crates/polyxml-core/src/codegen/java/mod.rs | 4 +- crates/polyxml-core/src/codegen/python/mod.rs | 4 +- crates/polyxml-core/src/codegen/rust/mod.rs | 5 +- .../src/codegen/typescript/mod.rs | 22 ++- crates/polyxml-core/src/parser.rs | 135 ++++++++++++++---- crates/polyxml-core/src/schema.rs | 57 +++++--- crates/polyxml-core/src/serializer.rs | 15 +- .../polyxml-core/tests/test_issue51_audit.rs | 4 +- .../tests/test_issue53_xsi_type.rs | 45 +++++- .../tests/test_pattern_codegen.rs | 34 +++-- .../polyxml-core/tests/test_python_codegen.rs | 2 +- crates/polyxml-core/tests/test_ts_codegen.rs | 4 +- crates/polyxml-python/src/lib.rs | 51 ++++++- crates/polyxml-python/tests/test_xsi_type.py | 51 ++++++- docs/guides/compiler.md | 4 +- docs/guides/polymorphism.md | 12 +- 21 files changed, 376 insertions(+), 128 deletions(-) diff --git a/.agents/skills/polyxml-codegen-workflow/SKILL.md b/.agents/skills/polyxml-codegen-workflow/SKILL.md index 1e0da732..8572db07 100644 --- a/.agents/skills/polyxml-codegen-workflow/SKILL.md +++ b/.agents/skills/polyxml-codegen-workflow/SKILL.md @@ -259,12 +259,12 @@ derived types inherit their base's patterns at parse time): - `emit_simple_type` appends `AfterValidator(_polyxml_patterns(r"p1", r"p2"))` to the `Annotated[...]` alias (single-pattern types keep the plain - `Field(pattern=...)` kwarg, byte-identical to before). + `Field(pattern=...)` kwarg, anchored for full-value matching). - `needs_pattern_validator(ir)` gates emission of the module-level `_polyxml_patterns` factory helper, the `import re` line, and the `AfterValidator,` prefix on the pydantic import — keep these three in sync. -- Semantics: unanchored `regex.search` per pattern, all must match (AND), - matching XSD pattern search semantics. +- Semantics: each pattern must match the entire lexical value (`fullmatch`); + patterns inherited across derivation steps all apply (AND). - Field-level facets (`FieldDef.facets`) are never populated by the parser today, so `emit_struct`'s field path needs no `AfterValidator` wiring; if that ever changes, mirror the type-level handling there. @@ -375,11 +375,10 @@ Per-language enforcement (all gated so unpatterned schemas stay byte-identical): expressions into the call** — binding `const auto& value = ;` self-references when the field is named `value` (range-for over a member named `value` is fine). `#include ` is conditional on `ir`. -- **Java**: `Pattern.compile(p).matcher(v).find()` (search semantics — - `Pattern.matches` anchors and is wrong for XSD); direct codecs resolve +- **Java**: `Pattern.compile(p).matcher(v).matches()`; direct codecs resolve patterned aliases through `primitive_base`. -- **C#**: `Regex.IsMatch` **without** `^...$` anchors (XSD patterns are - unanchored searches); escape `\` before `"` in the pattern string. +- **C#**: `Regex.IsMatch` with `\A(?:...)\z` full-value anchors; + escape `\` before `"` in the pattern string. - **TypeScript**: one pattern → `pattern:` option (zod/valibot) or single `Type.String({pattern})`; multiple → `AfterValidator`-equivalent AND via `_polyxml_patterns` (Python) / `Type.Intersect([...])` (TypeBox — duplicating diff --git a/.agents/skills/polyxml-core-engine/SKILL.md b/.agents/skills/polyxml-core-engine/SKILL.md index 6b7c8eab..ae8b48b7 100644 --- a/.agents/skills/polyxml-core-engine/SKILL.md +++ b/.agents/skills/polyxml-core-engine/SKILL.md @@ -89,12 +89,11 @@ The runtime dispatches polymorphic elements through a **type registry on `crates/polyxml-core/tests/test_issue53_xsi_type.rs`. - **Registry**: `ModelSchema` carries `is_abstract: bool` plus a - `variants: OnceLock>>` (methods: `set_variants`, - `variants`, `has_variants`, `find_variant(local_bytes)`, - `matches_variant(candidate)`). `OnceLock` keeps `ModelSchema`'s `Clone` - derive intact and guarantees each schema registers once. Variants are - keyed by the type QName local part (`xml_name`), which is `Meta.name` on - the Python side and the type local on the XSD side. + `variants: Arc>>>` (methods: `set_variants`, + `variants`, `has_variants`, `find_variant_qname(namespace, local_bytes)`, + `matches_variant(candidate)`). The lock lets Python refresh the registry + after new subclasses are imported. Variants carry their QName namespace + and local name (`xml_name`), which is `Meta.name` on the Python side. - **Population (`schema.rs::from_ir`)**: `build_struct` (a) flattens the `xs:extension` content model by collecting the base-chain structs (root-base first, cycle-guarded) and prepending their fields — mirroring @@ -107,14 +106,19 @@ The runtime dispatches polymorphic elements through a **type registry on `__subclasses__()` transitively (ptr-seen set, extracts only dataclass/Pydantic classes) — running *after* cache insertion so a subclass field typed as the base resolves from cache instead of - re-entering extraction. -- **Parse (`parser.rs`)**: `resolve_record_schema(declared, start)` runs at + re-entering extraction. Cached classes refresh the registry on lookup; + refresh walks nested field schemas too, so a subclass loaded after a + containing model was cached is visible. A thread-local discovery guard + prevents recursive refresh during extraction. +- **Parse (`parser.rs`)**: `resolve_record_schema(declared, start, scope)` runs at *every* frame creation site — root `Start`/`Empty` in `deserialize_with_limit`, the root frame in `parse_sub_tree`, both nested `Start`/`Empty` arms (single and list), and the `XmlItemStream` - `Empty` arm. `xsi_type_value` matches the attribute **local name** - `type` (namespace-blind, like `is_nil_element`). Policy: registered - variant → switch schema; unknown value on `is_abstract` → + `Empty` arm. Namespace scopes track declarations through nested elements, + including streamed records. The attribute must resolve to the XML Schema + Instance namespace, and its QName value must match both the variant's + namespace and local name. Policy: registered concrete variant → switch + schema; absent or unknown value on `is_abstract` → `PolyXmlError::SchemaError` listing known variants, or an "escape hatch" error when none are registered; everything else falls back to the declared schema. Schemas that are neither abstract nor have @@ -124,8 +128,8 @@ The runtime dispatches polymorphic elements through a **type registry on - **Serialize (`serializer.rs`)**: `write_model` checks `schema.matches_variant(rec)` on `Record` values, shadows the effective schema to the variant, and pushes `xsi:type` (qualified QName via - `qualify_element`, falling back to literal `xsi:type` when namespaces - are disabled) after content attributes. `NamespaceContext::collect_namespaces` + `qualify_element`) after content attributes. Serialization errors when + namespaces are disabled or XSI has no bound prefix. `NamespaceContext::collect_namespaces` recurses `variants` and injects the XSI URI so `xmlns:xsi` is always declared at the root whenever dispatch is possible. - **JSON (`json.rs`)**: `poly_value_to_json_value` prefers the record's diff --git a/crates/polyxml-core/src/codegen/cpp/mod.rs b/crates/polyxml-core/src/codegen/cpp/mod.rs index c5b4e6da..2a5c8ba4 100644 --- a/crates/polyxml-core/src/codegen/cpp/mod.rs +++ b/crates/polyxml-core/src/codegen/cpp/mod.rs @@ -655,7 +655,7 @@ concept XmlModel = requires(T a) {{ if self.options.validate_facets && !simple.facets.patterns.is_empty() { writeln!(out, "[[nodiscard]] inline bool validate_{}_patterns(std::string_view value) noexcept {{\n try {{", type_name).unwrap(); for (i, pattern) in simple.facets.patterns.iter().enumerate() { - writeln!(out, " static const std::regex pattern_{}({:?});\n if (!std::regex_search(value.begin(), value.end(), pattern_{})) return false;", i, pattern, i).unwrap(); + writeln!(out, " static const std::regex pattern_{}({:?});\n if (!std::regex_match(value.begin(), value.end(), pattern_{})) return false;", i, pattern, i).unwrap(); } writeln!(out, " return true;\n }} catch (const std::regex_error&) {{ return false; }}\n}}\n").unwrap(); } @@ -996,7 +996,7 @@ concept XmlModel = requires(T a) {{ indent: &str, ) { for pattern in &facets.patterns { - writeln!(out, "{}try {{ static const std::regex pattern({:?}); if (!std::regex_search({}, pattern)) return false; }} catch (const std::regex_error&) {{ return false; }}", indent, pattern, target).unwrap(); + writeln!(out, "{}try {{ static const std::regex pattern({:?}); if (!std::regex_match({}, pattern)) return false; }} catch (const std::regex_error&) {{ return false; }}", indent, pattern, target).unwrap(); } if let Some(min_len) = facets.min_length { writeln!( diff --git a/crates/polyxml-core/src/codegen/csharp/mod.rs b/crates/polyxml-core/src/codegen/csharp/mod.rs index 94abdaae..fc30832f 100644 --- a/crates/polyxml-core/src/codegen/csharp/mod.rs +++ b/crates/polyxml-core/src/codegen/csharp/mod.rs @@ -906,7 +906,9 @@ impl CSharpCodegen { .unwrap(); } for pattern in &facets.patterns { - let escaped = pattern.replace('\\', "\\\\").replace('"', "\\\""); + let escaped = format!(r"\A(?:{pattern})\z") + .replace('\\', "\\\\") + .replace('"', "\\\""); writeln!( out, "{}if (!Regex.IsMatch({}.ToString() ?? \"\", \"{}\")) yield return new ValidationResult(\"{} does not match pattern {}\", [nameof({})]);", diff --git a/crates/polyxml-core/src/codegen/go/mod.rs b/crates/polyxml-core/src/codegen/go/mod.rs index f3719df0..1f7f4e22 100644 --- a/crates/polyxml-core/src/codegen/go/mod.rs +++ b/crates/polyxml-core/src/codegen/go/mod.rs @@ -421,7 +421,7 @@ impl GoCodegen { if self.options.validate_facets && !simple.facets.patterns.is_empty() { writeln!(out, "func (s {}) Validate() error {{", type_name).unwrap(); for pattern in &simple.facets.patterns { - writeln!(out, " if matched, err := regexp.MatchString({:?}, fmt.Sprint(s)); err != nil || !matched {{ return fmt.Errorf(\"pattern constraint failed\") }}", pattern).unwrap(); + writeln!(out, " if matched, err := regexp.MatchString({:?}, fmt.Sprint(s)); err != nil || !matched {{ return fmt.Errorf(\"pattern constraint failed\") }}", format!("^(?:{pattern})$")).unwrap(); } writeln!(out, " return nil\n}}\n").unwrap(); if self @@ -832,7 +832,7 @@ impl GoCodegen { indent: &str, ) { for pattern in &facets.patterns { - writeln!(out, "{}if matched, err := regexp.MatchString({:?}, fmt.Sprint({})); err != nil || !matched {{ return fmt.Errorf(\"pattern constraint failed\") }}", indent, pattern, target).unwrap(); + writeln!(out, "{}if matched, err := regexp.MatchString({:?}, fmt.Sprint({})); err != nil || !matched {{ return fmt.Errorf(\"pattern constraint failed\") }}", indent, format!("^(?:{pattern})$"), target).unwrap(); } if let Some(min_len) = facets.min_length { writeln!( diff --git a/crates/polyxml-core/src/codegen/java/mod.rs b/crates/polyxml-core/src/codegen/java/mod.rs index 0ee4f806..7fcb3ed9 100644 --- a/crates/polyxml-core/src/codegen/java/mod.rs +++ b/crates/polyxml-core/src/codegen/java/mod.rs @@ -835,7 +835,7 @@ impl JavaCodegen { } for pat in &facets.patterns { checks.push(format!( - "{}.ifPresent(v -> {{ if (!Pattern.compile({:?}).matcher(v).find()) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?}); }});", + "{}.ifPresent(v -> {{ if (!Pattern.compile({:?}).matcher(v).matches()) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?}); }});", var_name, pat, var_name, pat )); } @@ -864,7 +864,7 @@ impl JavaCodegen { } for pat in &facets.patterns { checks.push(format!( - "if (!Pattern.compile({:?}).matcher({}).find()) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?});", + "if (!Pattern.compile({:?}).matcher({}).matches()) throw new IllegalArgumentException(\"{} does not match pattern: \" + {:?});", pat, var_name, var_name, pat )); } diff --git a/crates/polyxml-core/src/codegen/python/mod.rs b/crates/polyxml-core/src/codegen/python/mod.rs index f53f04cb..f9350445 100644 --- a/crates/polyxml-core/src/codegen/python/mod.rs +++ b/crates/polyxml-core/src/codegen/python/mod.rs @@ -21,7 +21,7 @@ const PATTERN_VALIDATOR_HELPER: &str = r#"def _polyxml_patterns(*patterns: str): def _validate(value: str) -> str: for regex in _compiled: - if regex.search(value) is None: + if regex.fullmatch(value) is None: raise ValueError(f"string_pattern_mismatch: {regex.pattern!r}") return value @@ -889,7 +889,7 @@ impl PythonCodegen { clauses.push(format!("min_length={}, max_length={}", l, l)); } if let Some(pat) = facets.patterns.first() { - clauses.push(format!("pattern=r\"{}\"", pat)); + clauses.push(format!("pattern=r\"\\A(?:{})\\z\"", pat)); } clauses.join(", ") diff --git a/crates/polyxml-core/src/codegen/rust/mod.rs b/crates/polyxml-core/src/codegen/rust/mod.rs index eb2ca984..8a0e728e 100644 --- a/crates/polyxml-core/src/codegen/rust/mod.rs +++ b/crates/polyxml-core/src/codegen/rust/mod.rs @@ -447,7 +447,8 @@ impl RustCodegen { let _ = writeln!(out, "pub fn validate_{}_patterns(value: &str) -> std::result::Result<(), &'static str> {{", type_name); for (i, pattern) in s.facets.patterns.iter().enumerate() { let _ = writeln!(out, " static PATTERN_{}: std::sync::OnceLock> = std::sync::OnceLock::new();", i); - let _ = writeln!(out, " let pattern = PATTERN_{}.get_or_init(|| regex::Regex::new({:?})).as_ref().map_err(|_| \"unsupported pattern syntax\")?;", i, pattern); + let full_pattern = format!(r"\A(?:{pattern})\z"); + let _ = writeln!(out, " let pattern = PATTERN_{}.get_or_init(|| regex::Regex::new({:?})).as_ref().map_err(|_| \"unsupported pattern syntax\")?;", i, full_pattern); out.push_str(" if !pattern.is_match(value) { return Err(\"pattern constraint failed\"); }\n"); } out.push_str(" Ok(())\n}\n"); @@ -1875,7 +1876,7 @@ impl RustCodegen { } else if self.field_is_bool(&field.type_ref, ir) { out.push_str(read); out.push_str(" let s = text.trim();\n"); - out.push_str(" let val = s == \"true\" || s == \"1\";\n"); + let _ = writeln!(out, " let val = match s {{ \"true\" | \"1\" => true, \"false\" | \"0\" => false, _ => return Err(PolyXmlError::ScalarParseError {{ field: {:?}.into(), expected: \"bool\", value: s.into() }}) }};", field.name); let _ = writeln!(out, " var_{} = Some(val);", rust_name); } else if let Some(enum_name) = self.field_is_enum(&field.type_ref, ir) { out.push_str(read); diff --git a/crates/polyxml-core/src/codegen/typescript/mod.rs b/crates/polyxml-core/src/codegen/typescript/mod.rs index ce5f2e2f..03f706e9 100644 --- a/crates/polyxml-core/src/codegen/typescript/mod.rs +++ b/crates/polyxml-core/src/codegen/typescript/mod.rs @@ -790,7 +790,10 @@ impl TypeScriptCodegen { zod_expr.push_str(&format!(".length({})", length)); } for pat in &facets.patterns { - zod_expr.push_str(&format!(".regex(new RegExp({:?}))", pat)); + zod_expr.push_str(&format!( + ".regex(new RegExp({:?}))", + format!("^(?:{pat})(?![\\s\\S])") + )); } } @@ -857,7 +860,10 @@ impl TypeScriptCodegen { actions.push(format!("v.length({})", length)); } for pat in &facets.patterns { - actions.push(format!("v.regex(/{}/)", pat.replace('/', "\\/"))); + actions.push(format!( + "v.regex(/^(?:{})(?![\\s\\S])/)", + pat.replace('/', "\\/") + )); } } if is_num { @@ -927,7 +933,10 @@ impl TypeScriptCodegen { opts.push(format!("minLength: {}, maxLength: {}", length, length)); } if facets.patterns.len() == 1 { - opts.push(format!("pattern: {:?}", facets.patterns[0])); + opts.push(format!( + "pattern: {:?}", + format!("^(?:{})(?![\\s\\S])", facets.patterns[0]) + )); } } if is_num { @@ -958,7 +967,12 @@ impl TypeScriptCodegen { let schemas = facets .patterns .iter() - .map(|p| format!("Type.String({{ pattern: {:?} }})", p)) + .map(|p| { + format!( + "Type.String({{ pattern: {:?} }})", + format!("^(?:{p})(?![\\s\\S])") + ) + }) .collect::>(); *expr = format!("Type.Intersect([{}, {}])", expr, schemas.join(", ")); } diff --git a/crates/polyxml-core/src/parser.rs b/crates/polyxml-core/src/parser.rs index 18615ae9..df8d472a 100644 --- a/crates/polyxml-core/src/parser.rs +++ b/crates/polyxml-core/src/parser.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use quick_xml::events::{BytesStart, Event}; @@ -9,6 +10,8 @@ use crate::error::{PolyXmlError, Result}; use crate::schema::{ModelSchema, ScalarType, ValueType}; use crate::value::PolyValue; +type NamespaceScope = Arc>; + pub(crate) struct StackFrame { schema: Arc, values: SmallVec<[Option; 8]>, @@ -101,20 +104,41 @@ fn is_nil_element(e: &BytesStart) -> bool { false } -/// Raw unescaped value of the `xsi:type` attribute on `e`, if present. -/// -/// Matching follows this parser's namespace-blind convention (like -/// `is_nil_element`): the attribute's local name must be `type`, so -/// `xsi:type`, any other prefixed `*:type`, or a bare `type` all match. -fn xsi_type_value(e: &BytesStart) -> Result>> { +/// Extend inherited namespace bindings for this element. Elements without +/// declarations share the existing map. +fn namespace_scope(parent: &NamespaceScope, e: &BytesStart) -> NamespaceScope { + let mut scope = None; + for attr in e.attributes().flatten() { + let key = attr.key.as_ref(); + if key == "xmlns" { + scope + .get_or_insert_with(|| (**parent).clone()) + .insert(String::new(), attr.value.as_ref().to_string()); + } else if let Some(prefix) = key.strip_prefix("xmlns:") { + scope + .get_or_insert_with(|| (**parent).clone()) + .insert(prefix.to_string(), attr.value.as_ref().to_string()); + } + } + scope.map(Arc::new).unwrap_or_else(|| Arc::clone(parent)) +} + +/// Raw unescaped value of the type attribute in the XML Schema Instance +/// namespace. The prefix is resolved from the element's namespace scope. +fn xsi_type_value(e: &BytesStart, scope: &HashMap) -> Result>> { for attr in e.attributes().flatten() { let raw_key = attr.key.as_ref(); if raw_key == "xmlns" || raw_key.starts_with("xmlns:") { continue; } - if attr.key.local_name().as_ref() == "type" { - let unescaped = quick_xml::escape::unescape(attr.value.as_ref())?; - return Ok(Some(unescaped.into_owned().into_bytes())); + if let Some(prefix) = raw_key.strip_suffix(":type") { + if scope + .get(prefix) + .is_some_and(|uri| uri == "http://www.w3.org/2001/XMLSchema-instance") + { + let unescaped = quick_xml::escape::unescape(attr.value.as_ref())?; + return Ok(Some(unescaped.into_owned().into_bytes())); + } } } Ok(None) @@ -128,20 +152,36 @@ fn xsi_type_value(e: &BytesStart) -> Result>> { /// without dispatch information keep today's behavior of parsing as /// declared, so a plain content attribute named `type` is never mistaken /// for dispatch on a non-abstract type. -fn resolve_record_schema(declared: &Arc, e: &BytesStart) -> Result> { +fn resolve_record_schema( + declared: &Arc, + e: &BytesStart, + scope: &NamespaceScope, +) -> Result> { if !declared.is_abstract && !declared.has_variants() { return Ok(Arc::clone(declared)); } - let raw = match xsi_type_value(e)? { + let raw = match xsi_type_value(e, scope)? { Some(raw) => raw, - None => return Ok(Arc::clone(declared)), + None if !declared.is_abstract => return Ok(Arc::clone(declared)), + None => { + return Err(PolyXmlError::SchemaError(format!( + "abstract type '{}' requires xsi:type naming a concrete derivation", + declared.name + ))) + } }; - let local: &[u8] = match raw.iter().rposition(|&b| b == b':') { - Some(i) => &raw[i + 1..], - None => &raw[..], + let (prefix, local): (&[u8], &[u8]) = match raw.iter().position(|&b| b == b':') { + Some(i) => (&raw[..i], &raw[i + 1..]), + None => (b"", &raw[..]), }; - if let Some(variant) = declared.find_variant(local) { - return Ok(variant); + let prefix = std::str::from_utf8(prefix).unwrap_or(""); + let namespace = scope.get(prefix).map(String::as_str); + if prefix.is_empty() || namespace.is_some() { + if let Some(variant) = declared.find_variant_qname(namespace.unwrap_or(""), local) { + if !variant.is_abstract { + return Ok(variant); + } + } } if declared.is_abstract { let value = String::from_utf8_lossy(&raw); @@ -213,10 +253,17 @@ impl XmlDeserializer { loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) => { - return Self::parse_sub_tree(&mut reader, root_schema, e, max_depth); + return Self::parse_sub_tree( + &mut reader, + root_schema, + e, + max_depth, + &Arc::new(HashMap::new()), + ); } Ok(Event::Empty(ref e)) => { - let schema = resolve_record_schema(&root_schema, e)?; + let scope = namespace_scope(&Arc::new(HashMap::new()), e); + let schema = resolve_record_schema(&root_schema, e, &scope)?; let mut frame = StackFrame::new(schema); Self::parse_attributes(e, &mut frame)?; return frame.finish(); @@ -243,9 +290,12 @@ impl XmlDeserializer { root_schema: Arc, root_start: &BytesStart, max_depth: usize, + inherited_scope: &NamespaceScope, ) -> Result { let mut stack: Vec = Vec::with_capacity(16); - let root_schema = resolve_record_schema(&root_schema, root_start)?; + let mut namespace_stack = vec![namespace_scope(inherited_scope, root_start)]; + let root_schema = + resolve_record_schema(&root_schema, root_start, namespace_stack.last().unwrap())?; let mut root_frame = StackFrame::new(root_schema); Self::parse_attributes(root_start, &mut root_frame)?; stack.push(root_frame); @@ -258,6 +308,7 @@ impl XmlDeserializer { loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) => { + namespace_stack.push(namespace_scope(namespace_stack.last().unwrap(), e)); if unknown_depth > 0 { unknown_depth += 1; continue; @@ -304,7 +355,11 @@ impl XmlDeserializer { } } ValueType::Nested(sub_schema) => { - let frame_schema = resolve_record_schema(sub_schema, e)?; + let frame_schema = resolve_record_schema( + sub_schema, + e, + namespace_stack.last().unwrap(), + )?; let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; stack.push(frame); @@ -312,7 +367,11 @@ impl XmlDeserializer { _ => {} }, ValueType::Nested(sub_schema) => { - let frame_schema = resolve_record_schema(sub_schema, e)?; + let frame_schema = resolve_record_schema( + sub_schema, + e, + namespace_stack.last().unwrap(), + )?; let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; stack.push(frame); @@ -327,6 +386,7 @@ impl XmlDeserializer { continue; } let local_name = e.local_name(); + let scope = namespace_scope(namespace_stack.last().unwrap(), e); let is_nil = is_nil_element(e); let current_schema = Arc::clone(&stack.last().unwrap().schema); @@ -354,7 +414,8 @@ impl XmlDeserializer { stack.last_mut().unwrap().push_list_item(field_idx, val); } ValueType::Nested(sub_schema) => { - let frame_schema = resolve_record_schema(sub_schema, e)?; + let frame_schema = + resolve_record_schema(sub_schema, e, &scope)?; let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; let instance = frame.finish()?; @@ -366,7 +427,7 @@ impl XmlDeserializer { _ => {} }, ValueType::Nested(sub_schema) => { - let frame_schema = resolve_record_schema(sub_schema, e)?; + let frame_schema = resolve_record_schema(sub_schema, e, &scope)?; let mut frame = StackFrame::new(frame_schema); Self::parse_attributes(e, &mut frame)?; let instance = frame.finish()?; @@ -420,6 +481,7 @@ impl XmlDeserializer { append_general_ref(e, active_scalar_field.is_some(), &mut text_buf, frame_tb)?; } Ok(Event::End(ref e)) => { + namespace_stack.pop(); if unknown_depth > 0 { unknown_depth -= 1; continue; @@ -505,6 +567,7 @@ pub struct XmlItemStream { target_tag: Vec, max_depth: usize, buf: Vec, + namespace_stack: Vec, } impl XmlItemStream { @@ -516,6 +579,7 @@ impl XmlItemStream { target_tag: target_tag.to_vec(), max_depth: DEFAULT_MAX_DEPTH, buf: Vec::new(), + namespace_stack: Vec::new(), } } @@ -534,27 +598,48 @@ impl XmlItemStream { if local.as_ref().as_bytes() == target_local || e.name().as_ref().as_bytes() == self.target_tag.as_slice() { + let inherited = self + .namespace_stack + .last() + .cloned() + .unwrap_or_else(|| Arc::new(HashMap::new())); let item = XmlDeserializer::parse_sub_tree( &mut self.reader, Arc::clone(&self.schema), e, self.max_depth, + &inherited, )?; return Ok(Some(item)); } + let inherited = self + .namespace_stack + .last() + .cloned() + .unwrap_or_else(|| Arc::new(HashMap::new())); + self.namespace_stack.push(namespace_scope(&inherited, e)); } Ok(Event::Empty(ref e)) => { let local = e.local_name(); if local.as_ref().as_bytes() == target_local || e.name().as_ref().as_bytes() == self.target_tag.as_slice() { - let schema = resolve_record_schema(&self.schema, e)?; + let inherited = self + .namespace_stack + .last() + .cloned() + .unwrap_or_else(|| Arc::new(HashMap::new())); + let scope = namespace_scope(&inherited, e); + let schema = resolve_record_schema(&self.schema, e, &scope)?; let mut frame = StackFrame::new(schema); XmlDeserializer::parse_attributes(e, &mut frame)?; return Ok(Some(frame.finish()?)); } } Ok(Event::Eof) => return Ok(None), + Ok(Event::End(_)) => { + self.namespace_stack.pop(); + } Err(err) => { return Err(PolyXmlError::XmlSyntaxError { position: self.reader.buffer_position(), diff --git a/crates/polyxml-core/src/schema.rs b/crates/polyxml-core/src/schema.rs index 7b632572..da75b055 100644 --- a/crates/polyxml-core/src/schema.rs +++ b/crates/polyxml-core/src/schema.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use std::sync::{Arc, OnceLock}; +use std::sync::{Arc, RwLock}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum FieldKind { @@ -82,11 +82,9 @@ pub struct ModelSchema { pub text_field: Option, /// Declared `abstract="true"` in the source schema (issue #53). pub is_abstract: bool, - /// Concrete derivations eligible for `xsi:type` dispatch, each keyed by - /// its type QName local part (stored in `xml_name`). Set once after the - /// schema is created: from `SchemaIR` extension chains in `from_ir`, or - /// from Python subclass hierarchies in the PyO3 layer. - variants: OnceLock>>, + /// Concrete derivations eligible for `xsi:type` dispatch. Python may + /// refresh this registry when subclasses are defined after first use. + variants: Arc>>>, } impl ModelSchema { @@ -95,35 +93,60 @@ impl ModelSchema { } /// Register the concrete derivations eligible for `xsi:type` dispatch. - /// Ignored on a second call (`OnceLock` semantics). + /// Replaces the registry when subclasses are discovered after first use. pub fn set_variants(&self, variants: Vec>) { - let _ = self.variants.set(variants); + *self.variants.write().unwrap_or_else(|p| p.into_inner()) = variants; } /// Registered derivations for `xsi:type` dispatch (empty when none). - pub fn variants(&self) -> &[Arc] { - self.variants.get().map(Vec::as_slice).unwrap_or(&[]) + pub fn variants(&self) -> Vec> { + self.variants + .read() + .unwrap_or_else(|p| p.into_inner()) + .clone() } /// Whether any `xsi:type` derivations are registered for this type. pub fn has_variants(&self) -> bool { - !self.variants().is_empty() + !self + .variants + .read() + .unwrap_or_else(|p| p.into_inner()) + .is_empty() } /// Look up a derivation by the QName local part of an `xsi:type` value. pub fn find_variant(&self, local: &[u8]) -> Option> { - self.variants() + self.variants + .read() + .unwrap_or_else(|p| p.into_inner()) .iter() .find(|v| v.xml_name.as_slice() == local) .cloned() } + /// Look up a derivation by its complete QName. + pub fn find_variant_qname(&self, namespace: &str, local: &[u8]) -> Option> { + self.variants + .read() + .unwrap_or_else(|p| p.into_inner()) + .iter() + .find(|v| { + v.xml_name.as_slice() == local && v.namespace.as_deref().unwrap_or("") == namespace + }) + .cloned() + } + /// Whether `candidate` is a registered derivation of `self`. pub fn matches_variant(&self, candidate: &ModelSchema) -> bool { - self.variants().iter().any(|v| { - std::ptr::eq(v.as_ref(), candidate) - || (v.xml_name == candidate.xml_name && v.namespace == candidate.namespace) - }) + self.variants + .read() + .unwrap_or_else(|p| p.into_inner()) + .iter() + .any(|v| { + std::ptr::eq(v.as_ref(), candidate) + || (v.xml_name == candidate.xml_name && v.namespace == candidate.namespace) + }) } /// Construct a runtime `ModelSchema` from a compiled `SchemaIR`. @@ -483,7 +506,7 @@ impl ModelSchemaBuilder { attribute_map, text_field, is_abstract: self.is_abstract, - variants: OnceLock::new(), + variants: Arc::new(RwLock::new(Vec::new())), }) } } diff --git a/crates/polyxml-core/src/serializer.rs b/crates/polyxml-core/src/serializer.rs index bf4565fa..6f0a95e8 100644 --- a/crates/polyxml-core/src/serializer.rs +++ b/crates/polyxml-core/src/serializer.rs @@ -116,7 +116,7 @@ impl NamespaceContext { uris.push(XSI_NS.to_string()); } for variant in variants { - Self::collect_namespaces(variant, uris); + Self::collect_namespaces(&variant, uris); } } } @@ -318,12 +318,17 @@ impl XmlSerializer { Some(ctx) => { let key = ctx.qualify_attribute("type", Some(XSI_NS)); if key.as_ref() == "type" { - "xsi:type".to_string() - } else { - key.into_owned() + return Err(PolyXmlError::SerializationError( + "xsi:type requires a namespace prefix".into(), + )); } + key.into_owned() + } + None => { + return Err(PolyXmlError::SerializationError( + "xsi:type requires namespaces to be enabled".into(), + )) } - None => "xsi:type".to_string(), }; elem.push_attribute((attr_key.as_str(), xsi_val.as_str())); } diff --git a/crates/polyxml-core/tests/test_issue51_audit.rs b/crates/polyxml-core/tests/test_issue51_audit.rs index 664a905c..52d27a04 100644 --- a/crates/polyxml-core/tests/test_issue51_audit.rs +++ b/crates/polyxml-core/tests/test_issue51_audit.rs @@ -451,9 +451,9 @@ fn issue51_item8_derived_simple_type_inherits_base_patterns() { py.contains(r#"AfterValidator(_polyxml_patterns(r"[A-Z]{6}[A-Z0-9]{3}", r"[A-Z]{6}"))"#), "both patterns must be AND-combined via AfterValidator:\n{py}" ); - // Single-pattern types keep the plain Field(pattern=...) kwarg. + // Single-pattern types keep Field(pattern=...), with full-value anchors. assert!( - py.contains(r#"Field(pattern=r"[A-Z]{6}")"#), + py.contains(r#"Field(pattern=r"\A(?:[A-Z]{6})\z")"#), "single-pattern Field kwarg missing:\n{py}" ); } diff --git a/crates/polyxml-core/tests/test_issue53_xsi_type.rs b/crates/polyxml-core/tests/test_issue53_xsi_type.rs index 238a83a7..76cc5ef6 100644 --- a/crates/polyxml-core/tests/test_issue53_xsi_type.rs +++ b/crates/polyxml-core/tests/test_issue53_xsi_type.rs @@ -135,7 +135,7 @@ fn issue53_from_ir_flattens_base_fields_and_registers_variants() { #[test] fn issue53_nested_xsi_type_dispatch_and_round_trip() { let root = schema_for(Some("Root")); - let xml = br#" + let xml = br#" V14 S1ore @@ -193,7 +193,7 @@ fn issue53_root_xsi_type_dispatch_including_empty_element() { assert!(root.is_abstract); assert!(root.has_variants()); - let xml = br#"T9ore"#; + let xml = br#"T9ore"#; let value = polyxml::deserialize(xml, Arc::clone(&root)).expect("root dispatch"); assert_eq!(schema_local(rec_schema(&value)), "Truck"); assert_str(rec_field(&value, "id"), "T9", "root id"); @@ -214,7 +214,7 @@ fn issue53_root_xsi_type_dispatch_including_empty_element() { assert_eq!(reparsed, value); // Empty elements dispatch through the root Empty branch too. - let empty = br#""#; + let empty = br#""#; let value = polyxml::deserialize(empty, root).expect("empty dispatch"); assert_eq!(schema_local(rec_schema(&value)), "Car"); } @@ -222,7 +222,7 @@ fn issue53_root_xsi_type_dispatch_including_empty_element() { #[test] fn issue53_unknown_xsi_type_on_abstract_type_errors() { let root = schema_for(Some("Root")); - let xml = br#"X"#; + let xml = br#"X"#; let err = polyxml::deserialize(xml, root).expect_err("unknown derivation must fail"); let msg = err.to_string(); assert!( @@ -251,7 +251,7 @@ fn issue53_abstract_type_without_derivations_errors() { assert!(root.is_abstract); assert!(!root.has_variants()); - let xml = br#"X"#; + let xml = br#"X"#; let err = polyxml::deserialize(xml, root).expect_err("must fail loudly"); let msg = err.to_string(); assert!( @@ -295,7 +295,7 @@ fn issue53_plain_type_attribute_is_not_mistaken_for_dispatch() { fn issue53_iterparse_dispatches_each_streamed_item() { let root = schema_for(Some("Root")); let declared = declared_vehicle(&root); - let xml = br#" + let xml = br#" A2 Bp @@ -317,3 +317,36 @@ fn issue53_iterparse_dispatches_each_streamed_item() { assert!(stream.next_item().expect("eof").is_none()); } + +#[test] +fn abstract_type_requires_concrete_selector() { + let root = schema_for(Some("Vehicle")); + for xml in [ + b"".as_slice(), + b"X".as_slice(), + ] { + let err = polyxml::deserialize(xml, Arc::clone(&root)).unwrap_err(); + assert!(err.to_string().contains("requires xsi:type"), "{err}"); + } +} + +#[test] +fn xsi_type_resolves_namespace_inherited_from_parent() { + let root = schema_for(Some("Root")); + let valid = br#"X2"#; + assert!(polyxml::deserialize(valid, Arc::clone(&root)).is_ok()); + let wrong = br#"X2"#; + assert!(polyxml::deserialize(wrong, Arc::clone(&root)).is_err()); + let wrong_attribute = br#"X2"#; + assert!(polyxml::deserialize(wrong_attribute, root).is_err()); +} + +#[test] +fn variant_serialization_requires_namespaces() { + let root = schema_for(Some("Vehicle")); + let xml = br#"X2"#; + let value = polyxml::deserialize(xml, Arc::clone(&root)).unwrap(); + let err = polyxml::serialize_with_options("Vehicle", &value, &root, None, Some(false), None) + .unwrap_err(); + assert!(err.to_string().contains("requires namespaces"), "{err}"); +} diff --git a/crates/polyxml-core/tests/test_pattern_codegen.rs b/crates/polyxml-core/tests/test_pattern_codegen.rs index e065d3fc..e8f1911f 100644 --- a/crates/polyxml-core/tests/test_pattern_codegen.rs +++ b/crates/polyxml-core/tests/test_pattern_codegen.rs @@ -14,9 +14,10 @@ const SCHEMA: &str = r#" - + + @@ -86,14 +87,14 @@ fn generated_go_pattern_validation_and_xml_roundtrip() { fs::write(dir.path().join("models_test.go"), r#"package models import("testing"; "encoding/xml") func TestPatterns(t *testing.T) { - for _, value := range []string{"ZZZ", "999", "xZZZy"} { + for _, value := range []string{"ZZZ", "999"} { if err := Mixed(value).Validate(); err != nil { t.Fatal(value, err) } var doc Document if err := xml.Unmarshal([]byte(""+value+""), &doc); err != nil { t.Fatal(err) } if err := doc.Validate(); err != nil { t.Fatal(err) } if _, err := xml.Marshal(doc); err != nil { t.Fatal(err) } } - for _, value := range []string{"ABC", "123", "Z", "9", "abc", ""} { + for _, value := range []string{"ABC", "123", "Z", "9", "abc", "xZZZy", ""} { if Mixed(value).Validate() == nil { t.Fatal("accepted", value) } var doc Document if xml.Unmarshal([]byte(""+value+""), &doc) == nil { t.Fatal("read accepted", value) } @@ -123,8 +124,8 @@ fn generated_cpp_pattern_validation() { #include using namespace polyxml::generated; int main() { - for (auto value : {"ZZZ", "999", "xZZZy"}) { assert(validate_Mixed_patterns(value)); Document doc; doc.value=value; assert(doc.validate()); } - for (auto value : {"ABC", "123", "Z", "9", "abc", ""}) { assert(!validate_Mixed_patterns(value)); Document doc; doc.value=value; assert(!doc.validate()); } + for (auto value : {"ZZZ", "999"}) { assert(validate_Mixed_patterns(value)); Document doc; doc.value=value; assert(doc.validate()); } + for (auto value : {"ABC", "123", "Z", "9", "abc", "xZZZy", ""}) { assert(!validate_Mixed_patterns(value)); Document doc; doc.value=value; assert(!doc.validate()); } assert(validate_ZBase_patterns("ABC")); assert(validate_ZBase_patterns("123")); Document doc; doc.value="ZZZ"; doc.optional="x"; assert(!doc.validate()); doc.optional.reset(); doc.items.push_back("x"); assert(!doc.validate()); @@ -169,13 +170,13 @@ regex="1" r#"mod models; use models::*; fn main() { - for value in ["ZZZ", "999", "xZZZy"] { + for value in ["ZZZ", "999"] { assert!(validate_Mixed_patterns(value).is_ok()); let xml=format!("{value}"); let doc=Document::from_xml(&xml).unwrap(); assert!(doc.to_xml().is_ok()); } - for value in ["ABC", "123", "Z", "9", "abc", ""] { + for value in ["ABC", "123", "Z", "9", "abc", "xZZZy", ""] { assert!(validate_Mixed_patterns(value).is_err()); let xml=format!("{value}"); assert!(Document::from_xml(&xml).is_err()); @@ -186,6 +187,9 @@ fn main() { assert!(Document::from_xml("").is_err()); assert!(validate_ZBase_patterns("ABC").is_ok()); assert!(validate_ZBase_patterns("123").is_ok()); + assert!(Flag::from_xml("true").is_ok()); + assert!(Flag::from_xml("0").is_ok()); + assert!(Flag::from_xml("garbage").is_err()); } "#, ) @@ -220,9 +224,9 @@ fn generated_python_pattern_validation() { r#"from models import Mixed, Derived, ZBase from pydantic import TypeAdapter, ValidationError mixed = TypeAdapter(Mixed) -for value in ('ZZZ', '999', 'xZZZy'): +for value in ('ZZZ', '999'): assert mixed.validate_python(value) == value -for value in ('ABC', '123', 'Z', '9', 'abc', ''): +for value in ('ABC', '123', 'Z', '9', 'abc', 'xZZZy', ''): try: mixed.validate_python(value) except ValidationError: @@ -257,13 +261,13 @@ fn generated_java_pattern_validation_and_direct_codecs() { import java.io.*; public class Check { public static void main(String[] args) throws Exception { - for (String value : new String[]{"ZZZ", "999", "xZZZy"}) { + for (String value : new String[]{"ZZZ", "999"}) { new Mixed(value); var parsed = MixedCodec.readXml(new ByteArrayInputStream((""+value+"").getBytes())); if (!parsed.value().equals(value)) throw new AssertionError(); MixedCodec.writeXml(parsed, new ByteArrayOutputStream()); } - for (String value : new String[]{"ABC", "123", "Z", "9", "abc", ""}) { + for (String value : new String[]{"ABC", "123", "Z", "9", "abc", "xZZZy", ""}) { try { new Mixed(value); throw new AssertionError(value); } catch (IllegalArgumentException expected) { } } @@ -296,8 +300,8 @@ fn generated_csharp_pattern_validation() { fs::write(dir.path().join("Program.cs"), r#"using Generated; using System.ComponentModel.DataAnnotations; static bool Valid(IValidatableObject value) => !value.Validate(new ValidationContext(value)).Any(); -foreach(var value in new[]{"ZZZ", "999", "xZZZy"}) if(!Valid(new Mixed(value))) throw new Exception(value); -foreach(var value in new[]{"ABC", "123", "Z", "9", "abc", ""}) if(Valid(new Mixed(value))) throw new Exception(value); +foreach(var value in new[]{"ZZZ", "999"}) if(!Valid(new Mixed(value))) throw new Exception(value); +foreach(var value in new[]{"ABC", "123", "Z", "9", "abc", "xZZZy", ""}) if(Valid(new Mixed(value))) throw new Exception(value); if(!Valid(new ZBase("ABC")) || !Valid(new ZBase("123"))) throw new Exception("OR"); "#).unwrap(); run(Command::new("dotnet") @@ -335,10 +339,10 @@ import { MixedSchema as v, ZBaseSchema as vb } from './valibot'; import { MixedSchema as t, ZBaseSchema as tb } from './typebox'; import * as valibot from 'valibot'; import { Value } from '@sinclair/typebox/value'; -for(const value of ['ZZZ','999','xZZZy']) { +for(const value of ['ZZZ','999']) { if(!z.safeParse(value).success || !valibot.safeParse(v,value).success || !Value.Check(t,value)) throw Error(value); } -for(const value of ['ABC','123','Z','9','abc','']) { +for(const value of ['ABC','123','Z','9','abc','xZZZy','']) { if(z.safeParse(value).success || valibot.safeParse(v,value).success || Value.Check(t,value)) throw Error(value); } for(const value of ['ABC','123']) { diff --git a/crates/polyxml-core/tests/test_python_codegen.rs b/crates/polyxml-core/tests/test_python_codegen.rs index c13871d8..1672cbcc 100644 --- a/crates/polyxml-core/tests/test_python_codegen.rs +++ b/crates/polyxml-core/tests/test_python_codegen.rs @@ -233,7 +233,7 @@ fn test_python_pydantic_codegen_with_facets() { assert!(code.contains("type Age = Annotated[int, Field(ge=0, le=120)]")); assert!(code.contains("class User(BaseModel):")); assert!(code.contains("model_config = ConfigDict(defer_build=True, populate_by_name=True)")); - assert!(code.contains("username: str = Field(..., json_schema_extra={\"type\": \"Element\", \"name\": \"username\", \"json_name\": \"username\"}, min_length=3, max_length=20, pattern=r\"^[a-zA-Z0-9_]+$\")")); + assert!(code.contains("username: str = Field(..., json_schema_extra={\"type\": \"Element\", \"name\": \"username\", \"json_name\": \"username\"}, min_length=3, max_length=20, pattern=r\"\\A(?:^[a-zA-Z0-9_]+$)\\z\")")); assert!(code.contains( "user_age: Age = Field(..., alias=\"age\", serialization_alias=\"age\", json_schema_extra={\"type\": \"Element\", \"name\": \"age\", \"json_name\": \"age\"})" )); diff --git a/crates/polyxml-core/tests/test_ts_codegen.rs b/crates/polyxml-core/tests/test_ts_codegen.rs index bcbae4c2..f878630a 100644 --- a/crates/polyxml-core/tests/test_ts_codegen.rs +++ b/crates/polyxml-core/tests/test_ts_codegen.rs @@ -296,7 +296,7 @@ fn test_ts_zod_schema_generation() { assert!(code.contains("import { z } from \"zod\";")); // Verify PostalCodeSchema - assert!(code.contains("export const PostalCodeSchema = z.string().min(3).max(10).regex(new RegExp(\"^[A-Z0-9]+$\"));")); + assert!(code.contains("export const PostalCodeSchema = z.string().min(3).max(10).regex(new RegExp(\"^(?:^[A-Z0-9]+$)(?![\\\\s\\\\S])\"));")); // Verify AgeSchema assert!(code.contains("export const AgeSchema = z.number().int().gte(18).lte(120);")); @@ -501,7 +501,7 @@ fn test_ts_valibot_schema_generation() { assert!(code.contains("import * as v from \"valibot\";")); // Verify simple type facets with v.pipe - assert!(code.contains("export const PostalCodeSchema = v.pipe(v.string(), v.minLength(3), v.maxLength(10), v.regex(/^[A-Z0-9]+$/));")); + assert!(code.contains("export const PostalCodeSchema = v.pipe(v.string(), v.minLength(3), v.maxLength(10), v.regex(/^(?:^[A-Z0-9]+$)(?![\\s\\S])/));")); // Verify enum picklist assert!(code.contains("export const StatusSchema = v.picklist([\"active\", \"inactive\"]);")); diff --git a/crates/polyxml-python/src/lib.rs b/crates/polyxml-python/src/lib.rs index 9d2137a7..21064759 100644 --- a/crates/polyxml-python/src/lib.rs +++ b/crates/polyxml-python/src/lib.rs @@ -6,6 +6,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList, PyString, PyTuple, PyType}; use pyo3::IntoPyObjectExt; +use std::cell::Cell; use std::collections::{HashMap, HashSet}; use std::sync::{Arc, RwLock}; @@ -37,6 +38,46 @@ struct CachedSchemaMeta { static SCHEMA_CACHE: RwLock>>> = RwLock::new(None); // Cache keyed by schema name to resolve nested Python types during deserialization static CLASS_BY_SCHEMA: RwLock>>> = RwLock::new(None); +thread_local! { static DISCOVERING_VARIANTS: Cell = const { Cell::new(false) }; } + +fn refresh_variants(cls: &Bound<'_, PyType>, meta: &CachedSchemaMeta) { + DISCOVERING_VARIANTS.with(|busy| { + if busy.replace(true) { + return; + } + let mut seen = HashSet::new(); + refresh_variant_tree(cls, &meta.schema, &mut seen); + busy.set(false); + }); +} + +fn refresh_variant_tree( + cls: &Bound<'_, PyType>, + schema: &Arc, + seen: &mut HashSet, +) { + if !seen.insert(Arc::as_ptr(schema) as usize) { + return; + } + schema.set_variants(discover_variants(cls)); + for field in &schema.fields { + let nested = match &field.val_type { + ValueType::Nested(nested) => Some(nested), + ValueType::List(inner) => match inner.as_ref() { + ValueType::Nested(nested) => Some(nested), + _ => None, + }, + _ => None, + }; + if let Some(nested) = nested { + if let Some(meta) = lookup_cached_meta(&nested.name) { + if let Ok(child_cls) = meta.py_cls.bind(cls.py()).cast::() { + refresh_variant_tree(child_cls, &meta.schema, seen); + } + } + } + } +} fn lookup_cached_meta(schema_name: &str) -> Option> { let class_map = CLASS_BY_SCHEMA.read().unwrap_or_else(|p| p.into_inner()); @@ -397,7 +438,10 @@ fn get_or_create_schema_meta<'py>(cls: &Bound<'py, PyType>) -> PyResult(cls: &Bound<'py, PyType>) -> PyResult" + b'' b'V14' b'S1ore' b'S22' @@ -122,7 +122,7 @@ def test_nested_xsi_type_round_trip(): def test_root_xsi_type_dispatch(): - xml = b'T9ore' + xml = b'T9ore' vehicle = polyxml.deserialize(xml, Vehicle) assert type(vehicle) is Truck @@ -150,14 +150,14 @@ def test_concrete_serialize_without_target_type(): def test_unknown_xsi_type_raises(): - xml = b'X' + xml = b'X' with pytest.raises(ValueError, match="known derivation"): polyxml.deserialize(xml, Fleet) def test_abstract_without_derivations_raises(): - xml = b'X' + xml = b'X' with pytest.raises(ValueError, match="no registered derivations"): polyxml.deserialize(xml, LonelyBase) @@ -200,7 +200,7 @@ class PFleet(BaseModel): def test_pydantic_xsi_type_dispatch_and_round_trip(): - xml = b'V92' + xml = b'V92' fleet = polyxml.deserialize(xml, PFleet) assert type(fleet.vehicle) is PCar @@ -216,7 +216,7 @@ def test_pydantic_xsi_type_dispatch_and_round_trip(): def test_iterparse_xsi_type_dispatch(): xml = ( - b"" + b'' b'A2' b'Bp' b"" @@ -229,9 +229,46 @@ def test_iterparse_xsi_type_dispatch(): def test_xml_to_json_keeps_variant_fields(): - xml = b'V14' + xml = b'V14' out = polyxml.xml_to_json(xml, target_type=Fleet) data = json.loads(out) assert data["vehicle"]["id"] == "V1" assert data["vehicle"]["doors"] == 4 + + +def test_subclass_defined_after_base_was_cached_dispatches(): + @dataclass + class LateBase: + class Meta: + name = "LateBase" + abstract = True + + id: str = field(metadata={"type": "Element", "name": "id"}) + + @dataclass + class LateFleet: + class Meta: + name = "LateFleet" + + vehicle: LateBase = field(metadata={"type": "Element", "name": "vehicle"}) + + xml = b'XY' + with pytest.raises(ValueError): + polyxml.deserialize(xml, LateBase) + fleet_xml = b'XY' + with pytest.raises(ValueError): + polyxml.deserialize(fleet_xml, LateFleet) + + @dataclass + class LateChild(LateBase): + class Meta: + name = "LateChild" + + extra: str = field(metadata={"type": "Element", "name": "extra"}) + + value = polyxml.deserialize(xml, LateBase) + assert isinstance(value, LateChild) + assert value.extra == "Y" + fleet = polyxml.deserialize(fleet_xml, LateFleet) + assert isinstance(fleet.vehicle, LateChild) diff --git a/docs/guides/compiler.md b/docs/guides/compiler.md index b07db0d8..4ea4b6a8 100644 --- a/docs/guides/compiler.md +++ b/docs/guides/compiler.md @@ -93,7 +93,7 @@ including with `--dry-run`. | Target | Backends (first is default) | Styles | Features | | --- | --- | --- | --- | | Python | `dataclass`, `pydantic` | `dataclass` (dataclass backend only) | `slots`, `kw-only` (dataclass backend only) | -| Rust | `standard` | — | `zero-copy`, `rkyv` | +| Rust | `standard` | — | `zero-copy`, `rkyv`, `phf` | | TypeScript | `interfaces`, `zod`, `valibot`, `typebox` | — | — | | Java | `standard`, `jackson` | `record` (default), `pojo` (alias `class`) | `builder`, `direct-codec` | | C# | `standard`, `source-gen` | `record-class` (default), `record-struct`, `class` (mutable); legacy aliases `record`, `pojo` | — | @@ -101,7 +101,7 @@ including with `--dry-run`. | Go | `standard`, `easyjson`, `sonic` | — | — | Defaults are unchanged: Rust zero-copy and Python slots/keyword-only fields are -already enabled. Future features such as `phf` and `aot`, Python plain `class`, +already enabled. Future features such as `aot`, Python plain `class`, and C# mutable `struct` are rejected until their generators support them. ```bash diff --git a/docs/guides/polymorphism.md b/docs/guides/polymorphism.md index d4ffa8f0..2efd2d69 100644 --- a/docs/guides/polymorphism.md +++ b/docs/guides/polymorphism.md @@ -24,11 +24,10 @@ The registry is populated from whichever schema source built the model: | **XSD** (`ModelSchema::from_ir`) | All *transitive* `xs:extension` derivations of the type are built recursively from the `SchemaIR`. Derived schemas **flatten the full base-chain content model**: inherited fields precede the type's own fields. | | **Python classes** (PyO3) | A `__subclasses__()` walk of the dataclass/Pydantic hierarchy, keyed by `Meta.name` (falling back to the class name). Generated Python already emits real class inheritance (`class Car(Vehicle):`), so inherited fields and metadata flow automatically. | -**Matching is namespace-blind**, consistent with the rest of the runtime: -the parser matches elements and attributes by local name, so an `xsi:type` -QName value is reduced to its local part before lookup. Both prefixed -(`xsi:type="t:Car"` / `xsi:type="p:Car"`) and bare (`type="Car"`) wire -forms are accepted — the attribute's local name must simply be `type`. +`xsi:type` is resolved as a QName using namespace declarations in scope at +the element. The attribute itself must be bound to the XML Schema Instance +namespace. A type with the same local name in a different namespace does not +select a registered variant. --- @@ -40,6 +39,7 @@ Dispatch is applied at **every frame creation point**: the document root - `xsi:type` names a registered derivation → the element is parsed with the concrete schema, so **base fields and concrete fields both survive**. +- An abstract declaration without `xsi:type` is rejected. - `xsi:type` names an unknown type on an **abstract** base → a loud `ValueError` listing the known derivations: ``` @@ -98,7 +98,7 @@ class Fleet: name = "Fleet" vehicle: Vehicle = field(metadata={"type": "Element", "name": "vehicle"}) -xml = b'V14' +xml = b'V14' fleet = polyxml.deserialize(xml, Fleet) # fleet.vehicle is a Car assert fleet.vehicle.doors == 4 # concrete field preserved