public class XsdGenerator
{
private const string Xs = "http://www.w3.org/2001/XMLSchema";
public XDocument Generate(XmlValidator validator, string rootElementName)
{
var schema = new XElement(XName.Get("schema", Xs),
new XAttribute(XNamespace.Xmlns + "xs", Xs));
var rootElement = new XElement(XName.Get("element", Xs),
new XAttribute("name", rootElementName));
var rootComplex = new XElement(XName.Get("complexType", Xs));
var rootSeq = new XElement(XName.Get("sequence", Xs));
foreach (var rule in validator.Rules.OfType<XmlCompositeRule>())
{
var segments = GetElementSegments(rootElementName, rule.Path);
XElement parent = rootSeq;
foreach (var seg in segments)
{
// Find or create element node
var existing = parent.Elements(XName.Get("element", Xs))
.FirstOrDefault(e => (string)e.Attribute("name") == seg);
if (existing == null)
{
existing = new XElement(XName.Get("element", Xs),
new XAttribute("name", seg));
parent.Add(existing);
var complex = new XElement(XName.Get("complexType", Xs));
var seq = new XElement(XName.Get("sequence", Xs));
complex.Add(seq);
existing.Add(complex);
}
parent = existing.Element(XName.Get("complexType", Xs))!
.Element(XName.Get("sequence", Xs))!;
}
// Apply restrictions/attributes at the deepest segment
var targetElement = parent.Parent!.Parent!; // the <xs:element> node
ApplyRuleToElement(targetElement, rule);
}
var globalRules = validator.Rules.OfType<XmlGlobalRule>();
var annotation = GetAnnotation(rootElement);
foreach (var gr in globalRules)
{
annotation.Add(new XElement(XName.Get("documentation", Xs),
gr.Message ?? "Global validation rule applied"));
}
rootComplex.Add(rootSeq);
rootElement.Add(rootComplex);
schema.Add(rootElement);
AddUniqueConstraints(new XDocument(schema), validator, rootElementName);
return new XDocument(schema);
}
private XElement GetAnnotation(XElement rootElement)
{
var annotation = rootElement.Element(XName.Get("annotation", Xs));
if (annotation == null)
{
annotation = new XElement(XName.Get("annotation", Xs));
rootElement.Add(annotation);
}
return annotation;
}
private static string[] GetElementSegments(string root, string path)
{
// Trim leading/trailing slashes and split
var segments = path.Trim('/').Split('/');
// Ensure the first segment matches root
if (segments.Length == 0 || segments[0] != root)
throw new InvalidOperationException($"Path '{path}' does not start with root '{root}'.");
// Return all segments after root
return segments.Skip(1).ToArray();
}
private static void ApplyRuleToElement(XElement elementNode, XmlCompositeRule rule)
{
var complex = elementNode.Element(XName.Get("complexType", Xs));
var sequence = complex?.Element(XName.Get("sequence", Xs));
var attributes = new Dictionary<string, List<RecordedStep>>();
var simpleContentNeeded = false;
// Add empty annotation
//// Collect annotation node once per element
//var annotation = elementNode.Element(XName.Get("annotation", Xs));
//if (annotation == null)
//{
// annotation = new XElement(XName.Get("annotation", Xs));
// elementNode.Add(annotation);
//}
foreach (var step in rule.RecordedSteps)
{
switch (step.Kind)
{
case XmlValidationRuleKind.AttributeRequired:
case XmlValidationRuleKind.AttributeRegex:
case XmlValidationRuleKind.AttributeRangeInt:
case XmlValidationRuleKind.AttributeUnique:
simpleContentNeeded = true;
var name = step.AttributeName!;
if (!attributes.ContainsKey(name)) attributes[name] = new List<RecordedStep>();
attributes[name].Add(step);
break;
case XmlValidationRuleKind.ElementRequired:
case XmlValidationRuleKind.ElementRegex:
case XmlValidationRuleKind.ElementRangeInt:
case XmlValidationRuleKind.ElementUnique:
simpleContentNeeded = true;
break;
case XmlValidationRuleKind.CustomElement:
//elementNode.Add(new XElement(XName.Get("annotation", Xs),
// new XElement(XName.Get("documentation", Xs),
// $"Custom rule applied: {step.Message}")));
break;
case XmlValidationRuleKind.ElementAttributeCross:
//elementNode.Add(new XElement(XName.Get("annotation", Xs),
// new XElement(XName.Get("documentation", Xs),
// $"Runtime rule on '{elementNode.Attribute("name")?.Value}': {step.Message ?? step.Kind.ToString()}")));
break;
}
}
if (simpleContentNeeded || attributes.Count > 0)
{
var simpleContent = new XElement(XName.Get("simpleContent", Xs));
var extension = new XElement(XName.Get("extension", Xs),
new XAttribute("base", InferBaseForElement(new[] { rule })));
foreach (var attr in attributes)
{
extension.Add(BuildAttribute(attr.Key, attr.Value));
}
simpleContent.Add(extension);
complex!.RemoveNodes();
complex.Add(simpleContent);
}
}
private static string GetTopElementName(string root, string path)
{
// path like /order/id or /order/items/item
var segments = path.Trim('/').Split('/');
if (segments.Length < 2 || segments[0] != root) return segments[0];
return segments[1];
}
private static string? GetChildElementName(string root, string path)
{
var segments = path.Trim('/').Split('/');
if (segments.Length <= 2) return null;
if (segments[0] != root) return null;
return segments.Last();
}
private static string InferMinOccurs(IEnumerable<XmlCompositeRule> rules)
{
var hasRequired = rules.Any(r => r.RecordedSteps.Any(s => s.Kind == XmlValidationRuleKind.ElementRequired));
return hasRequired ? "1" : "0";
}
private static XName XS(string local) => XName.Get(local, Xs);
private static XElement? BuildSimpleTypeForElement(IEnumerable<XmlCompositeRule> rules)
{
var regex = rules.SelectMany(r => r.RecordedSteps)
.Where(s => s.Kind == XmlValidationRuleKind.ElementRegex)
.Select(s => s.Arg1 as string)
.FirstOrDefault();
var range = rules.SelectMany(r => r.RecordedSteps)
.Where(s => s.Kind == XmlValidationRuleKind.ElementRangeInt)
.Select(s => (Min: (int)s.Arg1!, Max: (int)s.Arg2!))
.FirstOrDefault();
if (regex == null && range.Min == 0 && range.Max == 0) return null;
var simpleType = new XElement(XS("simpleType"));
var restriction = new XElement(XS("restriction"),
new XAttribute("base", range.Min != 0 || range.Max != 0 ? "xs:int" : "xs:string"));
if (range.Min != 0 || range.Max != 0)
{
restriction.Add(new XElement(XS("minInclusive"), new XAttribute("value", range.Min)));
restriction.Add(new XElement(XS("maxInclusive"), new XAttribute("value", range.Max)));
}
if (regex is not null)
{
restriction.Add(new XElement(XS("pattern"), new XAttribute("value", regex)));
}
simpleType.Add(restriction);
return simpleType;
}
private static string InferBaseForElement(IEnumerable<XmlCompositeRule> rules)
{
var hasIntRange = rules.SelectMany(r => r.RecordedSteps)
.Any(s => s.Kind == XmlValidationRuleKind.ElementRangeInt);
return hasIntRange ? "xs:int" : "xs:string";
}
private static XElement BuildAttribute(string name, IEnumerable<RecordedStep> steps)
{
var attr = new XElement(XS("attribute"), new XAttribute("name", name));
var required = steps.Any(s => s.Kind == XmlValidationRuleKind.AttributeRequired);
if (required) attr.Add(new XAttribute("use", "required"));
var pattern = steps.Where(s => s.Kind == XmlValidationRuleKind.AttributeRegex).Select(s => s.Arg1 as string).FirstOrDefault();
var range = steps.Where(s => s.Kind == XmlValidationRuleKind.AttributeRangeInt).Select(s => (Min: (int)s.Arg1!, Max: (int)s.Arg2!)).FirstOrDefault();
if (pattern != null || (range.Min != 0 || range.Max != 0))
{
var simpleType = new XElement(XS("simpleType"));
var baseType = (range.Min != 0 || range.Max != 0) ? "xs:int" : "xs:string";
var restriction = new XElement(XS("restriction"), new XAttribute("base", baseType));
if (range.Min != 0 || range.Max != 0)
{
restriction.Add(new XElement(XS("minInclusive"), new XAttribute("value", range.Min)));
restriction.Add(new XElement(XS("maxInclusive"), new XAttribute("value", range.Max)));
}
if (pattern != null)
{
restriction.Add(new XElement(XS("pattern"), new XAttribute("value", pattern)));
}
simpleType.Add(restriction);
attr.Add(simpleType);
}
return attr;
}
private static void AddUniqueConstraints(XDocument schema, XmlValidator validator, string rootElementName)
{
// Element-level unique constraints
var elementUniqueRules = validator.Rules.OfType<XmlCompositeRule>()
.Where(r => r.RecordedSteps.Any(s => s.Kind == XmlValidationRuleKind.ElementUnique));
foreach (var rule in elementUniqueRules)
{
schema.Root!.Add(new XElement(XName.Get("unique", Xs),
new XAttribute("name", $"unique_{San(rule.Path)}"),
new XElement(XName.Get("selector", Xs), new XAttribute("xpath", rule.Path)),
new XElement(XName.Get("field", Xs), new XAttribute("xpath", "."))));
}
// Attribute-level unique constraints
var attributeUniqueRules = validator.Rules.OfType<XmlCompositeRule>()
.SelectMany(r => r.RecordedSteps.Select(s => (Rule: r, Step: s)))
.Where(rs => rs.Step.Kind == XmlValidationRuleKind.AttributeUnique);
foreach (var (r, step) in attributeUniqueRules)
{
schema.Root!.Add(new XElement(XName.Get("unique", Xs),
new XAttribute("name", $"unique{San(r.Path)}{step.AttributeName}"),
new XElement(XName.Get("selector", Xs), new XAttribute("xpath", r.Path)),
new XElement(XName.Get("field", Xs), new XAttribute("xpath", $"@{step.AttributeName}"))));
}
static string San(string s) => s.Trim('/').Replace("/", "").Replace("@", "");
}
}