Skip to content

Commit

Permalink
Add support for Date primitive (#22)
Browse files Browse the repository at this point in the history
* Add support for Date primitive

* Improve Code Coverage

* Removed constructor from DateOnlyConverter and MapDateOnlyConverter
Removed System.Runtime references
Added Null forgiving operator in unit tests

* Updated the converter classes and unit test to remove null references warnings

* Updated comments and string null check

Co-authored-by: NancyGargMS <v-nancygarg@DESKTOP-OPTHJ0J>
  • Loading branch information
NancyGargMS and NancyGargMS committed Nov 18, 2022
1 parent dea17cb commit b2f3765
Show file tree
Hide file tree
Showing 22 changed files with 480 additions and 37 deletions.
8 changes: 6 additions & 2 deletions README.md
Expand Up @@ -6,7 +6,7 @@ This Digital Twin Definition Language (DTDL) Model Generator parses your DTDL js

## Project Components

- *Generator*: This is the core aspect of this project and is the package that's published to NuGet.
- *Generator*: This is the core aspect of this project (supports .Net 6 or higher) and is the package that's published to NuGet.
- *Generator.TemplateProject*: This is a template project that serves a couple purposes.
1. It serves as a holding-ground for our custom, complementary classes that help connect the dots between certain aspects of the generated model classes.
2. In the event a user of the generator doesn't have their own project destination to place the generated classes, this project serves as a template for the user to start from. Our generator will inject the correct Namespace and Assembly information into the template project based on options passed into the Generator.
Expand All @@ -29,7 +29,11 @@ The following are some Prerequisites/assumptions to be considered:

### For Users
- The following classes have been copied at the location where model generator assembly is being executed:
- DateOnlyConverter.cs
- DurationConverter.cs
- Extensions.cs
- MapDateOnlyConverter.cs
- MapDurationConverter.cs
- ModelHelper.cs
- Relationship.cs
- RelationshipCollection.cs
Expand All @@ -56,7 +60,7 @@ The following are some Prerequisites/assumptions to be considered:
| Schemas | DTDL v2 | Model Generator |
| ------- | ------- | --------------- |
| Boolean | :white_check_mark: | :white_check_mark: |
| Date | :white_check_mark: | :x: |
| Date | :white_check_mark: | :white_check_mark: |
| DateTime | :white_check_mark: | :white_check_mark: |
| Double | :white_check_mark: | :white_check_mark: |
| Duration | :white_check_mark: | :white_check_mark: |
Expand Down
36 changes: 36 additions & 0 deletions src/Generator.TemplateProject/Custom/DateOnlyConverter.cs
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Generator.CustomModels;

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;

/// <summary>
/// Converts an RFC 3339 Date into a DateOnly.
/// </summary>
public class DateOnlyConverter : JsonConverter<DateOnly>
{
private readonly string serializationFormat = "yyyy-MM-dd";
/// <inheritdoc/>
public override DateOnly Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();

if(string.IsNullOrWhiteSpace(value))
{
return new DateOnly();
}
return DateOnly.Parse(value);
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateOnly value,
JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(serializationFormat));
}
9 changes: 6 additions & 3 deletions src/Generator.TemplateProject/Custom/DurationConverter.cs
Expand Up @@ -18,9 +18,12 @@ public class DurationConverter : JsonConverter<TimeSpan>
/// <inheritdoc/>
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
#pragma warning disable CS8604 // Possible null reference argument. Read() is not called if the value is null. Verified in unit test.
return XmlConvert.ToTimeSpan(reader.GetString());
#pragma warning restore CS8604 // Possible null reference argument.
var value = reader.GetString();
if(string.IsNullOrWhiteSpace(value))
{
return TimeSpan.Zero;
}
return XmlConvert.ToTimeSpan(value);
}

/// <inheritdoc/>
Expand Down
76 changes: 76 additions & 0 deletions src/Generator.TemplateProject/Custom/MapDateOnlyConverter.cs
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Generator.CustomModels;

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;

/// <summary>
/// Converts a map of RFC 3339 into an <see cref="IDictionary{String, DateOnly}"/>.
/// </summary>
public class MapDateOnlyConverter : JsonConverter<IDictionary<string, DateOnly>>
{
private readonly string serializationFormat = "yyyy-MM-dd";
/// <inheritdoc/>
public override IDictionary<string, DateOnly> Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("First token was not the start of a JSON object.");
}

var dictionary = new Dictionary<string, DateOnly>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return dictionary;
}

if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}

var key = reader.GetString();

reader.Read();
if (reader.TokenType == JsonTokenType.Null || string.IsNullOrWhiteSpace(key))
{
continue;
}

var value = reader.GetString();

if(!string.IsNullOrWhiteSpace(value))
{
var parsedValue = DateOnly.Parse(value);
dictionary.Add(key, parsedValue);
}
}

throw new JsonException("Final token was not the end of a JSON object.");
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, IDictionary<string, DateOnly> dictionary,
JsonSerializerOptions options)
{
writer.WriteStartObject();

foreach ((string key, DateOnly value) in dictionary)
{
writer.WritePropertyName(options.PropertyNamingPolicy?.ConvertName(key) ?? key);
writer.WriteStringValue(value.ToString(serializationFormat));
}

writer.WriteEndObject();
}
}
17 changes: 8 additions & 9 deletions src/Generator.TemplateProject/Custom/MapDurationConverter.cs
Expand Up @@ -12,7 +12,7 @@ namespace Generator.CustomModels;
using System.Xml;

/// <summary>
/// Converts a map of ISO 8601 durations into an IDictionary&lt;string, TimeSpan&lt;.
/// Converts a map of ISO 8601 durations into an <see cref="IDictionary{String, TimeSpan}"/>.
/// </summary>
public class MapDurationConverter : JsonConverter<IDictionary<string, TimeSpan>>
{
Expand Down Expand Up @@ -41,18 +41,17 @@ public class MapDurationConverter : JsonConverter<IDictionary<string, TimeSpan>>
var key = reader.GetString();

reader.Read();
if (reader.TokenType == JsonTokenType.Null)
if (reader.TokenType == JsonTokenType.Null || string.IsNullOrWhiteSpace(key))
{
continue;
}
var value = reader.GetString();

#pragma warning disable CS8604 // Possible null reference argument.
// reader.GetString() won't return null, already checked above.
var value = XmlConvert.ToTimeSpan(reader.GetString());

// key cannot be null.
dictionary.Add(key, value);
#pragma warning restore CS8604 // Possible null reference argument.
if (!string.IsNullOrWhiteSpace(value))
{
var parsedValue = XmlConvert.ToTimeSpan(value);
dictionary.Add(key, parsedValue);
}
}

throw new JsonException("Final token was not the end of a JSON object.");
Expand Down
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net6</TargetFrameworks>
<TargetFrameworks>net6</TargetFrameworks>
<LangVersion>latest</LangVersion>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
Expand Down
5 changes: 5 additions & 0 deletions src/Generator/Base/ClassEntity.cs
Expand Up @@ -61,6 +61,11 @@ protected Property CreateProperty(DTNamedEntityInfo entity, DTSchemaInfo schema)
return new DurationProperty(entity, Options);
}

if (schema is DTDateInfo)
{
return new DateOnlyProperty(entity, Options);
}

return new PrimitiveProperty(entity, schema, Name, Options);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Generator/ModelGenerator.cs
Expand Up @@ -10,8 +10,10 @@ public class ModelGenerator
{
private readonly IEnumerable<string> customClasses = new List<string>
{
"DateOnlyConverter.cs",
"DurationConverter.cs",
"Extensions.cs",
"MapDateOnlyConverter.cs",
"MapDurationConverter.cs",
"ModelHelper.cs",
"Relationship.cs",
Expand Down
21 changes: 21 additions & 0 deletions src/Generator/Property/DateOnlyProperty.cs
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DigitalWorkplace.DigitalTwins.Models.Generator;

internal class DateOnlyProperty : Property
{
internal DateOnlyProperty(DTNamedEntityInfo entity, ModelGeneratorOptions options) : base(options)
{
Name = entity.Name;
JsonName = entity.Name;
Obsolete = entity.IsObsolete();
Type = "DateOnly?";
}

internal override void WriteTo(StreamWriter streamWriter)
{
streamWriter.WriteLine($"{indent}{indent}[JsonConverter(typeof(DateOnlyConverter))]");
base.WriteTo(streamWriter);
}
}
5 changes: 5 additions & 0 deletions src/Generator/Property/MapProperty.cs
Expand Up @@ -40,6 +40,11 @@ internal override void WriteTo(StreamWriter streamWriter)
streamWriter.WriteLine($"{indent}{indent}[JsonConverter(typeof(MapDurationConverter))]");
}

if (mapValue == nameof(DateOnly))
{
streamWriter.WriteLine($"{indent}{indent}[JsonConverter(typeof(MapDateOnlyConverter))]");
}

base.WriteTo(streamWriter);
}
}
3 changes: 2 additions & 1 deletion src/Generator/Types.cs
Expand Up @@ -25,7 +25,8 @@ internal static class Types
{ DTEntityKind.Float, "float" },
{ DTEntityKind.Double, "double" },
{ DTEntityKind.String, "string" },
{ DTEntityKind.Duration, "TimeSpan" }
{ DTEntityKind.Duration, "TimeSpan" },
{ DTEntityKind.Date, "DateOnly" }
};

internal static bool TryGetNullable(DTEntityKind entityKind, out string? type)
Expand Down
10 changes: 8 additions & 2 deletions test/Generator.Tests.Generated/Asset.cs
Expand Up @@ -27,9 +27,15 @@ public Asset()
[JsonConverter(typeof(DurationConverter))]
[JsonPropertyName("maintenanceInterval")]
public TimeSpan? MaintenanceInterval { get; set; }
[JsonConverter(typeof(DateOnlyConverter))]
[JsonPropertyName("installedOn")]
public DateOnly? InstalledOn { get; set; }
[JsonConverter(typeof(MapDurationConverter))]
[JsonPropertyName("runtimeDurations")]
public IDictionary<string, TimeSpan>? RuntimeDurations { get; set; }
[JsonConverter(typeof(MapDateOnlyConverter))]
[JsonPropertyName("runtimeDetails")]
public IDictionary<string, DateOnly>? RuntimeDetails { get; set; }
[JsonIgnore]
public AssetIsLocatedInRelationshipCollection IsLocatedIn { get; set; } = new AssetIsLocatedInRelationshipCollection();
public override bool Equals(object? obj)
Expand All @@ -39,7 +45,7 @@ public override bool Equals(object? obj)

public bool Equals(Asset? other)
{
return other is not null && Id == other.Id && Metadata.ModelId == other.Metadata.ModelId && AssetTag == other.AssetTag && Name == other.Name && SerialNumber == other.SerialNumber && MaintenanceInterval == other.MaintenanceInterval && RuntimeDurations == other.RuntimeDurations;
return other is not null && Id == other.Id && Metadata.ModelId == other.Metadata.ModelId && AssetTag == other.AssetTag && Name == other.Name && SerialNumber == other.SerialNumber && MaintenanceInterval == other.MaintenanceInterval && InstalledOn == other.InstalledOn && RuntimeDurations == other.RuntimeDurations && RuntimeDetails == other.RuntimeDetails;
}

public static bool operator ==(Asset? left, Asset? right)
Expand All @@ -54,7 +60,7 @@ public bool Equals(Asset? other)

public override int GetHashCode()
{
return this.CustomHash(Id?.GetHashCode(), Metadata?.ModelId?.GetHashCode(), AssetTag?.GetHashCode(), Name?.GetHashCode(), SerialNumber?.GetHashCode(), MaintenanceInterval?.GetHashCode(), RuntimeDurations?.GetHashCode());
return this.CustomHash(Id?.GetHashCode(), Metadata?.ModelId?.GetHashCode(), AssetTag?.GetHashCode(), Name?.GetHashCode(), SerialNumber?.GetHashCode(), MaintenanceInterval?.GetHashCode(), InstalledOn?.GetHashCode(), RuntimeDurations?.GetHashCode(), RuntimeDetails?.GetHashCode());
}

public bool Equals(BasicDigitalTwin? other)
Expand Down
36 changes: 36 additions & 0 deletions test/Generator.Tests.Generated/DateOnlyConverter.cs
@@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Generator.Tests.Generated;

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml;

/// <summary>
/// Converts an RFC 3339 Date into a DateOnly.
/// </summary>
public class DateOnlyConverter : JsonConverter<DateOnly>
{
private readonly string serializationFormat = "yyyy-MM-dd";
/// <inheritdoc/>
public override DateOnly Read(ref Utf8JsonReader reader,
Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();

if(string.IsNullOrWhiteSpace(value))
{
return new DateOnly();
}
return DateOnly.Parse(value);
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateOnly value,
JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(serializationFormat));
}
9 changes: 6 additions & 3 deletions test/Generator.Tests.Generated/DurationConverter.cs
Expand Up @@ -18,9 +18,12 @@ public class DurationConverter : JsonConverter<TimeSpan>
/// <inheritdoc/>
public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
#pragma warning disable CS8604 // Possible null reference argument. Read() is not called if the value is null. Verified in unit test.
return XmlConvert.ToTimeSpan(reader.GetString());
#pragma warning restore CS8604 // Possible null reference argument.
var value = reader.GetString();
if(string.IsNullOrWhiteSpace(value))
{
return TimeSpan.Zero;
}
return XmlConvert.ToTimeSpan(value);
}

/// <inheritdoc/>
Expand Down
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.1;net6</TargetFrameworks>
<TargetFrameworks>net6</TargetFrameworks>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<RootNamespace>Generator.Tests.Generated</RootNamespace>
Expand Down

0 comments on commit b2f3765

Please sign in to comment.