Skip to content
Merged
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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ jobs:
fi
echo "schematron-invalid correctly failed"

- name: Schematron - .NET variant (drives the SchXslt CLI jar from the Maven local repo)
run: |
set -e
SCH=Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch
D="dotnet run --project Schematron_DataQuality_Checks/Basic_Checks/invocation --"
$D $SCH FundsXML_Files/4.2.9/positions/Mixed-Fund_Positions.xml
if $D $SCH tests/fixtures/invalid/schematron-invalid_Positions.xml; then
echo "::error::schematron-invalid fixture unexpectedly passed (.NET)"; exit 1
fi
echo ".NET SchematronValidate: sample ok, fixture correctly failed"

- name: Smoke - XSLT transforms produce non-empty output
run: |
set -e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ exits 1 on any failed-assert (including warnings).
|-------|------|----------------------|
| Java (native) | [`SchematronValidate.java`](SchematronValidate.java) | ✅ verified (SchXslt Java API, via Maven Wrapper) |
| Python | [`validate_schematron.py`](validate_schematron.py) | saxonche via repo venv (`pip install -e .`); SchXslt jar via `$FUNDSXML_SCHXSLT_JAR` or Maven local repo — reference variant |
| .NET/C# | [`SchematronValidate.cs`](SchematronValidate.cs) | reference variant, **does not restore as-is**: Saxonica publishes its .NET packages (`SaxonHE12Net*`) as dotnet *tools*, not libraries (`NU1212`); pair the code with a Saxon .NET library build of your own. SchXslt jar via `$FUNDSXML_SCHXSLT_JAR` or Maven local repo |
| .NET/C# | [`SchematronValidate.cs`](SchematronValidate.cs) | ✅ verified (.NET SDK 8, in CI): no NuGet deps — runs the self-contained SchXslt CLI jar (`$FUNDSXML_SCHXSLT_JAR` or Maven local repo) as a child process and classifies the SVRL itself; needs a JDK on PATH/`$JAVA_HOME` |
| shared | [`svrl-summary.py`](svrl-summary.py) | ✅ classifier used by all + CI |

The Java example runs standalone and cross-platform via the committed Maven
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
// Schematron validation in .NET / C# via Saxon for .NET (Saxonica SaxonHE).
// Schematron validation in .NET / C# — drives the SchXslt CLI, classifies SVRL.
//
// # add the Saxon-HE-for-.NET package matching your TFM (see .csproj note)
// dotnet run --project Schematron_DataQuality_Checks/Basic_Checks/invocation \
// -- Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch document.xml
// -- Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch document.xml [--fail-on error|any]
// Exit: 0 = no error-role failed-assert, 1 = at least one, 2 = setup error.
//
// basic_checks.sch uses queryBinding="xslt2"; the SaxonHE NuGet package
// supplies the XSLT 3.0 engine (resolved by `dotnet build`, standalone). The
// SchXslt pipeline stylesheets have no NuGet/.NET distribution, so they are
// reused from the SchXslt CLI jar, located via $FUNDSXML_SCHXSLT_JAR or the
// Maven local repo (see below); the whole xslt/ tree is extracted so the
// pipeline's relative imports resolve, then compile .sch -> SVRL stylesheet
// -> apply to instance -> SVRL, then classify (same logic as svrl-summary.py).
// WHY NOT A .NET XSLT ENGINE
// basic_checks.sch uses queryBinding="xslt2". The .NET BCL only has XSLT 1.0
// (System.Xml.Xsl), and there is no Saxon-HE library on NuGet for .NET 8:
// Saxonica publishes its .NET packages (SaxonHE12Net*) as dotnet *tools*,
// SaxonCS is not on NuGet, and the third-party IKVM cross-compiles are
// experimental. So this example does what a .NET service in a mixed shop
// typically does: it runs the SchXslt CLI (a self-contained jar that bundles
// its own Saxon) as a child process and owns the result — the SVRL parsing,
// the error/warning classification and the exit-code contract are the same
// as in svrl-summary.py and the Java example. Prerequisite: a JDK on PATH
// (or $JAVA_HOME) — the same one the Maven Wrapper uses.
//
// NOTE: reference variant. The Java Schematron example is the verified, fully
// standalone path (Maven Wrapper). The flow here mirrors it exactly.
// The SchXslt CLI jar has no NuGet distribution; it is located standalone via
// $FUNDSXML_SCHXSLT_JAR or the Maven local repo (populated by the Java module).
//
// Verified with .NET SDK 8 + JDK 21/26 (also in CI): canonical sample -> 0
// errors / 12 warnings, negative fixture -> 1 error / exit 1.

using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Xml;
using Saxon.Api;

internal static class SchematronValidate
{
Expand Down Expand Up @@ -66,34 +71,50 @@ private static int Main(string[] args)
}

string tmp = Directory.CreateTempSubdirectory().FullName;
using (var zip = ZipFile.OpenRead(cliJar))
foreach (var e in zip.Entries.Where(e => e.FullName.StartsWith("xslt/")
&& !e.FullName.EndsWith("/")))
{
string dest = Path.Combine(tmp, e.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
e.ExtractToFile(dest, true);
}

var processor = new Processor(false);
var comp = processor.NewXsltCompiler();

string compiled = Path.Combine(tmp, "compiled.xsl");
string svrl = Path.Combine(tmp, "report.svrl");

// 1) Schematron -> SVRL stylesheet
Transform(comp, Path.Combine(tmp, "xslt/2.0/pipeline-for-svrl.xsl"),
sch, compiled, processor);
// 2) instance -> SVRL
Transform(comp, compiled, xml, svrl, processor);
// java from $JAVA_HOME if set (what the Maven Wrapper honours), else PATH.
string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
string java = string.IsNullOrEmpty(javaHome) ? "java"
: Path.Combine(javaHome, "bin", "java");

// SchXslt CLI: -s schema, -d document, -o SVRL output. Its own exit code
// is left at the default (0) on purpose — the classification below
// decides, exactly like the Java example and svrl-summary.py.
var psi = new ProcessStartInfo(java)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
};
foreach (var a in new[] { "-jar", cliJar, "-s", sch, "-d", xml, "-o", svrl })
psi.ArgumentList.Add(a);
using (var proc = Process.Start(psi)!)
{
// Drain both pipes concurrently so a chatty child cannot block.
var stdoutTask = proc.StandardOutput.ReadToEndAsync();
string stderr = proc.StandardError.ReadToEnd();
stdoutTask.Wait();
proc.WaitForExit();
if (proc.ExitCode != 0 || !File.Exists(svrl))
{
Console.Error.WriteLine("SchXslt CLI failed (exit "
+ proc.ExitCode + "):\n" + stderr.Trim());
return 2;
}
}

var doc = new XmlDocument();
doc.Load(svrl);
var ns = new XmlNamespaceManager(doc.NameTable);
ns.AddNamespace("svrl", Svrl);

// Severity is the @role attribute, not the element name: this ruleset
// raises warnings as <assert role="warning"> (=> failed-assert) and the
// rounding checks as <report role="warning"> (=> successful-report).
int errors = 0, warnings = 0;
foreach (XmlElement fa in doc.SelectNodes("//svrl:failed-assert", ns)!)
foreach (XmlElement fa in doc.SelectNodes(
"//svrl:failed-assert | //svrl:successful-report", ns)!)
{
string role = fa.GetAttribute("role").ToLowerInvariant();
string text = (fa.SelectSingleNode("svrl:text", ns)?.InnerText ?? "")
Expand All @@ -106,14 +127,4 @@ private static int Main(string[] args)
if (failOn == "any" && (errors > 0 || warnings > 0)) return 1;
return errors > 0 ? 1 : 0;
}

private static void Transform(XsltCompiler comp, string xsl, string src,
string outFile, Processor p)
{
var exe = comp.Compile(new Uri(Path.GetFullPath(xsl)));
var t = exe.Load30();
using var os = File.Create(outFile);
t.Transform(new Uri(Path.GetFullPath(src)),
p.NewSerializer(os));
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Reference variant. SaxonHE comes from NuGet (restored by dotnet build);
the SchXslt jar is located via $FUNDSXML_SCHXSLT_JAR or the Maven local
repo (no NuGet SchXslt). Run from the repo root with "dotnet run", the
project switch pointing at this directory, then
Schematron_DataQuality_Checks/Basic_Checks/basic_checks.sch <xml-file>
[fail-on error|any]; see the SchematronValidate.cs header for the exact
command line (XML comments cannot contain a double dash). -->
<!-- .NET Schematron example: no NuGet dependencies. There is no Saxon-HE
library package for .NET 8 (Saxonica ships dotnet tools only), so the
program runs the self-contained SchXslt CLI jar as a child process and
does the SVRL classification itself. The jar is located via
$FUNDSXML_SCHXSLT_JAR or the Maven local repo (populated by
./mvnw compile of the Java module); a JDK must be on PATH or $JAVA_HOME.
Run from the repo root with "dotnet run", the project switch pointing
at this directory, then basic_checks.sch <xml-file> [fail-on error|any];
see the SchematronValidate.cs header for the exact command line. -->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<AssemblyName>SchematronValidate</AssemblyName>
<RootNamespace>FundsXml.Schematron</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!-- REFERENCE VARIANT — not verified, not in CI. Saxonica's Saxon-HE 12
for .NET (the `Saxon.Api` s9api this file uses) is published as a set
of `SaxonHE12Net*` packages whose exact id/version/target-framework
pairing is environment-specific (and unrelated to FundsXML). Pick the
one matching your TFM, e.g.:
dotnet add package SaxonHE12NetXslt
The Java Schematron example is the verified, fully standalone path
(Maven Wrapper); this .NET file mirrors its logic for reference. -->
<PackageReference Include="SaxonHE12NetXslt" Version="12.9.10" />
</ItemGroup>
</Project>