Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a dedicated substitution naming provider #377

Merged
merged 2 commits into from
May 16, 2023
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
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<a name="nullables"></a>
---------------------------------

Expand Down
47 changes: 45 additions & 2 deletions XmlSchemaClassGenerator.Console/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using XmlSchemaClassGenerator;
using XmlSchemaClassGenerator.NamingProviders;
using System;
using System.CodeDom;
using System.Collections.Generic;
Expand All @@ -11,7 +12,7 @@
using System.Threading.Tasks;
using Mono.Options;
using Ganss.IO;

namespace XmlSchemaClassGenerator.Console
{
static class Program
Expand All @@ -20,6 +21,7 @@ static void Main(string[] args)
{
var showHelp = args.Length == 0;
var namespaces = new List<string>();
var nameSubstitutes = new List<string>();
var outputFolder = (string)null;
Type integerType = null;
var useIntegerTypeAsFallback = false;
Expand Down Expand Up @@ -59,6 +61,7 @@ static void Main(string[] args)
var useArrayItemAttribute = true;
var enumAsString = false;
var namespaceFiles = new List<string>();
var nameSubstituteFiles = new List<string>();

var options = new OptionSet {
{ "h|help", "show this message and exit", v => showHelp = v != null },
Expand All @@ -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 => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -265,6 +283,31 @@ private static void ParseNamespaceFiles(List<string> namespaces, List<string> na
}
}

private static void ParseNameSubstituteFiles(List<string> nameSubstitutes, List<string> 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...");
Expand Down
18 changes: 18 additions & 0 deletions XmlSchemaClassGenerator/EnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,5 +97,23 @@ public static IEnumerable<NamespaceHierarchyItem> MarkAmbiguousNamespaceTypes(
yield return hierarchyItem;
}
}

/// <summary>
/// Creates a <see cref="Dictionary{TKey,TValue}" /> from an <see cref="IEnumerable{T}" /> splitting the values into key and value pairs based on the <paramref name="delimiter"/>.
/// </summary>
/// <param name="source">An <see cref="IEnumerable{T}" /> to create a <see cref="Dictionary{TKey,TValue}" /> from.</param>
/// <param name="delimiter">An optional value that supplies the delimiter to use to create the key and value from the <paramref name="source"/>.</param>
/// <returns>A <see cref="Dictionary{TKey,TValue}" /> that contains keys and values. The values within each group are in the same order as in <paramref name="source" />.</returns>
public static Dictionary<string, string> ToDictionary(this IEnumerable<string> source, string delimiter = "=")
{
var result = new Dictionary<string, string>();
foreach (string value in source ?? Enumerable.Empty<string>())
{
var pair = value.Split(new string[] { delimiter ?? "=" }, 2, StringSplitOptions.None);
result.Add(pair[0], pair[1]);
}

return result;
}
}
}
18 changes: 18 additions & 0 deletions XmlSchemaClassGenerator/INamingProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@ public interface INamingProvider
/// <returns>Name of the property</returns>
string PropertyNameFromElement(string typeModelName, string elementName, XmlSchemaElement element);

/// <summary>
/// Creates a name for an inner type from an attribute name
/// </summary>
/// <param name="typeModelName">Name of the typeModel</param>
/// <param name="attributeName">Attribute name</param>
/// <param name="attribute">Original XSD attribute</param>
/// <returns>Name of the inner type</returns>
string TypeNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute);

/// <summary>
/// Creates a name for an inner type from an element name
/// </summary>
/// <param name="typeModelName">Name of the typeModel</param>
/// <param name="elementName">Element name</param>
/// <param name="element">Original XSD element</param>
/// <returns>Name of the inner type</returns>
string TypeNameFromElement(string typeModelName, string elementName, XmlSchemaElement element);

/// <summary>
/// Creates a name for an enum member based on a value
/// </summary>
Expand Down
4 changes: 2 additions & 2 deletions XmlSchemaClassGenerator/ModelBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))
Expand Down
9 changes: 9 additions & 0 deletions XmlSchemaClassGenerator/NamingProvider.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace XmlSchemaClassGenerator
{
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

/// <summary>
Expand Down Expand Up @@ -44,6 +45,14 @@ public virtual string PropertyNameFromElement(string typeModelName, string eleme
return typeModelName.ToTitleCase(_namingScheme) + elementName.ToTitleCase(_namingScheme);
}

/// <inheritdoc/>
public virtual string TypeNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute)
=> PropertyNameFromAttribute(typeModelName, attributeName, attribute);

/// <inheritdoc/>
public virtual string TypeNameFromElement(string typeModelName, string elementName, XmlSchemaElement element)
=> PropertyNameFromElement(typeModelName, elementName, element);

/// <summary>
/// Creates a name for an enum member based on a value
/// </summary>
Expand Down
113 changes: 113 additions & 0 deletions XmlSchemaClassGenerator/NamingProviders/SubstituteNamingProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;

namespace XmlSchemaClassGenerator.NamingProviders
{
/// <summary>
/// Provides options to customize member names, and automatically substitute names for defined types/members.
/// </summary>
public class SubstituteNamingProvider
: NamingProvider, INamingProvider
{
private readonly Dictionary<string, string> _nameSubstitutes;

/// <inheritdoc cref="SubstituteNamingProvider(NamingScheme, Dictionary{string, string})"/>
public SubstituteNamingProvider(NamingScheme namingScheme)
: this(namingScheme, new())
{
}

/// <inheritdoc cref="SubstituteNamingProvider(NamingScheme, Dictionary{string, string})"/>
public SubstituteNamingProvider(Dictionary<string, string> nameSubstitutes)
: this(NamingScheme.PascalCase, nameSubstitutes)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SubstituteNamingProvider"/> class.
/// </summary>
/// <param name="namingScheme">The naming scheme.</param>
/// <param name="nameSubstitutes">
/// A dictionary containing name substitute pairs.
/// <para>
/// Keys need to be prefixed with an appropriate kind ID as documented at:
/// <see href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d42-id-string-format">https://t.ly/HHEI</see>.
/// </para>
/// <para>Prefix with <c>A:</c> to substitute any type/member.</para>
/// </param>
public SubstituteNamingProvider(NamingScheme namingScheme, Dictionary<string, string> nameSubstitutes)
: base(namingScheme)
{
_nameSubstitutes = nameSubstitutes;
}

/// <inheritdoc/>
public override string PropertyNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute)
=> SubstituteName("P", base.PropertyNameFromAttribute(typeModelName, attributeName, attribute));

/// <inheritdoc/>
public override string PropertyNameFromElement(string typeModelName, string elementName, XmlSchemaElement element)
=> SubstituteName("P", base.PropertyNameFromElement(typeModelName, elementName, element));

/// <inheritdoc/>
public override string TypeNameFromAttribute(string typeModelName, string attributeName, XmlSchemaAttribute attribute)
=> SubstituteName("T", base.PropertyNameFromAttribute(typeModelName, attributeName, attribute));

/// <inheritdoc/>
public override string TypeNameFromElement(string typeModelName, string elementName, XmlSchemaElement element)
=> SubstituteName("T", base.PropertyNameFromElement(typeModelName, elementName, element));

/// <inheritdoc/>
public override string EnumMemberNameFromValue(string enumName, string value, XmlSchemaEnumerationFacet xmlFacet)
=> SubstituteName($"T:{enumName}", base.EnumMemberNameFromValue(enumName, value, xmlFacet));

/// <inheritdoc/>
public override string ComplexTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaComplexType complexType)
=> SubstituteName("T", base.ComplexTypeNameFromQualifiedName(qualifiedName, complexType));

/// <inheritdoc/>
public override string AttributeGroupTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaAttributeGroup attributeGroup)
=> SubstituteName("T", base.AttributeGroupTypeNameFromQualifiedName(qualifiedName, attributeGroup));

/// <inheritdoc/>
public override string GroupTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaGroup group)
=> SubstituteName("T", base.GroupTypeNameFromQualifiedName(qualifiedName, group));

/// <inheritdoc/>
public override string SimpleTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaSimpleType simpleType)
=> SubstituteName("T", base.SimpleTypeNameFromQualifiedName(qualifiedName, simpleType));

/// <inheritdoc/>
public override string RootClassNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaElement xmlElement)
=> SubstituteName("T", base.RootClassNameFromQualifiedName(qualifiedName, xmlElement));

/// <inheritdoc/>
public override string EnumTypeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaSimpleType xmlSimpleType)
=> SubstituteName("T", base.EnumTypeNameFromQualifiedName(qualifiedName, xmlSimpleType));

/// <inheritdoc/>
public override string AttributeNameFromQualifiedName(XmlQualifiedName qualifiedName, XmlSchemaAttribute xmlAttribute)
=> SubstituteName("P", base.AttributeNameFromQualifiedName(qualifiedName, xmlAttribute));

/// <inheritdoc/>
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;
}
}
}