diff --git a/Directory.Packages.props b/Directory.Packages.props index 326b381e..324714b2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,15 +15,15 @@ - - - - - - + + + + + + - + @@ -38,7 +38,7 @@ - + diff --git a/NuGet.config b/NuGet.config index 38ac8e75..25f97f34 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,9 +2,13 @@ + + + + diff --git a/global.json b/global.json index 52dae233..7c2687c0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.100-rc.1.24452.12", + "version": "9.0.100-rc.2.24463.1", "allowPrerelease": false, "rollForward": "latestMajor" }, diff --git a/src/LondonTravel.Skill/AlexaSkill.cs b/src/LondonTravel.Skill/AlexaSkill.cs index 43046726..40cbde3b 100644 --- a/src/LondonTravel.Skill/AlexaSkill.cs +++ b/src/LondonTravel.Skill/AlexaSkill.cs @@ -26,7 +26,7 @@ internal sealed class AlexaSkill( /// /// The to return from the skill. /// - public SkillResponse OnError(ISystemExceptionRequest error, Session session) + public SkillResponse OnError(SystemExceptionRequest error, Session session) { Log.SystemError( logger, @@ -67,7 +67,7 @@ public SkillResponse OnError(Exception? exception, Session session, string reque /// A representing the asynchronous operation /// which returns the to return from the skill. /// - public async Task OnIntentAsync(IIntentRequest intent, Session session) + public async Task OnIntentAsync(IntentRequest intent, Session session) { var handler = intentFactory.Create(intent.Intent); return await handler.RespondAsync(intent.Intent, session); diff --git a/src/LondonTravel.Skill/AppJsonSerializerContext.cs b/src/LondonTravel.Skill/AppJsonSerializerContext.cs index 04a1f94f..731becbc 100644 --- a/src/LondonTravel.Skill/AppJsonSerializerContext.cs +++ b/src/LondonTravel.Skill/AppJsonSerializerContext.cs @@ -19,5 +19,5 @@ namespace MartinCostello.LondonTravel.Skill; [JsonSerializable(typeof(SkillResponse))] [JsonSerializable(typeof(SkillUserPreferences))] [JsonSerializable(typeof(StandardCard))] -[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSourceGenerationOptions(AllowOutOfOrderMetadataProperties = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] public sealed partial class AppJsonSerializerContext : JsonSerializerContext; diff --git a/src/LondonTravel.Skill/FunctionHandler.cs b/src/LondonTravel.Skill/FunctionHandler.cs index 8eb73f42..98f0ee9f 100644 --- a/src/LondonTravel.Skill/FunctionHandler.cs +++ b/src/LondonTravel.Skill/FunctionHandler.cs @@ -49,12 +49,12 @@ private async Task HandleRequestAsync(SkillRequest request) { try { - return request.Request.Type switch + return request.Request switch { - RequestTypes.Intent => await skill.OnIntentAsync(request.Request, request.Session), - RequestTypes.Launch => skill.OnLaunch(request.Session), - RequestTypes.SessionEnded => skill.OnSessionEnded(request.Session), - RequestTypes.SystemException => skill.OnError(request.Request, request.Session), + IntentRequest intent => await skill.OnIntentAsync(intent, request.Session), + LaunchRequest => skill.OnLaunch(request.Session), + SessionEndedRequest => skill.OnSessionEnded(request.Session), + SystemExceptionRequest exception => skill.OnError(exception, request.Session), _ => skill.OnError(null, request.Session, request.Request.Type), }; } diff --git a/src/LondonTravel.Skill/Models/IRequest.cs b/src/LondonTravel.Skill/Models/IRequest.cs deleted file mode 100644 index c92a4bc5..00000000 --- a/src/LondonTravel.Skill/Models/IRequest.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Martin Costello, 2017. All rights reserved. -// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. - -namespace MartinCostello.LondonTravel.Skill.Models; - -internal interface IRequest -{ - string Type { get; } - - string RequestId { get; } - - string Locale { get; } - - DateTime Timestamp { get; } -} diff --git a/src/LondonTravel.Skill/Models/ISessionEndedRequest.cs b/src/LondonTravel.Skill/Models/ISessionEndedRequest.cs deleted file mode 100644 index 12b9809d..00000000 --- a/src/LondonTravel.Skill/Models/ISessionEndedRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Martin Costello, 2017. All rights reserved. -// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. - -namespace MartinCostello.LondonTravel.Skill.Models; - -internal interface ISessionEndedRequest : IRequest -{ - Reason Reason { get; } - - AlexaError Error { get; } -} diff --git a/src/LondonTravel.Skill/Models/ISystemExceptionRequest.cs b/src/LondonTravel.Skill/Models/ISystemExceptionRequest.cs deleted file mode 100644 index dfef918b..00000000 --- a/src/LondonTravel.Skill/Models/ISystemExceptionRequest.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) Martin Costello, 2017. All rights reserved. -// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. - -namespace MartinCostello.LondonTravel.Skill.Models; - -internal interface ISystemExceptionRequest : IRequest -{ - AlexaErrorCause ErrorCause { get; } - - AlexaError Error { get; } -} diff --git a/src/LondonTravel.Skill/Models/IntentRequest.cs b/src/LondonTravel.Skill/Models/IntentRequest.cs new file mode 100644 index 00000000..fa7fb912 --- /dev/null +++ b/src/LondonTravel.Skill/Models/IntentRequest.cs @@ -0,0 +1,18 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class IntentRequest : Request +{ + [JsonIgnore] + public override string Type => "IntentRequest"; + + [JsonPropertyName("dialogState")] + public string DialogState { get; set; } = default!; + + [JsonPropertyName("intent")] + public Intent Intent { get; set; } = default!; +} diff --git a/src/LondonTravel.Skill/Models/IIntentRequest.cs b/src/LondonTravel.Skill/Models/LaunchRequest.cs similarity index 60% rename from src/LondonTravel.Skill/Models/IIntentRequest.cs rename to src/LondonTravel.Skill/Models/LaunchRequest.cs index 7f646b69..4982ec2e 100644 --- a/src/LondonTravel.Skill/Models/IIntentRequest.cs +++ b/src/LondonTravel.Skill/Models/LaunchRequest.cs @@ -1,11 +1,12 @@ // Copyright (c) Martin Costello, 2017. All rights reserved. // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. +using System.Text.Json.Serialization; + namespace MartinCostello.LondonTravel.Skill.Models; -internal interface IIntentRequest : IRequest +public sealed class LaunchRequest : Request { - string DialogState { get; } - - Intent Intent { get; } + [JsonIgnore] + public override string Type => "LaunchRequest"; } diff --git a/src/LondonTravel.Skill/Models/Request.cs b/src/LondonTravel.Skill/Models/Request.cs index d2d408d5..2e93ff30 100644 --- a/src/LondonTravel.Skill/Models/Request.cs +++ b/src/LondonTravel.Skill/Models/Request.cs @@ -5,11 +5,15 @@ namespace MartinCostello.LondonTravel.Skill.Models; -public sealed class Request : IRequest, IIntentRequest, ISessionEndedRequest, ISystemExceptionRequest +[JsonDerivedType(typeof(IntentRequest), RequestTypes.Intent)] +[JsonDerivedType(typeof(LaunchRequest), RequestTypes.Launch)] +[JsonDerivedType(typeof(SessionEndedRequest), RequestTypes.SessionEnded)] +[JsonDerivedType(typeof(SystemExceptionRequest), RequestTypes.SystemException)] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +public abstract class Request { - [JsonPropertyName("type")] - [JsonRequired] - public string Type { get; set; } = default!; + [JsonIgnore] + public abstract string Type { get; } [JsonPropertyName("requestId")] public string RequestId { get; set; } = default!; @@ -20,29 +24,4 @@ public sealed class Request : IRequest, IIntentRequest, ISessionEndedRequest, IS [JsonConverter(typeof(MixedDateTimeConverter))] [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } - - //// Properties for "IntentRequest" - - [JsonPropertyName("dialogState")] - public string DialogState { get; set; } = default!; - - [JsonPropertyName("intent")] - public Intent Intent { get; set; } = default!; - - //// Properties for "SessionEndedRequest" - - [JsonConverter(typeof(JsonStringEnumConverter))] - [JsonPropertyName("reason")] - public Reason Reason { get; set; } - - //// Properties for "System.ExceptionEncountered" - - [JsonPropertyName("cause")] - public AlexaErrorCause ErrorCause { get; set; } = default!; - - //// Properties for "SessionEndedRequest" and "System.ExceptionEncountered" - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("error")] - public AlexaError Error { get; set; } = default!; } diff --git a/src/LondonTravel.Skill/Models/SessionEndedRequest.cs b/src/LondonTravel.Skill/Models/SessionEndedRequest.cs new file mode 100644 index 00000000..7737a994 --- /dev/null +++ b/src/LondonTravel.Skill/Models/SessionEndedRequest.cs @@ -0,0 +1,20 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class SessionEndedRequest : Request +{ + [JsonIgnore] + public override string Type => "SessionEndedRequest"; + + [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonPropertyName("reason")] + public Reason Reason { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public AlexaError Error { get; set; } = default!; +} diff --git a/src/LondonTravel.Skill/Models/SystemExceptionRequest.cs b/src/LondonTravel.Skill/Models/SystemExceptionRequest.cs new file mode 100644 index 00000000..32edf6a0 --- /dev/null +++ b/src/LondonTravel.Skill/Models/SystemExceptionRequest.cs @@ -0,0 +1,19 @@ +// Copyright (c) Martin Costello, 2017. All rights reserved. +// Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. + +using System.Text.Json.Serialization; + +namespace MartinCostello.LondonTravel.Skill.Models; + +public sealed class SystemExceptionRequest : Request +{ + [JsonIgnore] + public override string Type => "System.ExceptionEncountered"; + + [JsonPropertyName("cause")] + public AlexaErrorCause ErrorCause { get; set; } = default!; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public AlexaError Error { get; set; } = default!; +} diff --git a/test/LondonTravel.Skill.NativeAotTests/EndToEndTests.cs b/test/LondonTravel.Skill.NativeAotTests/EndToEndTests.cs index 00c00d31..cec1ade7 100644 --- a/test/LondonTravel.Skill.NativeAotTests/EndToEndTests.cs +++ b/test/LondonTravel.Skill.NativeAotTests/EndToEndTests.cs @@ -132,7 +132,7 @@ public async Task Alexa_Function_Can_Process_Intent_Request_For_Line_Status() public async Task Alexa_Function_Can_Process_Launch_Request() { // Arrange - var request = CreateRequest("LaunchRequest"); + var request = CreateRequest(); // Act var actual = await ProcessRequestAsync(request); @@ -153,7 +153,7 @@ public async Task Alexa_Function_Can_Process_Launch_Request() public async Task Alexa_Function_Can_Process_Session_Ended_Request() { // Arrange - var session = new Request() + var session = new SessionEndedRequest() { Reason = Reason.ExceededMaxReprompts, Error = new() @@ -163,7 +163,7 @@ public async Task Alexa_Function_Can_Process_Session_Ended_Request() }, }; - var request = CreateRequest("SessionEndedRequest", session); + var request = CreateRequest(session); // Act var actual = await ProcessRequestAsync(request); @@ -184,7 +184,7 @@ public async Task Alexa_Function_Can_Process_Session_Ended_Request() public async Task Alexa_Function_Can_Process_System_Exception_Request() { // Arrange - var exception = new Request() + var exception = new SystemExceptionRequest() { Error = new() { @@ -197,7 +197,7 @@ public async Task Alexa_Function_Can_Process_System_Exception_Request() }, }; - var request = CreateRequest("System.ExceptionEncountered", exception); + var request = CreateRequest(exception); // Act var actual = await ProcessRequestAsync(request); @@ -215,7 +215,7 @@ public async Task Alexa_Function_Can_Process_System_Exception_Request() private static SkillRequest CreateIntentRequest(string name, params Slot[] slots) { - var request = new Request() + var request = new IntentRequest() { Intent = new Intent() { @@ -233,10 +233,11 @@ private static SkillRequest CreateIntentRequest(string name, params Slot[] slots } } - return CreateRequest("IntentRequest", request); + return CreateRequest(request); } - private static SkillRequest CreateRequest(string type, Request? request = null) + private static SkillRequest CreateRequest(T? request = null) + where T : Request, new() { var application = new Application() { @@ -265,7 +266,7 @@ private static SkillRequest CreateRequest(string type, Request? request = null) User = user, }, }, - Request = request ?? new(), + Request = request ?? new T(), Session = new() { Application = application, @@ -276,7 +277,6 @@ private static SkillRequest CreateRequest(string type, Request? request = null) Version = "1.0", }; - result.Request.Type = type; result.Request.Locale = "en-GB"; return result; diff --git a/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs b/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs index ce872b9e..b9654f35 100644 --- a/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs +++ b/test/LondonTravel.Skill.Tests/AlexaFunctionTests.cs @@ -58,7 +58,7 @@ public async Task Cannot_Invoke_Function_With_System_Failure() var function = await CreateFunctionAsync(); var context = new TestLambdaContext(); - var error = new Request() + var error = new SystemExceptionRequest() { Error = new() { @@ -71,7 +71,7 @@ public async Task Cannot_Invoke_Function_With_System_Failure() }, }; - var request = CreateRequest("System.ExceptionEncountered", error); + var request = CreateRequest(error); // Act var actual = await function.HandlerAsync(request, context); diff --git a/test/LondonTravel.Skill.Tests/EndToEndTests.cs b/test/LondonTravel.Skill.Tests/EndToEndTests.cs index d05bd3ee..f218185b 100644 --- a/test/LondonTravel.Skill.Tests/EndToEndTests.cs +++ b/test/LondonTravel.Skill.Tests/EndToEndTests.cs @@ -126,7 +126,7 @@ public async Task Alexa_Function_Can_Process_Intent_Request_For_Line_Status( public async Task Alexa_Function_Can_Process_Launch_Request() { // Arrange - var request = CreateRequest("LaunchRequest"); + var request = CreateRequest(); // Act var actual = await ProcessRequestAsync(request); @@ -146,7 +146,7 @@ public async Task Alexa_Function_Can_Process_Launch_Request() public async Task Alexa_Function_Can_Process_Session_Ended_Request() { // Arrange - var session = new Request() + var session = new SessionEndedRequest() { Reason = Reason.ExceededMaxReprompts, Error = new() @@ -156,7 +156,7 @@ public async Task Alexa_Function_Can_Process_Session_Ended_Request() }, }; - var request = CreateRequest("SessionEndedRequest", session); + var request = CreateRequest(session); // Act var actual = await ProcessRequestAsync(request); @@ -176,7 +176,7 @@ public async Task Alexa_Function_Can_Process_Session_Ended_Request() public async Task Alexa_Function_Can_Process_System_Exception_Request() { // Arrange - var exception = new Request() + var exception = new SystemExceptionRequest() { Error = new() { @@ -189,7 +189,7 @@ public async Task Alexa_Function_Can_Process_System_Exception_Request() }, }; - var request = CreateRequest("System.ExceptionEncountered", exception); + var request = CreateRequest(exception); // Act var actual = await ProcessRequestAsync(request); diff --git a/test/LondonTravel.Skill.Tests/FunctionTests.cs b/test/LondonTravel.Skill.Tests/FunctionTests.cs index 5ba890f7..207f55cf 100644 --- a/test/LondonTravel.Skill.Tests/FunctionTests.cs +++ b/test/LondonTravel.Skill.Tests/FunctionTests.cs @@ -38,7 +38,7 @@ protected virtual async Task CreateFunctionAsync() protected virtual SkillRequest CreateIntentRequest(string name, params Slot[] slots) { - var request = new Request() + var request = new IntentRequest() { Intent = new Intent() { @@ -56,10 +56,11 @@ protected virtual SkillRequest CreateIntentRequest(string name, params Slot[] sl } } - return CreateRequest("IntentRequest", request); + return CreateRequest(request); } - protected virtual SkillRequest CreateRequest(string type, Request? request = null) + protected virtual SkillRequest CreateRequest(T? request = null) + where T : Request, new() { var application = new Application() { @@ -88,7 +89,7 @@ protected virtual SkillRequest CreateRequest(string type, Request? request = nul User = user, }, }, - Request = request ?? new(), + Request = request ?? new T(), Session = new() { Application = application, @@ -99,7 +100,6 @@ protected virtual SkillRequest CreateRequest(string type, Request? request = nul Version = "1.0", }; - result.Request.Type = type; result.Request.Locale = "en-GB"; return result; diff --git a/test/LondonTravel.Skill.Tests/LaunchTests.cs b/test/LondonTravel.Skill.Tests/LaunchTests.cs index 7ae7d87a..a8ed22bf 100644 --- a/test/LondonTravel.Skill.Tests/LaunchTests.cs +++ b/test/LondonTravel.Skill.Tests/LaunchTests.cs @@ -14,7 +14,7 @@ public async Task Can_Invoke_Function() // Arrange var function = await CreateFunctionAsync(); - var request = CreateRequest("LaunchRequest"); + var request = CreateRequest(); var context = new TestLambdaContext(); // Act diff --git a/test/LondonTravel.Skill.Tests/SerializationTests.cs b/test/LondonTravel.Skill.Tests/SerializationTests.cs index 2c638e5e..f1eaf8bf 100644 --- a/test/LondonTravel.Skill.Tests/SerializationTests.cs +++ b/test/LondonTravel.Skill.Tests/SerializationTests.cs @@ -9,11 +9,11 @@ namespace MartinCostello.LondonTravel.Skill; public static class SerializationTests { [Theory] - [InlineData("IntentRequest")] - [InlineData("LaunchRequest")] - [InlineData("LaunchRequestWithEpochTimestamp")] - [InlineData("SessionEndedRequest")] - public static void Can_Deserialize_Request(string name) + [InlineData("IntentRequest", typeof(IntentRequest))] + [InlineData("LaunchRequest", typeof(LaunchRequest))] + [InlineData("LaunchRequestWithEpochTimestamp", typeof(LaunchRequest))] + [InlineData("SessionEndedRequest", typeof(SessionEndedRequest))] + public static void Can_Deserialize_Request(string name, Type expectedType) { // Arrange JsonSerializer.IsReflectionEnabledByDefault.ShouldBeFalse(); @@ -27,6 +27,7 @@ public static void Can_Deserialize_Request(string name) // Assert actual.ShouldNotBeNull(); actual.Request.ShouldNotBeNull(); + actual.Request.ShouldBeOfType(expectedType); } [Fact] diff --git a/test/LondonTravel.Skill.Tests/SessionEndedTests.cs b/test/LondonTravel.Skill.Tests/SessionEndedTests.cs index f20e9a28..830cc4a1 100644 --- a/test/LondonTravel.Skill.Tests/SessionEndedTests.cs +++ b/test/LondonTravel.Skill.Tests/SessionEndedTests.cs @@ -14,7 +14,7 @@ public async Task Can_Invoke_Function() // Arrange var function = await CreateFunctionAsync(); - var request = CreateRequest("SessionEndedRequest"); + var request = CreateRequest(); var context = new TestLambdaContext(); // Act diff --git a/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs b/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs index f26ff149..6b478650 100644 --- a/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs +++ b/test/LondonTravel.Skill.Tests/UnknownIntentTests.cs @@ -30,4 +30,9 @@ public async Task Can_Invoke_Function() response.OutputSpeech.Type.ShouldBe("SSML"); response.OutputSpeech.Ssml.ShouldBe("Sorry, I don't understand how to do that."); } + + private sealed class UnknownRequest : Request + { + public override string Type => "Unknown"; + } } diff --git a/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs b/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs index 8de4420a..93501056 100644 --- a/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs +++ b/test/LondonTravel.Skill.Tests/UnknownRequestTests.cs @@ -15,7 +15,7 @@ public async Task Can_Invoke_Function() var function = await CreateFunctionAsync(); var context = new TestLambdaContext(); - var request = CreateRequest("Unknown"); + var request = CreateRequest(); // Act var actual = await function.HandlerAsync(request, context); @@ -23,4 +23,9 @@ public async Task Can_Invoke_Function() // Assert AssertResponse(actual); } + + private sealed class UnknownRequest : Request + { + public override string Type => "Unknown"; + } }