Skip to content

Commit 0064c06

Browse files
committed
DateOnly and TimeOnly implementation
1 parent 6f928ee commit 0064c06

6 files changed

Lines changed: 169 additions & 30 deletions

File tree

XmlSchemaClassGenerator.Console/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ static int Main(string[] args)
2424
var nameSubstitutes = new List<string>();
2525
var outputFolder = (string)null;
2626
bool dateTimeWithTimeZone = false;
27+
bool useDateOnly = false;
2728
Type integerType = null;
2829
var useIntegerTypeAsFallback = false;
2930
var namespacePrefix = "";
@@ -93,6 +94,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l
9394
Lines starting with # and empty lines are ignored.", v => nameSubstituteFiles.Add(v) },
9495
{ "o|output=", "the {FOLDER} to write the resulting .cs files to", v => outputFolder = v },
9596
{ "d|datetime-offset", "map xs:datetime and derived types to System.DateTimeOffset instead of System.DateTime", v => dateTimeWithTimeZone = v != null },
97+
{ "do|dateOnly", "map xs:date to System.DateOnly and xs:time to System.TimeOnly", v => useDateOnly = v != null },
9698
{ "i|integer=", @"map xs:integer and derived types to {TYPE} instead of automatic approximation
9799
{TYPE} can be i[nt], l[ong], or d[ecimal]", v => {
98100
switch (v)
@@ -238,6 +240,7 @@ A file name may be given by appending a pipe sign (|) followed by a file name (l
238240
IntegerDataType = integerType,
239241
UseIntegerDataTypeAsFallback = useIntegerTypeAsFallback,
240242
DateTimeWithTimeZone = dateTimeWithTimeZone,
243+
UseDateOnly = useDateOnly,
241244
EntityFramework = entityFramework,
242245
GenerateInterfaces = interfaces,
243246
AssemblyVisible = assembly,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Xml.Schema;
6+
using Xunit;
7+
8+
namespace XmlSchemaClassGenerator.Tests;
9+
10+
public sealed class DateOnlyTimeOnlyTests
11+
{
12+
private static IEnumerable<string> ConvertXml(string xsd, Generator generatorPrototype)
13+
{
14+
var writer = new MemoryOutputWriter();
15+
16+
var gen = new Generator
17+
{
18+
OutputWriter = writer,
19+
Version = new("Tests", "1.0.0.1"),
20+
NamespaceProvider = generatorPrototype.NamespaceProvider,
21+
GenerateNullables = generatorPrototype.GenerateNullables,
22+
DateTimeWithTimeZone = generatorPrototype.DateTimeWithTimeZone,
23+
UseDateOnly = generatorPrototype.UseDateOnly,
24+
DataAnnotationMode = generatorPrototype.DataAnnotationMode,
25+
GenerateDesignerCategoryAttribute = generatorPrototype.GenerateDesignerCategoryAttribute,
26+
GenerateComplexTypesForCollections = generatorPrototype.GenerateComplexTypesForCollections,
27+
EntityFramework = generatorPrototype.EntityFramework,
28+
AssemblyVisible = generatorPrototype.AssemblyVisible,
29+
GenerateInterfaces = generatorPrototype.GenerateInterfaces,
30+
MemberVisitor = generatorPrototype.MemberVisitor,
31+
CodeTypeReferenceOptions = generatorPrototype.CodeTypeReferenceOptions
32+
};
33+
34+
var set = new XmlSchemaSet();
35+
36+
using (var stringReader = new StringReader(xsd))
37+
{
38+
var schema = XmlSchema.Read(stringReader, (_, e) => throw new InvalidOperationException($"{e.Severity}: {e.Message}", e.Exception));
39+
ArgumentNullException.ThrowIfNull(schema);
40+
set.Add(schema);
41+
}
42+
43+
gen.Generate(set);
44+
45+
return writer.Content;
46+
}
47+
48+
[Fact]
49+
public void WhenUseDateOnlyIsTrue_DateOnlyAndTimeOnlyAreGenerated()
50+
{
51+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
52+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
53+
<xs:complexType name=""document"">
54+
<xs:sequence>
55+
<xs:element name=""someDate"" type=""xs:date"" />
56+
<xs:element name=""someTime"" type=""xs:time"" />
57+
</xs:sequence>
58+
</xs:complexType>
59+
</xs:schema>";
60+
61+
var generatedType = ConvertXml(
62+
xsd, new()
63+
{
64+
NamespaceProvider = new()
65+
{
66+
GenerateNamespace = _ => "Test"
67+
},
68+
UseDateOnly = true
69+
});
70+
71+
var code = string.Join(Environment.NewLine, generatedType);
72+
73+
Assert.Contains("public System.DateOnly SomeDate", code);
74+
Assert.Contains("public System.TimeOnly SomeTime", code);
75+
Assert.Contains("DataType=\"date\"", code);
76+
Assert.Contains("DataType=\"time\"", code);
77+
}
78+
79+
[Fact]
80+
public void WhenUseDateOnlyIsFalse_DateTimeIsGenerated()
81+
{
82+
var xsd = @$"<?xml version=""1.0"" encoding=""UTF-8""?>
83+
<xs:schema elementFormDefault=""qualified"" xmlns:xs=""http://www.w3.org/2001/XMLSchema"">
84+
<xs:complexType name=""document"">
85+
<xs:sequence>
86+
<xs:element name=""someDate"" type=""xs:date"" />
87+
<xs:element name=""someTime"" type=""xs:time"" />
88+
</xs:sequence>
89+
</xs:complexType>
90+
</xs:schema>";
91+
92+
var generatedType = ConvertXml(
93+
xsd, new()
94+
{
95+
NamespaceProvider = new()
96+
{
97+
GenerateNamespace = _ => "Test"
98+
},
99+
UseDateOnly = false
100+
});
101+
102+
var code = string.Join(Environment.NewLine, generatedType);
103+
104+
Assert.Contains("public System.DateTime SomeDate", code);
105+
Assert.Contains("public System.DateTime SomeTime", code);
106+
Assert.Contains("DataType=\"date\"", code);
107+
Assert.Contains("DataType=\"time\"", code);
108+
}
109+
}

XmlSchemaClassGenerator/CodeUtilities.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,9 @@ public static Type GetEffectiveType(this XmlSchemaDatatype type, GeneratorConfig
169169
XmlTypeCode.AnyAtomicType => configuration.MapUnionToWidestCommonType ? GetUnionType(configuration, schemaType, attribute) : typeof(string), // union
170170
XmlTypeCode.AnyUri or XmlTypeCode.GDay or XmlTypeCode.GMonth or XmlTypeCode.GMonthDay or XmlTypeCode.GYear or XmlTypeCode.GYearMonth => typeof(string),
171171
XmlTypeCode.Duration => configuration.NetCoreSpecificCode ? type.ValueType : typeof(string),
172-
XmlTypeCode.Time or XmlTypeCode.DateTime => configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime),
172+
XmlTypeCode.Time => configuration.UseDateOnly ? typeof(TimeOnly) : (configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime)),
173+
XmlTypeCode.Date => configuration.UseDateOnly ? typeof(DateOnly) : typeof(DateTime),
174+
XmlTypeCode.DateTime => configuration.DateTimeWithTimeZone ? typeof(DateTimeOffset) : typeof(DateTime),
173175
XmlTypeCode.Idref => typeof(string),
174176
XmlTypeCode.Integer or XmlTypeCode.NegativeInteger or XmlTypeCode.NonNegativeInteger or XmlTypeCode.NonPositiveInteger or XmlTypeCode.PositiveInteger => GetIntegerDerivedType(type, configuration, restrictions),
175177
XmlTypeCode.Decimal when restrictions.OfType<FractionDigitsRestrictionModel>().SingleOrDefault() is { IsSupported: true, Value: 0 } => GetIntegerDerivedType(type, configuration, restrictions),
@@ -436,6 +438,11 @@ public static CodeTypeReference CreateTypeReference(Type type, GeneratorConfigur
436438
}
437439
else
438440
{
441+
if (type == typeof(DateOnly))
442+
return new CodeTypeReference("System.DateOnly", conf.CodeTypeReferenceOptions);
443+
if (type == typeof(TimeOnly))
444+
return new CodeTypeReference("System.TimeOnly", conf.CodeTypeReferenceOptions);
445+
439446
var typeRef = new CodeTypeReference(type, conf.CodeTypeReferenceOptions);
440447

441448
foreach (var typeArgRef in typeRef.TypeArguments.OfType<CodeTypeReference>())
@@ -530,6 +537,8 @@ private static TypeInfo Make(string @namespace, [CallerMemberName] string name =
530537
public static TypeInfo AllowNull { get; } = Make(CodeAnalysis);
531538
public static TypeInfo MaybeNull { get; } = Make(CodeAnalysis);
532539
}
540+
internal struct DateOnly { }
541+
internal struct TimeOnly { }
533542
}
534543

535544
//Fixes a bug with VS2019 (https://developercommunity.visualstudio.com/content/problem/1244809/error-cs0518-predefined-type-systemruntimecompiler.html)

XmlSchemaClassGenerator/Generator.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ private Uri NormalizeUri(Uri baseUri, Uri resolvedUri)
3232
{
3333
case "none": return resolvedUri;
3434
case "same":
35-
{
36-
newScheme = baseUri.Scheme;
37-
break;
38-
}
35+
{
36+
newScheme = baseUri.Scheme;
37+
break;
38+
}
3939
}
4040

4141
var builder = new UriBuilder(resolvedUri) { Scheme = newScheme, Port = -1 };
@@ -53,7 +53,7 @@ public class Generator
5353
public string ForceUriScheme
5454
{
5555
get { return _configuration.ForceUriScheme; }
56-
set { _configuration.ForceUriScheme = value; }
56+
set { _configuration.ForceUriScheme = value; }
5757
}
5858

5959
public NamespaceProvider NamespaceProvider
@@ -228,6 +228,12 @@ public bool DateTimeWithTimeZone
228228
set { _configuration.DateTimeWithTimeZone = value; }
229229
}
230230

231+
public bool UseDateOnly
232+
{
233+
get { return _configuration.UseDateOnly; }
234+
set { _configuration.UseDateOnly = value; }
235+
}
236+
231237
public bool EntityFramework
232238
{
233239
get { return _configuration.EntityFramework; }
@@ -356,7 +362,7 @@ public bool NetCoreSpecificCode
356362
public bool UseArrayItemAttribute
357363
{
358364
get { return _configuration.UseArrayItemAttribute; }
359-
set { _configuration.UseArrayItemAttribute = value;}
365+
set { _configuration.UseArrayItemAttribute = value; }
360366
}
361367

362368
public bool GenerateCommandLineArgumentsComment

XmlSchemaClassGenerator/GeneratorConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ public NamingScheme NamingScheme
160160
/// </summary>
161161
public bool DateTimeWithTimeZone { get; set; } = false;
162162
/// <summary>
163+
/// Generate Net 6.0 DateOnly and TimeOnly properties for xs:time and xs:date elements
164+
/// </summary>
165+
public bool UseDateOnly { get; set; } = false;
166+
/// <summary>
163167
/// Generate Entity Framework Code First compatible classes
164168
/// </summary>
165169
public bool EntityFramework { get; set; }

XmlSchemaClassGenerator/TypeModel.cs

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,14 @@ public override CodeExpression GetDefaultValueFor(string defaultString, bool att
13061306
{
13071307
return new CodeMethodInvokeExpression(TypeRefExpr<DateTime>(), nameof(DateTime.Parse), new CodePrimitiveExpression(defaultString));
13081308
}
1309+
else if (type == typeof(DateOnly))
1310+
{
1311+
return new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("System.DateOnly"), "Parse", new CodePrimitiveExpression(defaultString));
1312+
}
1313+
else if (type == typeof(TimeOnly))
1314+
{
1315+
return new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("System.TimeOnly"), "Parse", new CodePrimitiveExpression(defaultString));
1316+
}
13091317
else if (type == typeof(TimeSpan))
13101318
{
13111319
return new CodeMethodInvokeExpression(TypeRefExpr<XmlConvert>(), nameof(XmlConvert.ToTimeSpan), new CodePrimitiveExpression(defaultString));
@@ -1411,20 +1419,20 @@ protected IEnumerable<CodeCommentStatement> GetComments(IReadOnlyList<Documentat
14111419
yield return new CodeCommentStatement("<summary>", true);
14121420

14131421
foreach (var doc in docs.Where
1414-
(
1415-
d => !string.IsNullOrWhiteSpace(d.Text)
1416-
&& (string.IsNullOrEmpty(d.Language)
1417-
|| Configuration.CommentLanguages.Count is 0
1418-
|| Configuration.CommentLanguages.Contains(d.Language)
1419-
|| Configuration.CommentLanguages
1420-
.Any(l => d.Language.StartsWith(l, StringComparison.OrdinalIgnoreCase)))
1421-
)
1422-
.OrderBy(d => d.Language))
1422+
(
1423+
d => !string.IsNullOrWhiteSpace(d.Text)
1424+
&& (string.IsNullOrEmpty(d.Language)
1425+
|| Configuration.CommentLanguages.Count is 0
1426+
|| Configuration.CommentLanguages.Contains(d.Language)
1427+
|| Configuration.CommentLanguages
1428+
.Any(l => d.Language.StartsWith(l, StringComparison.OrdinalIgnoreCase)))
1429+
)
1430+
.OrderBy(d => d.Language))
14231431
{
1424-
var text = doc.Text;
1425-
var comment = $"<para{(string.IsNullOrEmpty(doc.Language) ? "" : $@" xml:lang=""{doc.Language}""")}>{CodeUtilities.NormalizeNewlines(text).Trim()}</para>";
1432+
var text = doc.Text;
1433+
var comment = $"<para{(string.IsNullOrEmpty(doc.Language) ? "" : $@" xml:lang=""{doc.Language}""")}>{CodeUtilities.NormalizeNewlines(text).Trim()}</para>";
14261434

1427-
yield return new(comment, true);
1435+
yield return new(comment, true);
14281436
}
14291437

14301438
yield return new CodeCommentStatement("</summary>", true);
@@ -1445,16 +1453,16 @@ protected void AddDescription(CodeAttributeDeclarationCollection attributes, IRe
14451453

14461454
private string GetSingleDoc(IReadOnlyList<DocumentationModel> docs) => string.Join
14471455
(
1448-
" ",
1449-
docs.Where
1450-
(
1451-
d => !string.IsNullOrWhiteSpace(d.Text)
1452-
&& (string.IsNullOrEmpty(d.Language)
1453-
|| Configuration.CommentLanguages.Count is 0
1454-
|| Configuration.CommentLanguages.Contains(d.Language)
1455-
|| Configuration.CommentLanguages
1456-
.Any(l => d.Language.StartsWith(l, StringComparison.OrdinalIgnoreCase)))
1457-
)
1458-
.Select(x => x.Text)
1456+
" ",
1457+
docs.Where
1458+
(
1459+
d => !string.IsNullOrWhiteSpace(d.Text)
1460+
&& (string.IsNullOrEmpty(d.Language)
1461+
|| Configuration.CommentLanguages.Count is 0
1462+
|| Configuration.CommentLanguages.Contains(d.Language)
1463+
|| Configuration.CommentLanguages
1464+
.Any(l => d.Language.StartsWith(l, StringComparison.OrdinalIgnoreCase)))
1465+
)
1466+
.Select(x => x.Text)
14591467
);
14601468
}

0 commit comments

Comments
 (0)