Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .agents/skills/polyxml-codegen-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ C++ uses fixed-width octal escapes for ASCII controls. Execute
`test_enum_literal_codegen` with all seven toolchains to verify exact runtime
values and Rust Serde round trips, not just whether generated files exist.

Schema defaults and fixed values need the same literal escaping as enums.
Python metadata must not use Rust Debug escapes, and C++ defaults need C++
control-character escapes. C# defaults parsed through an internal XML element
must encode carriage returns as `
` after escaping XML markup; otherwise XML
line-ending normalization changes the lexical value. Execute generated consumers
against independent UTF-8 expected data, including backslash-u text, referenced
whitespace, Unicode line separators, empty-element defaults and fixed rejection.

## C# runtime availability for smoke checks

The C# execution fixtures target `net8.0`. An installed .NET 10 SDK/runtime
Expand Down Expand Up @@ -962,3 +970,11 @@ owned/borrowed XML and JSON round trips, use the declared root name explicitly
when it differs from its type name, and distinguish C++/TypeScript model-only
generation from their runtime binding schemas. Strict docs builds catch broken
anchors and misplaced code fences, including guides appended after old fences.

When Python generated-model tests use an isolated absolute CARGO_TARGET_DIR,
resolve the CLI under that target, not the checkout's default target directory
or an unrelated global executable. Relative target values resolve from the
checkout because the helper builds with that cwd. The generated-model test
helper now honors this override and builds the matching CLI if needed. Keep
workspace targets owned by one worktree; switching checkouts in a shared target
can leave stale unchanged executables even after a core compile message.
6 changes: 4 additions & 2 deletions crates/polyxml-core/src/codegen/cpp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,9 @@ inline int polyxml_integer_compare(std::string_view a,std::string_view b) noexce
// Value field
let init = match &f.default_value {
Some(v) => match f.type_ref {
TypeRef::Primitive(p) if p.is_unbounded_integer() => format!(" = {v:?}"),
TypeRef::Primitive(p) if p.is_unbounded_integer() => {
format!(" = {}", super::cpp_string_literal(v))
}
TypeRef::Primitive(PrimitiveType::Boolean) => {
format!(" = {}", v.to_lowercase())
}
Expand All @@ -1019,7 +1021,7 @@ inline int polyxml_integer_compare(std::string_view a,std::string_view b) noexce
TypeRef::Primitive(PrimitiveType::String)
| TypeRef::Primitive(PrimitiveType::Token)
| TypeRef::Primitive(PrimitiveType::NormalizedString) => {
format!(" = \"{}\"", v.replace('\\', "\\\\").replace('"', "\\\""))
format!(" = {}", super::cpp_string_literal(v))
}
TypeRef::Primitive(_) => format!(" = {}", v),
TypeRef::Named(_) => format!(" = {}", v),
Expand Down
11 changes: 7 additions & 4 deletions crates/polyxml-core/src/codegen/csharp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1304,10 +1304,10 @@ impl CSharpCodegen {
if let Some(kind) = integer_kind {
return format!("PolyxmlIntegerLexical.Validate({text},{kind})");
}
format!("({base})new XmlSerializer(typeof({base}), new XmlRootAttribute(\"value\")).Deserialize(new System.IO.StringReader(\"<value>\" + System.Security.SecurityElement.Escape({text}) + \"</value>\"))!")
format!("({base})new XmlSerializer(typeof({base}), new XmlRootAttribute(\"value\")).Deserialize(new System.IO.StringReader(\"<value>\" + System.Security.SecurityElement.Escape({text}).Replace(\"\\r\", \"&#13;\") + \"</value>\"))!")
};
if let Some(fixed) = &field.fixed_value {
let expected = parse(&format!("{fixed:?}"));
let expected = parse(&super::string_literal(fixed));
let initial = if field.cardinality.is_optional() && field.kind == FieldKind::Element {
"default!".to_string()
} else {
Expand All @@ -1327,7 +1327,7 @@ impl CSharpCodegen {
{indent} [XmlIgnore] public {ty} {name} {{ get => _{name}; set {{ if ({present}!({equal})) throw new System.ComponentModel.DataAnnotations.ValidationException(\"Fixed value constraint violated for {name}\"); _{name} = value; }} }}").unwrap();
} else {
let initial = if field.kind == FieldKind::Attribute {
parse(&format!("{default:?}"))
parse(&super::string_literal(default))
} else {
"default!".to_string()
};
Expand Down Expand Up @@ -1361,7 +1361,10 @@ impl CSharpCodegen {
let text = if field.kind == FieldKind::Attribute {
"value".into()
} else {
format!("value.Length == 0 ? {default:?} : value")
format!(
"value.Length == 0 ? {} : value",
super::string_literal(default)
)
};
let parsed = parse("text");
writeln!(out, "{indent} set {{ if (value is null) {{ {name} = default!; return; }} var text = {text}; {name} = {parsed}; }}
Expand Down
14 changes: 7 additions & 7 deletions crates/polyxml-core/src/codegen/python/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,10 +1012,10 @@ impl PythonCodegen {
}

if let Some(ref fixed) = field.fixed_value {
parts.push(format!("\"fixed\": {fixed:?}"));
parts.push(format!("\"fixed\": {}", super::string_literal(fixed)));
}
if let Some(ref default) = field.default_value {
parts.push(format!("\"default\": {default:?}"));
parts.push(format!("\"default\": {}", super::string_literal(default)));
}
if matches!(super::primitive_base(&field.type_ref, ir), TypeRef::List(_)) {
parts.push("\"tokens\": True".into());
Expand Down Expand Up @@ -1045,7 +1045,7 @@ impl PythonCodegen {
fn format_default_value(&self, val: &str, type_ref: &TypeRef, ir: &SchemaIR) -> String {
if matches!(super::primitive_base(type_ref, ir), TypeRef::Primitive(p) if p.is_unbounded_integer())
{
return format!("int({val:?})");
return format!("int({})", super::string_literal(val));
}

let base = super::primitive_base(type_ref, ir);
Expand Down Expand Up @@ -1073,20 +1073,20 @@ impl PythonCodegen {
if val.trim().parse::<i64>().is_ok() {
val.trim().to_string()
} else {
format!("\"{}\"", val.replace('"', "\\\""))
super::string_literal(val)
}
}
TypeRef::Primitive(PrimitiveType::Float | PrimitiveType::Double) => {
if val.trim().parse::<f64>().is_ok() {
val.trim().to_string()
} else {
format!("\"{}\"", val.replace('"', "\\\""))
super::string_literal(val)
}
}
TypeRef::Primitive(PrimitiveType::Decimal) => {
format!("Decimal(\"{}\")", val.replace('"', "\\\""))
format!("Decimal({})", super::string_literal(val))
}
_ => format!("\"{}\"", val.replace('"', "\\\"")),
_ => super::string_literal(val),
}
}

Expand Down
150 changes: 150 additions & 0 deletions crates/polyxml-core/tests/test_default_literal_codegen.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//! Execute consumers: escaping must preserve values as well as valid source.
use polyxml::codegen::*;
use polyxml::schema_parser::XsdParser;
use std::{fs, path::Path, process::Command};

fn run(directory: &Path, program: &str, args: &[&str]) {
let output = Command::new(program)
.args(args)
.current_dir(directory)
.output()
.unwrap();
assert!(
output.status.success(),
"{program} {args:?}:\n{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}

#[test]
fn generated_string_defaults_and_fixed_values_preserve_xml_characters() {
let ir = XsdParser::new()
.parse_str(include_str!(
"../../../research/fixtures/default_literal_escaping.xsd"
))
.unwrap();
let expected = "\"quoted\"\\path\\u000a\t\n\r\u{85}\u{2028}café 😀";
let root = tempfile::tempdir().unwrap();
let consumer = |name: &str| {
let path = root.path().join(name);
fs::create_dir(&path).unwrap();
fs::write(path.join("expected.txt"), expected).unwrap();
path
};
let python = consumer("python");
fs::write(
python.join("models.py"),
PythonCodegen::new(PythonOptions::default()).generate_module(&ir),
)
.unwrap();
fs::write(
python.join("check.py"),
r#"import dataclasses
from models import RootType
with open('expected.txt', encoding='utf-8', newline='') as f:
expected = f.read()
model = RootType(fixed=expected)
assert model.value == expected, repr(model.value)
assert model.label == expected, repr(model.label)
fields = {f.name: f for f in dataclasses.fields(RootType)}
assert fields['value'].metadata['default'] == expected
assert fields['label'].metadata['default'] == expected
assert fields['fixed'].metadata['fixed'] == expected
"#,
)
.unwrap();
run(&python, "python3", &["check.py"]);
let pydantic = consumer("pydantic");
fs::write(
pydantic.join("models.py"),
PythonCodegen::new(PythonOptions {
backend: PythonBackend::Pydantic,
..Default::default()
})
.generate_module(&ir),
)
.unwrap();
fs::write(
pydantic.join("check.py"),
r#"from models import RootType
with open('expected.txt', encoding='utf-8', newline='') as f:
expected = f.read()
model = RootType(fixed=expected)
assert model.value == expected
assert model.label == expected
assert RootType.model_fields['value'].json_schema_extra['default'] == expected
assert RootType.model_fields['fixed'].json_schema_extra['fixed'] == expected
"#,
)
.unwrap();
run(&pydantic, "python3", &["check.py"]);
let cpp = consumer("cpp");
fs::write(
cpp.join("models.hpp"),
CppCodegen::new(CppOptions::default()).generate_header(&ir),
)
.unwrap();
fs::write(
cpp.join("main.cpp"),
r#"#include "models.hpp"
#include <fstream>
#include <iterator>
#include <stdexcept>
int main() {
std::ifstream input("expected.txt", std::ios::binary);
std::string expected((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
polyxml::generated::RootType model;
if(model.value != expected) throw std::runtime_error("default mismatch");
}
"#,
)
.unwrap();
run(
&cpp,
"g++",
&[
"-std=c++20",
"-Wall",
"-Wextra",
"-Werror",
"main.cpp",
"-o",
"check",
],
);
run(&cpp, "./check", &[]);
let csharp = consumer("csharp");
fs::write(csharp.join("Check.csproj"), "<Project Sdk=\"Microsoft.NET.Sdk\"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><TreatWarningsAsErrors>true</TreatWarningsAsErrors></PropertyGroup></Project>").unwrap();
fs::write(
csharp.join("Models.cs"),
CSharpCodegen::new(CSharpOptions {
namespace: "Models".into(),
..Default::default()
})
.generate_module(&ir),
)
.unwrap();
fs::write(
csharp.join("Program.cs"),
r#"using Models;
using System.ComponentModel.DataAnnotations;
var expected = File.ReadAllText("expected.txt");
var model = new RootType();
if(model.Label != expected || model.Fixed != expected) throw new Exception("initializer mismatch");
model.ValueXml = "";
if(model.Value != expected) throw new Exception("element default mismatch");
model.Fixed = expected;
try { model.Fixed = expected + "wrong"; throw new Exception("fixed value accepted"); }
catch(ValidationException) {}
model.LabelXml = expected;
if(model.Label != expected) throw new Exception("attribute proxy mismatch");
"#,
)
.unwrap();
run(
&csharp,
"dotnet",
&["run", "--project", "Check.csproj", "--verbosity", "quiet"],
);
}
64 changes: 57 additions & 7 deletions crates/polyxml-python/tests/test_generated_models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import importlib.util
import os
import pathlib
import shutil
import subprocess
Expand Down Expand Up @@ -262,17 +263,20 @@ def _load_module_from_file(module_name: str, file_path: pathlib.Path):
def _get_polyxml_bin() -> pathlib.Path:
repo_root = pathlib.Path(__file__).parent.parent.parent.parent
exe_name = "polyxml.exe" if sys.platform == "win32" else "polyxml"
candidates = [
repo_root / "target" / "debug" / exe_name,
repo_root / "target" / "release" / exe_name,
]
configured_target = os.environ.get("CARGO_TARGET_DIR")
target_dir = pathlib.Path(configured_target or "target")
if not target_dir.is_absolute():
target_dir = repo_root / target_dir
target_dir = target_dir.resolve()
candidates = [target_dir / profile / exe_name for profile in ("debug", "release")]
for c in candidates:
if c.exists():
return c

which_path = shutil.which(exe_name) or shutil.which("polyxml")
if which_path:
return pathlib.Path(which_path)
if not configured_target:
which_path = shutil.which(exe_name) or shutil.which("polyxml")
if which_path:
return pathlib.Path(which_path)

# Attempt on-the-fly compilation via cargo if not found
subprocess.run(
Expand All @@ -288,6 +292,52 @@ def _get_polyxml_bin() -> pathlib.Path:
raise FileNotFoundError(f"polyxml CLI binary could not be found or built at {candidates}")


@pytest.mark.parametrize("relative", [False, True])
def test_cli_discovery_respects_cargo_target_directory(tmp_path, monkeypatch, relative):
repo_root = tmp_path / "repo"
monkeypatch.setitem(
_get_polyxml_bin.__globals__,
"__file__",
str(repo_root / "crates/polyxml-python/tests/test_generated_models.py"),
)
exe_name = "polyxml.exe" if sys.platform == "win32" else "polyxml"
stale = repo_root / "target/debug" / exe_name
stale.parent.mkdir(parents=True)
stale.write_text("old revision")
matching = repo_root / "custom-target/debug" / exe_name
matching.parent.mkdir(parents=True)
matching.write_text("matching revision")
monkeypatch.setenv(
"CARGO_TARGET_DIR", "custom-target" if relative else str(matching.parent.parent)
)
monkeypatch.setattr(shutil, "which", lambda _: str(stale))
assert _get_polyxml_bin() == matching


def test_configured_target_builds_matching_cli_instead_of_using_global(tmp_path, monkeypatch):
repo_root = tmp_path / "repo"
monkeypatch.setitem(
_get_polyxml_bin.__globals__,
"__file__",
str(repo_root / "crates/polyxml-python/tests/test_generated_models.py"),
)
exe_name = "polyxml.exe" if sys.platform == "win32" else "polyxml"
matching = repo_root / "custom-target/debug" / exe_name
monkeypatch.setenv("CARGO_TARGET_DIR", str(matching.parent.parent))
monkeypatch.setattr(shutil, "which", lambda _: str(tmp_path / "old-global-cli"))
builds = []

def build(command, *, cwd, check, capture_output):
builds.append((command, cwd))
assert check and capture_output
matching.parent.mkdir(parents=True)
matching.write_text("matching revision")

monkeypatch.setattr(subprocess, "run", build)
assert _get_polyxml_bin() == matching
assert builds == [(["cargo", "build", "-p", "polyxml-cli"], repo_root)]


@pytest.fixture(scope="module")
def generated_models():
"""Generates both Dataclass and Pydantic models from SAMPLE_XSD using polyxml CLI,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
memcap: <=7711 MiB (60% of 12853 MiB available) [systemd MemoryMax, swap off]
╭────────────────────────────────────────────────────────────╮
│ PolyXML W3C XML Schema Conformance Suite │
│ Compiler Binary: /tmp/polyxml-followup-default-bin/polyxml │
│ W3C Submodule: /home/bailey/github/polyxml-w3c-tests/w3c │
│ Selected Suite: AttrDecl │
╰────────────────────────────────────────────────────────────╯
Executing 50 test group(s)...

Testing AttrDecl/ad_name00108m5... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00

W3C XML Schema Conformance Summary
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━┓
┃ Category ┃ Count ┃ Rate ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━┩
│ Total Test Groups │ 50 │ 100.0% │
│ Schema Compilations Passed │ 50 │ 100.0% │
│ Schema Compilations Failed │ 0 │ 0.0% │
│ Valid Instances Evaluated │ 50 │ - │
│ Roundtrip Validations Passed │ 50 │ 100.0% │
└──────────────────────────────┴───────┴────────┘

All executed tests passed with 100% compliance!
Loading