From f07f9f7ca6ff55d96a76fb46035306118abfd59c Mon Sep 17 00:00:00 2001 From: Adam Smith-Platts Date: Thu, 27 Apr 2023 21:37:19 +1000 Subject: [PATCH 1/2] Added new NamingProvider, SubstituteNamingProvider, which can store a dictionary of generated names and an alternate to substitute with - Updated the INamingProvider to add two specific Type-based methods for PropertyNameFromAttribute and PropertyNameFromElement, to ensure that custom implementors can split the implementation accordingly. - Updated XmlSchemaClassGenerator.Console to take in command line options for --typeNameSubstitute and typeNameSubstituteFile. What's left: - Update README - Add tests --- XmlSchemaClassGenerator.Console/Program.cs | 47 +++++++- .../EnumerableExtensions.cs | 18 +++ XmlSchemaClassGenerator/INamingProvider.cs | 18 +++ XmlSchemaClassGenerator/ModelBuilder.cs | 4 +- XmlSchemaClassGenerator/NamingProvider.cs | 9 ++ .../SubstituteNamingProvider.cs | 113 ++++++++++++++++++ 6 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 XmlSchemaClassGenerator/NamingProviders/SubstituteNamingProvider.cs diff --git a/XmlSchemaClassGenerator.Console/Program.cs b/XmlSchemaClassGenerator.Console/Program.cs index ec7d8c2b..ae8034fa 100644 --- a/XmlSchemaClassGenerator.Console/Program.cs +++ b/XmlSchemaClassGenerator.Console/Program.cs @@ -1,4 +1,5 @@ using XmlSchemaClassGenerator; +using XmlSchemaClassGenerator.NamingProviders; using System; using System.CodeDom; using System.Collections.Generic; @@ -11,7 +12,7 @@ using System.Threading.Tasks; using Mono.Options; using Ganss.IO; - + namespace XmlSchemaClassGenerator.Console { static class Program @@ -20,6 +21,7 @@ static void Main(string[] args) { var showHelp = args.Length == 0; var namespaces = new List(); + var nameSubstitutes = new List(); var outputFolder = (string)null; Type integerType = null; var useIntegerTypeAsFallback = false; @@ -59,6 +61,7 @@ static void Main(string[] args) var useArrayItemAttribute = true; var enumAsString = false; var namespaceFiles = new List(); + var nameSubstituteFiles = new List(); var options = new OptionSet { { "h|help", "show this message and exit", v => showHelp = v != null }, @@ -67,9 +70,16 @@ static void Main(string[] args) One option must be given for each namespace to be mapped. A file name may be given by appending a pipe sign (|) followed by a file name (like schema.xsd) to the XML namespace. If no mapping is found for an XML namespace, a name is generated automatically (may fail).", v => namespaces.Add(v) }, - { "nf|namespaceFile=", @"file containing mapppings from XML namespaces to C# namespaces + { "nf|namespaceFile=", @"file containing mappings from XML namespaces to C# namespaces The line format is one mapping per line: XML namespace = C# namespace [optional file name]. Lines starting with # and empty lines are ignored.", v => namespaceFiles.Add(v) }, + { "tns|typeNameSubstitute=", @"substitute a generated type/member name +Separate type/member name and substitute name by '='. +Prefix type/member name with an appropriate kind ID as documented at: https://t.ly/HHEI. +Prefix with 'A:' to substitute any type/member.", v => nameSubstitutes.Add(v) }, + { "tnsf|typeNameSubstituteFile=", @"file containing generated type/member name substitute mappings +The line format is one mapping per line: prefixed type/member name = substitute name. +Lines starting with # and empty lines are ignored.", v => nameSubstituteFiles.Add(v) }, { "o|output=", "the {FOLDER} to write the resulting .cs files to", v => outputFolder = v }, { "i|integer=", @"map xs:integer and derived types to {TYPE} instead of automatic approximation {TYPE} can be i[nt], l[ong], or d[ecimal]", v => { @@ -178,6 +188,9 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l return name; }); + ParseNameSubstituteFiles(nameSubstitutes, nameSubstituteFiles); + var nameSubstituteMap = nameSubstitutes.ToDictionary(); + if (!string.IsNullOrEmpty(outputFolder)) { outputFolder = Path.GetFullPath(outputFolder); @@ -221,6 +234,11 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l EnumAsString = enumAsString, }; + if (nameSubstituteMap.Any()) + { + generator.NamingProvider = new SubstituteNamingProvider(nameSubstituteMap); + } + generator.CommentLanguages.AddRange(commentLanguages); if (pclCompatible) @@ -265,6 +283,31 @@ private static void ParseNamespaceFiles(List namespaces, List na } } + private static void ParseNameSubstituteFiles(List nameSubstitutes, List nameSubstituteFiles) + { + foreach (var nameSubstituteFile in nameSubstituteFiles) + { + foreach (var (line, number) in File.ReadAllLines(nameSubstituteFile) + .Select((l, i) => (Line: l.Trim(), Number: i + 1)) + .Where(l => !string.IsNullOrWhiteSpace(l.Line) && !l.Line.StartsWith("#"))) + { + var parts = line.Split('='); + + if (parts.Length != 2) + { + System.Console.WriteLine($"{nameSubstituteFile}:{number}: Line format is prefixed type/member name = substitute name"); + Environment.Exit(2); + } + + var generatedName = parts[0].Trim(); + var substituteName = parts[1].Trim(); + var nameSubstitute = $"{generatedName}={substituteName}"; + + nameSubstitutes.Add(nameSubstitute); + } + } + } + static void ShowHelp(OptionSet p) { System.Console.WriteLine("Usage: xscgen [OPTIONS]+ xsdFile..."); diff --git a/XmlSchemaClassGenerator/EnumerableExtensions.cs b/XmlSchemaClassGenerator/EnumerableExtensions.cs index 038f60a3..5f1c02d9 100644 --- a/XmlSchemaClassGenerator/EnumerableExtensions.cs +++ b/XmlSchemaClassGenerator/EnumerableExtensions.cs @@ -97,5 +97,23 @@ public static IEnumerable MarkAmbiguousNamespaceTypes( yield return hierarchyItem; } } + + /// + /// Creates a from an splitting the values into key and value pairs based on the . + /// + /// An to create a from. + /// An optional value that supplies the delimiter to use to create the key and value from the . + /// A that contains keys and values. The values within each group are in the same order as in . + public static Dictionary ToDictionary(this IEnumerable source, string delimiter = "=") + { + var result = new Dictionary(); + foreach (string value in source ?? Enumerable.Empty()) + { + var pair = value.Split(new string[] { delimiter ?? "=" }, 2, StringSplitOptions.None); + result.Add(pair[0], pair[1]); + } + + return result; + } } } diff --git a/XmlSchemaClassGenerator/INamingProvider.cs b/XmlSchemaClassGenerator/INamingProvider.cs index d04d6dad..65ca1dbb 100644 --- a/XmlSchemaClassGenerator/INamingProvider.cs +++ b/XmlSchemaClassGenerator/INamingProvider.cs @@ -23,6 +23,24 @@ public interface INamingProvider /// Name of the property string PropertyNameFromElement(string typeModelName, string elementName, XmlSchemaElement element); + /// + /// Creates a name for an inner type from an attribute name + /// + /// Name of the typeModel + /// Attribute name + /// Original XSD attribute + /// Name of the inner type + string TypeNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute); + + /// + /// Creates a name for an inner type from an element name + /// + /// Name of the typeModel + /// Element name + /// Original XSD element + /// Name of the inner type + string TypeNameFromElement(string typeModelName, string elementName, XmlSchemaElement element); + /// /// Creates a name for an enum member based on a value /// diff --git a/XmlSchemaClassGenerator/ModelBuilder.cs b/XmlSchemaClassGenerator/ModelBuilder.cs index 9d5fe194..a81d2abe 100644 --- a/XmlSchemaClassGenerator/ModelBuilder.cs +++ b/XmlSchemaClassGenerator/ModelBuilder.cs @@ -878,7 +878,7 @@ private PropertyModel PropertyFromAttribute(TypeModel owningTypeModel, XmlSchema if (attributeQualifiedName.IsEmpty || string.IsNullOrEmpty(attributeQualifiedName.Namespace)) { // inner type, have to generate a type name - var typeName = _configuration.NamingProvider.PropertyNameFromAttribute(owningTypeModel.Name, attribute.QualifiedName.Name, attribute); + var typeName = _configuration.NamingProvider.TypeNameFromAttribute(owningTypeModel.Name, attribute.QualifiedName.Name, attribute); attributeQualifiedName = new XmlQualifiedName(typeName, owningTypeModel.XmlSchemaName.Namespace); // try to avoid name clashes if (NameExists(attributeQualifiedName)) @@ -1024,7 +1024,7 @@ private XmlQualifiedName GetQualifiedName(TypeModel typeModel, XmlSchemaParticle { // inner type, have to generate a type name var typeModelName = xmlParticle is XmlSchemaGroupRef groupRef ? groupRef.RefName : typeModel.XmlSchemaName; - var typeName = _configuration.NamingProvider.PropertyNameFromElement(typeModelName.Name, element.QualifiedName.Name, element); + var typeName = _configuration.NamingProvider.TypeNameFromElement(typeModelName.Name, element.QualifiedName.Name, element); elementQualifiedName = new XmlQualifiedName(typeName, typeModel.XmlSchemaName.Namespace); // try to avoid name clashes if (NameExists(elementQualifiedName)) diff --git a/XmlSchemaClassGenerator/NamingProvider.cs b/XmlSchemaClassGenerator/NamingProvider.cs index 754a0d00..a9de595e 100644 --- a/XmlSchemaClassGenerator/NamingProvider.cs +++ b/XmlSchemaClassGenerator/NamingProvider.cs @@ -1,6 +1,7 @@ namespace XmlSchemaClassGenerator { using System.Xml; + using System.Xml.Linq; using System.Xml.Schema; /// @@ -44,6 +45,14 @@ public virtual string PropertyNameFromElement(string typeModelName, string eleme return typeModelName.ToTitleCase(_namingScheme) + elementName.ToTitleCase(_namingScheme); } + /// + public virtual string TypeNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute) + => PropertyNameFromAttribute(typeModelName, attributeName, attribute); + + /// + public virtual string TypeNameFromElement(string typeModelName, string elementName, XmlSchemaElement element) + => PropertyNameFromElement(typeModelName, elementName, element); + /// /// Creates a name for an enum member based on a value /// diff --git a/XmlSchemaClassGenerator/NamingProviders/SubstituteNamingProvider.cs b/XmlSchemaClassGenerator/NamingProviders/SubstituteNamingProvider.cs new file mode 100644 index 00000000..cba9a081 --- /dev/null +++ b/XmlSchemaClassGenerator/NamingProviders/SubstituteNamingProvider.cs @@ -0,0 +1,113 @@ +using System.Collections.Generic; +using System.Xml; +using System.Xml.Schema; + +namespace XmlSchemaClassGenerator.NamingProviders +{ + /// + /// Provides options to customize member names, and automatically substitute names for defined types/members. + /// + public class SubstituteNamingProvider + : NamingProvider, INamingProvider + { + private readonly Dictionary _nameSubstitutes; + + /// + public SubstituteNamingProvider(NamingScheme namingScheme) + : this(namingScheme, new()) + { + } + + /// + public SubstituteNamingProvider(Dictionary nameSubstitutes) + : this(NamingScheme.PascalCase, nameSubstitutes) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The naming scheme. + /// + /// A dictionary containing name substitute pairs. + /// + /// Keys need to be prefixed with an appropriate kind ID as documented at: + /// https://t.ly/HHEI. + /// + /// Prefix with A: to substitute any type/member. + /// + public SubstituteNamingProvider(NamingScheme namingScheme, Dictionary nameSubstitutes) + : base(namingScheme) + { + _nameSubstitutes = nameSubstitutes; + } + + /// + public override string PropertyNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute) + => SubstituteName("P", base.PropertyNameFromAttribute(typeModelName, attributeName, attribute)); + + /// + public override string PropertyNameFromElement(string typeModelName, string elementName, XmlSchemaElement element) + => SubstituteName("P", base.PropertyNameFromElement(typeModelName, elementName, element)); + + /// + public override string TypeNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute) + => SubstituteName("T", base.PropertyNameFromAttribute(typeModelName, attributeName, attribute)); + + /// + public override string TypeNameFromElement(string typeModelName, string elementName, XmlSchemaElement element) + => SubstituteName("T", base.PropertyNameFromElement(typeModelName, elementName, element)); + + /// + public override string EnumMemberNameFromValue(string enumName, string value, XmlSchemaEnumerationFacet xmlFacet) + => SubstituteName($"T:{enumName}", base.EnumMemberNameFromValue(enumName, value, xmlFacet)); + + /// + public override string ComplexTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaComplexType complexType) + => SubstituteName("T", base.ComplexTypeNameFromQualifiedName(qualifiedName, complexType)); + + /// + public override string AttributeGroupTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaAttributeGroup attributeGroup) + => SubstituteName("T", base.AttributeGroupTypeNameFromQualifiedName(qualifiedName, attributeGroup)); + + /// + public override string GroupTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaGroup group) + => SubstituteName("T", base.GroupTypeNameFromQualifiedName(qualifiedName, group)); + + /// + public override string SimpleTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaSimpleType simpleType) + => SubstituteName("T", base.SimpleTypeNameFromQualifiedName(qualifiedName, simpleType)); + + /// + public override string RootClassNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaElement xmlElement) + => SubstituteName("T", base.RootClassNameFromQualifiedName(qualifiedName, xmlElement)); + + /// + public override string EnumTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaSimpleType xmlSimpleType) + => SubstituteName("T", base.EnumTypeNameFromQualifiedName(qualifiedName, xmlSimpleType)); + + /// + public override string AttributeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaAttribute xmlAttribute) + => SubstituteName("P", base.AttributeNameFromQualifiedName(qualifiedName, xmlAttribute)); + + /// + public override string ElementNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaElement xmlElement) + => SubstituteName("P", base.ElementNameFromQualifiedName(qualifiedName, xmlElement)); + + private string SubstituteName(string typeIdPrefix, string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return name; + } + + string substituteName; + if (_nameSubstitutes.TryGetValue($"{typeIdPrefix}:{name}", out substituteName) || _nameSubstitutes.TryGetValue($"A:{name}", out substituteName)) + { + return substituteName; + } + + return name; + } + } +} From a00972e2b62798495881060c77b22ca5519ec662 Mon Sep 17 00:00:00 2001 From: Adam Smith-Platts Date: Sun, 30 Apr 2023 23:35:59 +1000 Subject: [PATCH 2/2] Updated README for substitutions --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86d19d25..bd24a4b4 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,26 @@ Options: If no mapping is found for an XML namespace, a name is generated automatically (may fail). --nf, --namespaceFile=VALUE - file containing mapppings from XML namespaces to C# - namespaces + file containing mappings from XML namespaces to C# + namespaces The line format is one mapping per line: XML namespace = C# namespace [optional file name]. Lines starting with # and empty lines are ignored. + --tns, --typeNameSubstitute=VALUE + substitute a generated type/member name + Separate type/member name and substitute name by + '='. + Prefix type/member name with an appropriate kind + ID as documented at: https://t.ly/HHEI. + Prefix with 'A:' to substitute any type/member. + --tnsf, --typeNameSubstituteFile=VALUE + file containing generated type/member name + substitute mappings + The line format is one mapping per line: + prefixed type/member name = substitute name. + Lines starting with # and empty lines are + ignored. -o, --output=FOLDER the FOLDER to write the resulting .cs files to -i, --integer=TYPE map xs:integer and derived types to TYPE instead of automatic approximation @@ -230,6 +244,37 @@ http://example.com = Example.NamespaceB b.xsd Use the `--nf` option to specify the mapping file. +### Substituting generated C# type and member names + +If a xsd file specifies obscure names for their types (classes, enums) or members (properties), you can substitute these using the `--tns`/`--typeNameSubstitute=` parameter: + +``` +xscgen --tns T:Example_RootType=Example --tns T:Example_RootTypeExampleScope=ExampleScope --tns P:StartDateDateTimeValue=StartDate example.xsd +``` + +The syntax for substitution is: `{kindId}:{generatedName}={substituteName}` + +The `{kindId}` is a single character identifier based on [documentation/analysis ID format](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d42-id-string-format), where valid values are: + +| ID | Scope | +| - | - | +| `P` | Property | +| `T` | Type: `class`, `enum`, `interface` | +| `A` | Any property and/or type | + +#### Using substitution files + +Instead of specifying the substitutions on the command line you can also use a substitution file which should contain one substitution per line in the following format: + +``` +# Comment +T:Example_RootType = Example +T:Example_RootTypeExampleScope = ExampleScope +P:StartDateDateTimeValue = StartDate +``` + +Use the `--tnsf`/`--typeNameSubstituteFile` option to specify the substitution file. + Nullables ---------------------------------