diff --git a/XSDVisualiser.Desktop/Converters/CardinalityToLabelConverter.cs b/XSDVisualiser.Desktop/Converters/CardinalityToLabelConverter.cs index 133e5ce..de2ebe9 100644 --- a/XSDVisualiser.Desktop/Converters/CardinalityToLabelConverter.cs +++ b/XSDVisualiser.Desktop/Converters/CardinalityToLabelConverter.cs @@ -9,13 +9,11 @@ namespace XSDVisualiser.Desktop.Converters { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value is Occurs occ) - { - var min = occ.Min; - var max = occ.MaxIsUnbounded ? "*" : occ.Max.ToString(culture); - return $"[{min}..{max}]"; - } - return null; + if (value is not Occurs occ) return null; + + var min = occ.Min; + var max = occ.MaxIsUnbounded ? "*" : occ.Max.ToString(culture); + return $"[{min}..{max}]"; } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) diff --git a/XSDVisualiser.Desktop/MainWindow.axaml b/XSDVisualiser.Desktop/MainWindow.axaml index 1f56b1f..1bdf804 100644 --- a/XSDVisualiser.Desktop/MainWindow.axaml +++ b/XSDVisualiser.Desktop/MainWindow.axaml @@ -19,7 +19,7 @@ - + - - - - - + @@ -50,7 +47,7 @@ + Padding="8" Margin="0,0,8,6" Background="{DynamicResource PanelBackgroundBrush}"> @@ -69,7 +66,7 @@ + Foreground="{DynamicResource SubtleTextBrush}" FontWeight="SemiBold"/> @@ -95,116 +92,31 @@ - - - + + + + + + - - + + + + + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/XSDVisualiser.Desktop/MainWindow.axaml.cs b/XSDVisualiser.Desktop/MainWindow.axaml.cs index b8be322..0a65705 100644 --- a/XSDVisualiser.Desktop/MainWindow.axaml.cs +++ b/XSDVisualiser.Desktop/MainWindow.axaml.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Threading; +using XSDVisualiser.Core; using XSDVisualiser.Models; -using XSDVisualiser.Parsing; namespace XSDVisualiser.Desktop { diff --git a/XSDVisualiser.Desktop/Program.cs b/XSDVisualiser.Desktop/Program.cs index 310f349..41f59e6 100644 --- a/XSDVisualiser.Desktop/Program.cs +++ b/XSDVisualiser.Desktop/Program.cs @@ -12,7 +12,7 @@ namespace XSDVisualiser.Desktop BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); } - public static AppBuilder BuildAvaloniaApp() => + private static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() .LogToTrace() diff --git a/XSDVisualiser/Parsing/XsdSchemaParser.cs b/XSDVisualiser/Parsing/XsdSchemaParser.cs index 6720b66..9e69023 100644 --- a/XSDVisualiser/Parsing/XsdSchemaParser.cs +++ b/XSDVisualiser/Parsing/XsdSchemaParser.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Xml; using System.Xml.Schema; using XSDVisualiser.Models; -namespace XSDVisualiser.Parsing +namespace XSDVisualiser.Core { public class XsdSchemaParser { @@ -16,16 +13,16 @@ namespace XSDVisualiser.Parsing _set.XmlResolver = new XmlUrlResolver(); using var reader = XmlReader.Create(xsdPath, new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore }); var schema = XmlSchema.Read(reader, ValidationCallback); - _set.Add(schema); + _set.Add(schema!); _set.CompilationSettings = new XmlSchemaCompilationSettings { EnableUpaCheck = true }; _set.Compile(); var model = new SchemaModel { - TargetNamespace = schema.TargetNamespace + TargetNamespace = schema!.TargetNamespace }; - foreach (XmlSchemaElement globalEl in _set.Schemas().Cast() + foreach (var globalEl in _set.Schemas().Cast() .SelectMany(s => s.Elements.Values.Cast())) { var node = BuildNodeForElement(globalEl, parentContentModel: null); @@ -58,24 +55,22 @@ namespace XSDVisualiser.Parsing }; var type = ResolveElementType(element); - if (type != null) + if (type == null) return node; + node.TypeName = GetQualifiedTypeName(type); + if (type.Datatype != null) { - node.TypeName = GetQualifiedTypeName(type); - if (type.Datatype != null) - { - node.BuiltInType = type.Datatype.TypeCode.ToString(); - } + node.BuiltInType = type.Datatype.TypeCode.ToString(); + } - switch (type) - { - case XmlSchemaComplexType ct: - HandleComplexType(node, ct); - break; - case XmlSchemaSimpleType st: - node.ContentModel = "simple"; - node.Constraints = ExtractConstraints(st); - break; - } + switch (type) + { + case XmlSchemaComplexType ct: + HandleComplexType(node, ct); + break; + case XmlSchemaSimpleType st: + node.ContentModel = "simple"; + node.Constraints = ExtractConstraints(st); + break; } return node; @@ -84,7 +79,7 @@ namespace XSDVisualiser.Parsing private static string? GetQualifiedTypeName(XmlSchemaType type) { if (!type.QualifiedName.IsEmpty) return type.QualifiedName.ToString(); - return type.BaseXmlSchemaType != null && !type.BaseXmlSchemaType.QualifiedName.IsEmpty + return type.BaseXmlSchemaType is { QualifiedName.IsEmpty: false } ? type.BaseXmlSchemaType.QualifiedName.ToString() : type.Name; } @@ -96,16 +91,71 @@ namespace XSDVisualiser.Parsing { return _set.GlobalTypes[el.SchemaTypeName] as XmlSchemaType; } - if (el.SchemaType != null) return el.SchemaType; - return null; + + return el.SchemaType; } private void HandleComplexType(SchemaNode node, XmlSchemaComplexType ct) { - // Attributes - foreach (XmlSchemaAttribute attr in ct.AttributeUses.Values.OfType()) + // Collect attributes (ensure uniqueness) + var seenAttrKeys = new HashSet(StringComparer.Ordinal); + + // 1) Compiled attribute uses (includes inherited/group attributes after Compile) + foreach (var attr in ct.AttributeUses.Values.OfType()) { - node.Attributes.Add(ExtractAttribute(attr)); + var qn = attr.QualifiedName.IsEmpty ? attr.RefName : attr.QualifiedName; + var key = qn.ToString(); + if (seenAttrKeys.Add(key)) + { + node.Attributes.Add(ExtractAttribute(attr)); + } + } + + // 2) Uncompiled attributes directly on the type (fallback) + foreach (var a in ct.Attributes.OfType()) + { + var qn = a.QualifiedName.IsEmpty ? a.RefName : a.QualifiedName; + var key = qn.ToString(); + if (seenAttrKeys.Add(key)) + { + node.Attributes.Add(ExtractAttribute(a)); + } + } + + // 3) Attributes from complexContent extension/restriction + if (ct.ContentModel is XmlSchemaComplexContent cc) + { + switch (cc.Content) + { + case XmlSchemaComplexContentExtension cext: + { + foreach (var a in cext.Attributes.OfType()) + { + var qn = a.QualifiedName.IsEmpty ? a.RefName : a.QualifiedName; + var key = qn.ToString(); + if (seenAttrKeys.Add(key)) + { + node.Attributes.Add(ExtractAttribute(a)); + } + } + + break; + } + case XmlSchemaComplexContentRestriction cres: + { + foreach (var a in cres.Attributes.OfType()) + { + var qn = a.QualifiedName.IsEmpty ? a.RefName : a.QualifiedName; + var key = qn.ToString(); + if (seenAttrKeys.Add(key)) + { + node.Attributes.Add(ExtractAttribute(a)); + } + } + + break; + } + } } // Content model @@ -158,31 +208,43 @@ namespace XSDVisualiser.Parsing else if (ct.ContentType == XmlSchemaContentType.TextOnly && ct.ContentModel is XmlSchemaSimpleContent simpleContent) { node.ContentModel = "simple"; - if (simpleContent.Content is XmlSchemaSimpleContentExtension ext) + switch (simpleContent.Content) { - var baseType = ResolveType(ext.BaseTypeName); - if (baseType is XmlSchemaSimpleType st) + case XmlSchemaSimpleContentExtension ext: { - node.Constraints = ExtractConstraints(st); - node.TypeName ??= GetQualifiedTypeName(st); - node.BuiltInType ??= st.Datatype?.TypeCode.ToString(); - } + var baseType = ResolveType(ext.BaseTypeName); + if (baseType is XmlSchemaSimpleType st) + { + node.Constraints = ExtractConstraints(st); + node.TypeName ??= GetQualifiedTypeName(st); + node.BuiltInType ??= st.Datatype?.TypeCode.ToString(); + } - foreach (XmlSchemaAttribute attr in ext.Attributes.OfType()) - { - node.Attributes.Add(ExtractAttribute(attr)); + foreach (var attr in ext.Attributes.OfType()) + { + var qn = attr.QualifiedName.IsEmpty ? attr.RefName : attr.QualifiedName; + var key = qn.ToString(); + if (seenAttrKeys.Add(key)) + { + node.Attributes.Add(ExtractAttribute(attr)); + } + } + + break; } - } - else if (simpleContent.Content is XmlSchemaSimpleContentRestriction res) - { - var baseType = ResolveType(res.BaseTypeName); - if (baseType is XmlSchemaSimpleType st) + case XmlSchemaSimpleContentRestriction res: { - var cons = ExtractConstraints(st); - MergeFacets(cons, res.Facets); - node.Constraints = cons; - node.TypeName ??= GetQualifiedTypeName(st); - node.BuiltInType ??= st.Datatype?.TypeCode.ToString(); + var baseType = ResolveType(res.BaseTypeName); + if (baseType is XmlSchemaSimpleType st) + { + var cons = ExtractConstraints(st); + MergeFacets(cons, res.Facets); + node.Constraints = cons; + node.TypeName ??= GetQualifiedTypeName(st); + node.BuiltInType ??= st.Datatype?.TypeCode.ToString(); + } + + break; } } } @@ -208,12 +270,10 @@ namespace XSDVisualiser.Parsing else if (!attr.SchemaTypeName.IsEmpty) st = ResolveType(attr.SchemaTypeName) as XmlSchemaSimpleType; else if (attr.SchemaType != null) st = attr.SchemaType; - if (st != null) - { - info.TypeName = GetQualifiedTypeName(st); - info.BuiltInType = st.Datatype?.TypeCode.ToString(); - info.Constraints = ExtractConstraints(st); - } + if (st == null) return info; + info.TypeName = GetQualifiedTypeName(st); + info.BuiltInType = st.Datatype?.TypeCode.ToString(); + info.Constraints = ExtractConstraints(st); return info; } @@ -225,33 +285,39 @@ namespace XSDVisualiser.Parsing BaseTypeName = GetQualifiedTypeName(st.BaseXmlSchemaType) }; - if (st.Content is XmlSchemaSimpleTypeRestriction restr) + switch (st.Content) { - MergeFacets(cons, restr.Facets); - } - else if (st.Content is XmlSchemaSimpleTypeList list) - { - cons.Patterns.Add("(list)"); - if (!list.ItemTypeName.IsEmpty) + case XmlSchemaSimpleTypeRestriction restr: + MergeFacets(cons, restr.Facets); + break; + case XmlSchemaSimpleTypeList list: { - var baseType = ResolveType(list.ItemTypeName); - if (baseType is XmlSchemaSimpleType itemSt) + cons.Patterns.Add("(list)"); + if (!list.ItemTypeName.IsEmpty) { - var sub = ExtractConstraints(itemSt); - Merge(cons, sub); + var baseType = ResolveType(list.ItemTypeName); + if (baseType is XmlSchemaSimpleType itemSt) + { + var sub = ExtractConstraints(itemSt); + Merge(cons, sub); + } } + + break; } - } - else if (st.Content is XmlSchemaSimpleTypeUnion union) - { - cons.Patterns.Add("(union)"); - foreach (var memberType in union.BaseMemberTypes) + case XmlSchemaSimpleTypeUnion union: { - if (memberType is XmlSchemaSimpleType mst) + cons.Patterns.Add("(union)"); + foreach (var memberType in union.BaseMemberTypes) { - var sub = ExtractConstraints(mst); - Merge(cons, sub); + if (memberType is { } mst) + { + var sub = ExtractConstraints(mst); + Merge(cons, sub); + } } + + break; } } @@ -261,8 +327,8 @@ namespace XSDVisualiser.Parsing private static void Merge(ConstraintSet target, ConstraintSet? source) { if (source == null) return; - foreach (var e in source.Enumerations) if (!target.Enumerations.Contains(e)) target.Enumerations.Add(e); - foreach (var p in source.Patterns) if (!target.Patterns.Contains(p)) target.Patterns.Add(p); + foreach (var e in source.Enumerations.Where(e => !target.Enumerations.Contains(e))) target.Enumerations.Add(e); + foreach (var p in source.Patterns.Where(p => !target.Patterns.Contains(p))) target.Patterns.Add(p); if (source.Numeric != null) { target.Numeric ??= new NumericBounds(); @@ -271,25 +337,25 @@ namespace XSDVisualiser.Parsing target.Numeric.MinExclusive ??= source.Numeric.MinExclusive; target.Numeric.MaxExclusive ??= source.Numeric.MaxExclusive; } - if (source.Length != null) + + if (source.Length == null) return; + + target.Length ??= new LengthBounds(); + if (source.Length.LengthSpecified && !target.Length.LengthSpecified) { - target.Length ??= new LengthBounds(); - if (source.Length.LengthSpecified && !target.Length.LengthSpecified) - { - target.Length.Length = source.Length.Length; - target.Length.LengthSpecified = true; - } - if (source.Length.MinLengthSpecified && !target.Length.MinLengthSpecified) - { - target.Length.MinLength = source.Length.MinLength; - target.Length.MinLengthSpecified = true; - } - if (source.Length.MaxLengthSpecified && !target.Length.MaxLengthSpecified) - { - target.Length.MaxLength = source.Length.MaxLength; - target.Length.MaxLengthSpecified = true; - } + target.Length.Length = source.Length.Length; + target.Length.LengthSpecified = true; } + if (source.Length.MinLengthSpecified && !target.Length.MinLengthSpecified) + { + target.Length.MinLength = source.Length.MinLength; + target.Length.MinLengthSpecified = true; + } + + if (!source.Length.MaxLengthSpecified || target.Length.MaxLengthSpecified) return; + + target.Length.MaxLength = source.Length.MaxLength; + target.Length.MaxLengthSpecified = true; } private static void MergeFacets(ConstraintSet cons, XmlSchemaObjectCollection facets) diff --git a/XSDVisualiser/Program.cs b/XSDVisualiser/Program.cs index a59fe62..589d33e 100644 --- a/XSDVisualiser/Program.cs +++ b/XSDVisualiser/Program.cs @@ -1,4 +1,4 @@ -using XSDVisualiser.Parsing; +using XSDVisualiser.Core; using XSDVisualiser.Utils; /* @@ -80,6 +80,8 @@ catch (Exception ex) Environment.Exit(3); } +return; + static void PrintHelp() { Console.WriteLine("XSDVisualiser - Parse an XSD and emit a structural model with types, constraints, and cardinality"); diff --git a/XSDVisualiser/Utils/Serialization.cs b/XSDVisualiser/Utils/Serialization.cs index 294360e..6495e9b 100644 --- a/XSDVisualiser/Utils/Serialization.cs +++ b/XSDVisualiser/Utils/Serialization.cs @@ -8,6 +8,12 @@ namespace XSDVisualiser.Utils { public static class Serialization { + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + public static string ToXml(T obj) { var serializer = new XmlSerializer(typeof(T)); @@ -20,12 +26,7 @@ namespace XSDVisualiser.Utils public static string ToJson(T obj) { - var options = new JsonSerializerOptions - { - WriteIndented = true, - DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull - }; - return JsonSerializer.Serialize(obj, options); + return JsonSerializer.Serialize(obj, JsonOptions); } private sealed class Utf8StringWriter : StringWriter