From c769a2f857ddf8a59e71c144916710d2b8d9584b Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Fri, 16 Jun 2023 13:11:10 +0100 Subject: [PATCH] feat: Generate Raw and DateTimeOffset properties for google-datetime properties We keep separate string and object representations for the sake of compatibility. --- .../GeneratedCodeTest.cs | 92 +++++++++++++++++++ .../Google.Apis.ManufacturerCenter.v1.cs | 35 ++++++- .../Google.Api.Generator.Rest.csproj | 2 +- .../Models/DataPropertyModel.cs | 57 ++++++++++++ 4 files changed, 184 insertions(+), 2 deletions(-) diff --git a/Google.Api.Generator.Rest.Tests/GeneratedCodeTest.cs b/Google.Api.Generator.Rest.Tests/GeneratedCodeTest.cs index 4a85e28e..7f617bc0 100644 --- a/Google.Api.Generator.Rest.Tests/GeneratedCodeTest.cs +++ b/Google.Api.Generator.Rest.Tests/GeneratedCodeTest.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Google.Apis.ManufacturerCenter.v1; +using Google.Apis.ManufacturerCenter.v1.Data; using Google.Apis.Storage.v1; using Google.Apis.Storage.v1.Data; using System; @@ -27,6 +29,7 @@ namespace Google.Apis.Tests.Apis.Services; public class GeneratedCodeTest { #pragma warning disable CS0618 // Type or member is obsolete + // Tests around Discovery date-time format properties. [Fact] public void StorageBucketCreatedTime_Default() { @@ -83,6 +86,95 @@ private static void AssertStorageBucketCreatedTimeProperties(Bucket bucket) var actualJson = new StorageService().SerializeObject(bucket); Assert.Equal(expectedJson, actualJson); } + + // Tests around Discovery google-datetime format properties. + [Fact] + public void IssueTimestamp_Default() + { + var issue = new Issue(); + Assert.Null(issue.TimestampRaw); + Assert.Null(issue.Timestamp); + Assert.Null(issue.TimestampDateTimeOffset); + } + + [Fact] + public void IssueTimestamp_ParsedFromJson() + { + string json = "{'timestamp':'2023-06-14T12:34:45.123Z'}".Replace('\'', '"'); + var issue = (Issue) new ManufacturerCenterService().Serializer.Deserialize(json, typeof(Issue)); + AssertIssueTimestampProperties(issue); + } + + [Fact] + public void Issue_SetTimestampDateTimeOffset_Utc() + { + var issue = new Issue(); + issue.TimestampDateTimeOffset = new DateTimeOffset(2023, 6, 14, 12, 34, 45, 123, TimeSpan.Zero); + AssertIssueTimestampProperties(issue); + } + + [Fact] + public void Issue_SetTimestampDateTimeOffset_NonUtc() + { + var issue = new Issue(); + // This gets normalized to the UTC version. + issue.TimestampDateTimeOffset = new DateTimeOffset(2023, 6, 14, 13, 34, 45, 123, TimeSpan.FromHours(1)); + AssertIssueTimestampProperties(issue); + } + + [Fact] + public void Issue_SetTimestampRaw() + { + var issue = new Issue(); + issue.TimestampRaw = "2023-06-14T12:34:45.123Z"; + AssertIssueTimestampProperties(issue); + } + + [Fact] + public void Issue_SetTimestamp_DateTime() + { + var issue = new Issue(); + var dateTime = new DateTime(2023, 6, 14, 12, 34, 45, 123, DateTimeKind.Utc); + issue.Timestamp = dateTime; + AssertIssueTimestampProperties(issue); + } + + [Fact] + public void Issue_SetTimestamp_String() + { + var issue = new Issue(); + var text = "2023-06-14T12:34:45.123Z"; + issue.Timestamp = text; + // Can't call AssertIssueTimestampProperties as Timestamp is a string. + Assert.Equal(text, issue.TimestampRaw); + Assert.Equal(text, issue.Timestamp); + Assert.Equal(new DateTimeOffset(2023, 6, 14, 12, 34, 45, 123, TimeSpan.Zero), issue.TimestampDateTimeOffset); + } + + [Fact] + public void Issue_SetTimestamp_DateTimeOffset() + { + var issue = new Issue(); + var dateTimeOffset = new DateTimeOffset(2023, 6, 14, 12, 34, 45, 123, TimeSpan.Zero); + issue.Timestamp = dateTimeOffset; + // Can't call AssertIssueTimestampProperties as Timestamp is a DTO, and the string representation + // uses +00:00 instead of Z. This unfortunately means that TimestampDateTimeOffset will fail, + // but it matches the current behavior - basically setting a google-datetime to a DateTimeOffset + // (via the object property) in a request will cause a failure (unless the server-side parsing is lenient). + Assert.Equal("2023-06-14T12:34:45.123+00:00", issue.TimestampRaw); + Assert.Equal(dateTimeOffset, issue.Timestamp); + Assert.Throws(() => issue.TimestampDateTimeOffset); + } + + private static void AssertIssueTimestampProperties(Issue issue) + { + Assert.Equal("2023-06-14T12:34:45.123Z", issue.TimestampRaw); + Assert.Equal(new DateTime(2023, 6, 14, 12, 34, 45, 123, DateTimeKind.Utc), issue.Timestamp); + Assert.Equal(new DateTimeOffset(2023, 6, 14, 12, 34, 45, 123, TimeSpan.Zero), issue.TimestampDateTimeOffset); + string expectedJson = "{'timestamp':'2023-06-14T12:34:45.123Z'}".Replace('\'', '"'); + var actualJson = new ManufacturerCenterService().SerializeObject(issue); + Assert.Equal(expectedJson, actualJson); + } #pragma warning restore CS0618 // Type or member is obsolete } diff --git a/Google.Api.Generator.Rest.Tests/GoldenTestData/Google.Apis.ManufacturerCenter.v1/Google.Apis.ManufacturerCenter.v1.cs b/Google.Api.Generator.Rest.Tests/GoldenTestData/Google.Apis.ManufacturerCenter.v1/Google.Apis.ManufacturerCenter.v1.cs index 8a03d854..ea51d832 100644 --- a/Google.Api.Generator.Rest.Tests/GoldenTestData/Google.Apis.ManufacturerCenter.v1/Google.Apis.ManufacturerCenter.v1.cs +++ b/Google.Api.Generator.Rest.Tests/GoldenTestData/Google.Apis.ManufacturerCenter.v1/Google.Apis.ManufacturerCenter.v1.cs @@ -1119,9 +1119,42 @@ public class Issue : Google.Apis.Requests.IDirectResponseSchema [Newtonsoft.Json.JsonPropertyAttribute("severity")] public virtual string Severity { get; set; } + private string _timestampRaw; + + private object _timestamp; + /// The timestamp when this issue appeared. [Newtonsoft.Json.JsonPropertyAttribute("timestamp")] - public virtual object Timestamp { get; set; } + public virtual string TimestampRaw + { + get => _timestampRaw; + set + { + _timestamp = Google.Apis.Util.Utilities.DeserializeForGoogleFormat(value); + _timestampRaw = value; + } + } + + /// representation of . + [Newtonsoft.Json.JsonIgnoreAttribute] + [System.ObsoleteAttribute("This property is obsolete and may behave unexpectedly; please use TimestampDateTimeOffset instead.")] + public virtual object Timestamp + { + get => _timestamp; + set + { + _timestampRaw = Google.Apis.Util.Utilities.SerializeForGoogleFormat(value); + _timestamp = value; + } + } + + /// representation of . + [Newtonsoft.Json.JsonIgnoreAttribute] + public virtual System.DateTimeOffset? TimestampDateTimeOffset + { + get => Google.Apis.Util.Utilities.GetDateTimeOffsetFromString(TimestampRaw); + set => TimestampRaw = Google.Apis.Util.Utilities.GetStringFromDateTimeOffset(value); + } /// Short title describing the nature of the issue. [Newtonsoft.Json.JsonPropertyAttribute("title")] diff --git a/Google.Api.Generator.Rest/Google.Api.Generator.Rest.csproj b/Google.Api.Generator.Rest/Google.Api.Generator.Rest.csproj index f45a8667..fa83d31e 100644 --- a/Google.Api.Generator.Rest/Google.Api.Generator.Rest.csproj +++ b/Google.Api.Generator.Rest/Google.Api.Generator.Rest.csproj @@ -7,7 +7,7 @@ - + diff --git a/Google.Api.Generator.Rest/Models/DataPropertyModel.cs b/Google.Api.Generator.Rest/Models/DataPropertyModel.cs index 6d435184..0ce3c138 100644 --- a/Google.Api.Generator.Rest/Models/DataPropertyModel.cs +++ b/Google.Api.Generator.Rest/Models/DataPropertyModel.cs @@ -65,6 +65,7 @@ public IEnumerable GeneratePropertyDeclarations(SourceF _schema.Format switch { "date-time" => GenerateDateTimeProperties(ctx), + "google-datetime" => GenerateGoogleDateTimeProperties(ctx), _ => GenerateRegularProperty(ctx) }; @@ -114,6 +115,62 @@ private IEnumerable GenerateDateTimeProperties(SourceFi .WithSetBody(rawProperty.Assign(ctx.Type(typeof(Utilities)).Call(nameof(Utilities.GetStringFromDateTime))(valueParameter))); } + private IEnumerable GenerateGoogleDateTimeProperties(SourceFileContext ctx) + { + if (_schema.Type != "string" || _schema.Required == true || _schema.Properties is object || + _schema.AdditionalProperties is object || _schema.Repeated == true || + _schema.Enum__ is object || _schema.Ref__ is object) + { + throw new ArgumentException("Unable to handle complex google-datetime properties"); + } + + // For google-datetime properties, we generate: + // - Three properties: Xyz (object), XyzRaw (string), XyzDateTimeOffset (DateTimeOffset?) + // - Two fields, to back Xyz and XyzRaw + // + // Each property setter will set both fields. + + // The type of "value" is irrelevant to our uses of this, so it's okay that in the raw property it should be a string. + var valueParameter = Parameter(ctx.Type(), "value"); + var rawField = Field(Modifier.Private, ctx.Type(), $"_{Name}Raw"); + var objectField = Field(Modifier.Private, ctx.Type(), $"_{Name}"); + + var rawProperty = Property(Modifier.Public | Modifier.Virtual, ctx.Type(), PropertyName + "Raw") + .WithAttribute(ctx.Type())(Name) + .WithGetBody(Return(rawField)) + .WithSetBody( + objectField.Assign(ctx.Type(typeof(Utilities)).Call(nameof(Utilities.DeserializeForGoogleFormat))(valueParameter)), + rawField.Assign(valueParameter) + ); + if (_schema.Description is object) + { + rawProperty = rawProperty.WithXmlDoc(XmlDoc.Summary(_schema.Description)); + } + + var dtoProperty = Property(Modifier.Public | Modifier.Virtual, ctx.Type(), PropertyName + "DateTimeOffset") + .WithAttribute(ctx.Type())() + .WithXmlDoc(XmlDoc.Summary(XmlDoc.SeeAlso(ctx.Type()), " representation of ", rawProperty, ".")) + .WithGetBody(Return(ctx.Type(typeof(Utilities)).Call(nameof(Utilities.GetDateTimeOffsetFromString))(rawProperty))) + .WithSetBody(rawProperty.Assign(ctx.Type(typeof(Utilities)).Call(nameof(Utilities.GetStringFromDateTimeOffset))(valueParameter))); + + var objectProperty = Property(Modifier.Public | Modifier.Virtual, ctx.Type(), PropertyName) + .WithAttribute(ctx.Type())() + .WithAttribute(ctx.Type())($"This property is obsolete and may behave unexpectedly; please use {PropertyName}DateTimeOffset instead.") + .WithXmlDoc(XmlDoc.Summary(XmlDoc.SeeAlso(ctx.Type()), " representation of ", rawProperty, ".")) + .WithGetBody(Return(objectField)) + .WithSetBody( + rawField.Assign(ctx.Type(typeof(Utilities)).Call(nameof(Utilities.SerializeForGoogleFormat))(valueParameter)), + objectField.Assign(valueParameter) + ); + + yield return rawField; + yield return objectField; + + yield return rawProperty; + yield return objectProperty; + yield return dtoProperty; + } + public IEnumerable GenerateAnonymousModels(SourceFileContext ctx) { if (_schema.AdditionalProperties is object && DataModel.GetProperties(_schema.AdditionalProperties) is object)