From d76549a6849d4c9a8571b140745e3c099d45baf5 Mon Sep 17 00:00:00 2001 From: Artur Drobinskiy Date: Fri, 16 Sep 2022 22:22:00 +0700 Subject: [PATCH] Fixes #1480. Convert DateOnly properties to local timezone (#1481) * Convert DateOnly properties to local timezone for TypeScriptDateTimeType.Date * adjust parseDateOnly * fix ArrayItemDate * fix DateOnly code --- .../DateCodeGenerationTests.cs | 69 ++++++++++++++----- .../DateTimeCodeGenerationTests.cs | 45 ++++++++---- .../DataConversionGenerator.cs | 4 ++ ...sonSchema.CodeGeneration.TypeScript.csproj | 1 + .../Templates/ConvertToClass.liquid | 14 ++-- .../Templates/File.ParseDateOnly.liquid | 5 ++ .../TypeScriptGenerator.cs | 7 +- .../TypeScriptGeneratorSettings.cs | 7 ++ 8 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 src/NJsonSchema.CodeGeneration.TypeScript/Templates/File.ParseDateOnly.liquid diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateCodeGenerationTests.cs b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateCodeGenerationTests.cs index 248a6e666..d62f359c9 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateCodeGenerationTests.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateCodeGenerationTests.cs @@ -15,9 +15,11 @@ public class DateCodeGenerationTests 'myTimeSpan': { 'type': 'string', 'format': 'time-span' } } }"; - - [Fact] - public async Task When_date_handling_is_string_then_string_property_is_generated_in_class() + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_string_then_string_property_is_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = await JsonSchema.FromJsonAsync(Json); @@ -26,7 +28,8 @@ public async Task When_date_handling_is_string_then_string_property_is_generated var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.String + DateTimeType = TypeScriptDateTimeType.String, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -36,8 +39,10 @@ public async Task When_date_handling_is_string_then_string_property_is_generated Assert.Contains("data[\"myDate\"] = this.myDate;", code); } - [Fact] - public async Task When_date_handling_is_moment_then_moment_property_is_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_moment_then_moment_property_is_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = await JsonSchema.FromJsonAsync(Json); @@ -46,7 +51,8 @@ public async Task When_date_handling_is_moment_then_moment_property_is_generated var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.MomentJS + DateTimeType = TypeScriptDateTimeType.MomentJS, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -76,8 +82,10 @@ public async Task When_date_handling_is_moment_then_duration_property_is_generat Assert.Contains("data[\"myTimeSpan\"] = this.myTimeSpan ? this.myTimeSpan.format('d.hh:mm:ss.SS', { trim: false }) : undefined;", code); } - [Fact] - public async Task When_date_handling_is_luxon_then_datetime_property_is_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_luxon_then_datetime_property_is_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = await JsonSchema.FromJsonAsync(Json); @@ -86,7 +94,8 @@ public async Task When_date_handling_is_luxon_then_datetime_property_is_generate var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.Luxon + DateTimeType = TypeScriptDateTimeType.Luxon, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -116,8 +125,10 @@ public async Task When_date_handling_is_luxon_then_duration_property_is_generate Assert.Contains("data[\"myTimeSpan\"] = this.myTimeSpan ? this.myTimeSpan.toString() : undefined;", code); } - [Fact] - public async Task When_date_handling_is_dayjs_then_dayjs_property_is_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_dayjs_then_dayjs_property_is_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = await JsonSchema.FromJsonAsync(Json); @@ -126,7 +137,8 @@ public async Task When_date_handling_is_dayjs_then_dayjs_property_is_generated_i var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.DayJS + DateTimeType = TypeScriptDateTimeType.DayJS, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -156,9 +168,33 @@ public async Task When_date_handling_is_date_then_date_property_is_generated_in_ Assert.Contains("data[\"myDate\"] = this.myDate ? formatDate(this.myDate) : undefined;", code); Assert.Contains("function formatDate(", code); } - + [Fact] - public async Task When_date_handling_is_offset_moment_then_date_property_is_generated_in_class() + public async Task When_date_handling_is_date_then_date_property_is_generated_in_class_with_local_timezone_conversion() + { + //// Arrange + var schema = await JsonSchema.FromJsonAsync(Json); + + //// Act + var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings + { + TypeStyle = TypeScriptTypeStyle.Class, + //DateTimeType = TypeScriptDateTimeType.Date, + ConvertDateToLocalTimezone = true + }); + var code = generator.GenerateFile("MyClass"); + + //// Assert + Assert.Contains("myDate: Date", code); + Assert.Contains("this.myDate = _data[\"myDate\"] ? parseDateOnly(_data[\"myDate\"].toString()) : undefined;", code); + Assert.Contains("data[\"myDate\"] = this.myDate ? formatDate(this.myDate) : undefined;", code); + Assert.Contains("function formatDate(", code); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_offset_moment_then_date_property_is_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = await JsonSchema.FromJsonAsync(Json); @@ -167,7 +203,8 @@ public async Task When_date_handling_is_offset_moment_then_date_property_is_gene var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.OffsetMomentJS + DateTimeType = TypeScriptDateTimeType.OffsetMomentJS, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); diff --git a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateTimeCodeGenerationTests.cs b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateTimeCodeGenerationTests.cs index 5d3e0c116..d21374db2 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateTimeCodeGenerationTests.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript.Tests/DateTimeCodeGenerationTests.cs @@ -12,8 +12,10 @@ public class ClassWithDateTimeProperty public DateTime MyDateTime { get; set; } } - [Fact] - public async Task When_date_handling_is_string_then_string_property_are_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_string_then_string_property_are_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = JsonSchema.FromType(); @@ -22,7 +24,8 @@ public async Task When_date_handling_is_string_then_string_property_are_generate var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.String + DateTimeType = TypeScriptDateTimeType.String, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -32,8 +35,10 @@ public async Task When_date_handling_is_string_then_string_property_are_generate Assert.Contains("data[\"MyDateTime\"] = this.myDateTime;", code); } - [Fact] - public async Task When_date_handling_is_moment_then_moment_property_are_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_moment_then_moment_property_are_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = JsonSchema.FromType(); @@ -42,7 +47,8 @@ public async Task When_date_handling_is_moment_then_moment_property_are_generate var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.MomentJS + DateTimeType = TypeScriptDateTimeType.MomentJS, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -52,8 +58,10 @@ public async Task When_date_handling_is_moment_then_moment_property_are_generate Assert.Contains("data[\"MyDateTime\"] = this.myDateTime ? this.myDateTime.toISOString() : undefined;", code); } - [Fact] - public async Task When_date_handling_is_offset_moment_then_moment_property_are_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_offset_moment_then_moment_property_are_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = JsonSchema.FromType(); @@ -62,7 +70,8 @@ public async Task When_date_handling_is_offset_moment_then_moment_property_are_g var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.OffsetMomentJS + DateTimeType = TypeScriptDateTimeType.OffsetMomentJS, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -72,8 +81,10 @@ public async Task When_date_handling_is_offset_moment_then_moment_property_are_g Assert.Contains("data[\"MyDateTime\"] = this.myDateTime ? this.myDateTime.toISOString(true) : undefined;", code); } - [Fact] - public async Task When_date_handling_is_dayjs_then_dayjs_property_are_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_dayjs_then_dayjs_property_are_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = JsonSchema.FromType(); @@ -82,7 +93,8 @@ public async Task When_date_handling_is_dayjs_then_dayjs_property_are_generated_ var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - DateTimeType = TypeScriptDateTimeType.DayJS + DateTimeType = TypeScriptDateTimeType.DayJS, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); @@ -92,8 +104,10 @@ public async Task When_date_handling_is_dayjs_then_dayjs_property_are_generated_ Assert.Contains("data[\"MyDateTime\"] = this.myDateTime ? this.myDateTime.toISOString() : undefined;", code); } - [Fact] - public async Task When_date_handling_is_date_then_date_property_are_generated_in_class() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task When_date_handling_is_date_then_date_property_are_generated_in_class(bool convertDateToLocalTimezone) { //// Arrange var schema = JsonSchema.FromType(); @@ -102,7 +116,8 @@ public async Task When_date_handling_is_date_then_date_property_are_generated_in var generator = new TypeScriptGenerator(schema, new TypeScriptGeneratorSettings { TypeStyle = TypeScriptTypeStyle.Class, - //DateTimeType = TypeScriptDateTimeType.Date + //DateTimeType = TypeScriptDateTimeType.Date, + ConvertDateToLocalTimezone = convertDateToLocalTimezone }); var code = generator.GenerateFile("MyClass"); diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/DataConversionGenerator.cs b/src/NJsonSchema.CodeGeneration.TypeScript/DataConversionGenerator.cs index 1177537ce..5d15d0ec1 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/DataConversionGenerator.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/DataConversionGenerator.cs @@ -87,6 +87,10 @@ private static object CreateModel(DataConversionParameters parameters) //StringToDateCode is used for date and date-time formats UseJsDate = parameters.Settings.DateTimeType == TypeScriptDateTimeType.Date, StringToDateCode = GetStringToDateTime(parameters, typeSchema), + StringToDateOnlyCode = parameters.Settings.DateTimeType == TypeScriptDateTimeType.Date + && parameters.Settings.ConvertDateToLocalTimezone + ? "parseDateOnly" + : GetStringToDateTime(parameters, typeSchema), DateToStringCode = GetDateToString(parameters, typeSchema), DateTimeToStringCode = GetDateTimeToString(parameters, typeSchema), diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj b/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj index 732e7b624..5fbb9599f 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj +++ b/src/NJsonSchema.CodeGeneration.TypeScript/NJsonSchema.CodeGeneration.TypeScript.csproj @@ -16,6 +16,7 @@ + diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/Templates/ConvertToClass.liquid b/src/NJsonSchema.CodeGeneration.TypeScript/Templates/ConvertToClass.liquid index 2f6241140..21ad6e1f7 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/Templates/ConvertToClass.liquid +++ b/src/NJsonSchema.CodeGeneration.TypeScript/Templates/ConvertToClass.liquid @@ -11,7 +11,9 @@ if (Array.isArray({{ Value }})) { {% if IsArrayItemNewableObject -%} {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push({{ ArrayItemType }}.fromJS(item{% if HandleReferences %}, _mappings{% endif %})); {% else -%} -{% if IsArrayItemDate or IsArrayItemDateTime -%} +{% if IsArrayItemDate -%} + {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push({{ StringToDateOnlyCode }}(item)); +{% elsif IsArrayItemDateTime -%} {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push({{ StringToDateCode }}(item)); {% else -%} {{ Variable }}{% if RequiresStrictPropertyInitialization %}!{% endif %}.push(item); @@ -31,7 +33,9 @@ if ({{ Value }}) { ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ DictionaryValueType }}.fromJS({{ Value }}[key]{% if HandleReferences %}, _mappings{% endif %}) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; {% elsif IsDictionaryValueNewableArray -%} ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ Value }}[key].map((i: any) => {{ DictionaryValueArrayItemType }}.fromJS(i{% if HandleReferences %}, _mappings{% endif %})) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; -{% elsif IsDictionaryValueDate or IsDictionaryValueDateTime -%} +{% elsif IsDictionaryValueDate -%} + ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ StringToDateOnlyCode }}({{ Value }}[key].toString()) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; +{% elsif IsDictionaryValueDateTime -%} ({{ Variable }}){% if RequiresStrictPropertyInitialization %}!{% endif %}[key] = {{ Value }}[key] ? {{ StringToDateCode }}({{ Value }}[key].toString()) : {% if HasDictionaryValueDefaultValue %}{{ DictionaryValueDefaultValue }}{% else %}{{ NullValue }}{% endif %}; {% else -%} {% if HasDictionaryValueDefaultValue or NullValue != "undefined" -%} @@ -47,7 +51,9 @@ if ({{ Value }}) { } {% endif -%} {% else -%} - {% if IsDate or IsDateTime -%} + {% if IsDate -%} +{{ Variable }} = {{ Value }} ? {{ StringToDateOnlyCode }}({{ Value }}.toString()) : {% if HasDefaultValue %}{{ StringToDateOnlyCode }}({{ DefaultValue }}){% else %}{{ NullValue }}{% endif %}; + {% elsif IsDateTime -%} {{ Variable }} = {{ Value }} ? {{ StringToDateCode }}({{ Value }}.toString()) : {% if HasDefaultValue %}{{ StringToDateCode }}({{ DefaultValue }}){% else %}{{ NullValue }}{% endif %}; {% else -%} {% if HasDefaultValue or NullValue != "undefined" -%} @@ -56,4 +62,4 @@ if ({{ Value }}) { {{ Variable }} = {{ Value }}; {% endif -%} {% endif -%} -{% endif -%} \ No newline at end of file +{% endif -%} diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/Templates/File.ParseDateOnly.liquid b/src/NJsonSchema.CodeGeneration.TypeScript/Templates/File.ParseDateOnly.liquid new file mode 100644 index 000000000..bae76f1dc --- /dev/null +++ b/src/NJsonSchema.CodeGeneration.TypeScript/Templates/File.ParseDateOnly.liquid @@ -0,0 +1,5 @@ +function parseDateOnly(s: string) { + const date = new Date(s); + return new Date(date.getTime() + + date.getTimezoneOffset() * 60000); +} \ No newline at end of file diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGenerator.cs b/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGenerator.cs index 1cfb38e67..3a080f141 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGenerator.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGenerator.cs @@ -96,7 +96,12 @@ public IEnumerable GenerateTypes(TypeScriptExtensionCode extension var template = Settings.TemplateFactory.CreateTemplate("TypeScript", "File.FormatDate", new object()); yield return new CodeArtifact("formatDate", CodeArtifactType.Function, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Utility, template); } - + if (artifacts.Any(r => r.Code.Contains("parseDateOnly("))) + { + var template = Settings.TemplateFactory.CreateTemplate("TypeScript", "File.ParseDateOnly", new object()); + yield return new CodeArtifact("parseDateOnly", CodeArtifactType.Function, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Utility, template); + } + if (Settings.HandleReferences) { var template = Settings.TemplateFactory.CreateTemplate("TypeScript", "File.ReferenceHandling", new object()); diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGeneratorSettings.cs b/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGeneratorSettings.cs index f92bbfa14..c627f1788 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGeneratorSettings.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/TypeScriptGeneratorSettings.cs @@ -67,6 +67,13 @@ public TypeScriptGeneratorSettings() /// Gets or sets the date time type (default: 'Date'). public TypeScriptDateTimeType DateTimeType { get; set; } + /// + /// Whether to use UTC (default) or local time zone when deserializing dates 'yyyy-MM-dd' (default: 'false'). + /// Only applicable if is . + /// Other DateTimeTypes use local timezone by default. + /// + public bool ConvertDateToLocalTimezone { get; set; } + /// Gets or sets the enum style (default: Enum). public TypeScriptEnumStyle EnumStyle { get; set; }