From bfc848a705278470de5489701f016733cf686b88 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Wed, 24 Sep 2025 13:25:40 +0200 Subject: [PATCH 01/15] Pass the description along with the response code --- src/Kiota.Builder/CodeDOM/CodeMethod.cs | 29 +++++++++++++++++++++++++ src/Kiota.Builder/KiotaBuilder.cs | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 66b36b8fba..37f7ba24a7 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -254,6 +254,7 @@ public void DeduplicateErrorMappings() public bool HasUrlTemplateOverride => !string.IsNullOrEmpty(UrlTemplateOverride); private ConcurrentDictionary errorMappings = new(StringComparer.OrdinalIgnoreCase); + private ConcurrentDictionary errorDescriptions = new(StringComparer.OrdinalIgnoreCase); /// /// Mapping of the error code and response types for this method. @@ -265,6 +266,17 @@ public IOrderedEnumerable> ErrorMappings return errorMappings.OrderBy(static x => x.Key); } } + + /// + /// Mapping of the error code and response descriptions from OpenAPI spec for this method. + /// + public IOrderedEnumerable> ErrorDescriptions + { + get + { + return errorDescriptions.OrderBy(static x => x.Key); + } + } public bool HasErrorMappingCode(string code) { ArgumentException.ThrowIfNullOrEmpty(code); @@ -304,6 +316,7 @@ public object Clone() Parent = Parent, OriginalIndexer = OriginalIndexer, errorMappings = new(errorMappings), + errorDescriptions = new(errorDescriptions), AcceptedResponseTypes = new List(AcceptedResponseTypes), PagingInformation = PagingInformation?.Clone() as PagingInformation, Documentation = (CodeDocumentation)Documentation.Clone(), @@ -330,4 +343,20 @@ public void AddErrorMapping(string errorCode, CodeTypeBase type) ArgumentException.ThrowIfNullOrEmpty(errorCode); errorMappings.TryAdd(errorCode, type); } + + public void AddErrorMapping(string errorCode, CodeTypeBase type, string description) + { + ArgumentNullException.ThrowIfNull(type); + ArgumentException.ThrowIfNullOrEmpty(errorCode); + errorMappings.TryAdd(errorCode, type); + if (!string.IsNullOrEmpty(description)) + errorDescriptions.TryAdd(errorCode, description); + } + + public string? GetErrorDescription(string errorCode) + { + ArgumentException.ThrowIfNullOrEmpty(errorCode); + errorDescriptions.TryGetValue(errorCode, out var description); + return description; + } } diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 2dc50ad5f7..2ee4440220 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1292,7 +1292,7 @@ private void AddErrorMappingToExecutorMethod(OpenApiUrlTreeNode currentNode, Ope { if (!codeClass.IsErrorDefinition) codeClass.IsErrorDefinition = true; - executorMethod.AddErrorMapping(errorCode, errorType); + executorMethod.AddErrorMapping(errorCode, errorType, response.Description ?? string.Empty); } else logger.LogWarning("Could not create error type for {Error} in {Operation}", errorCode, operation.OperationId); From 1c3d3e89dcc0c68c73a9d8f4757756227f0a63ed Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Wed, 24 Sep 2025 13:27:23 +0200 Subject: [PATCH 02/15] Give an error class a constructor that takes a message to be passed to base, following C# conventions --- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 101 ++++++++++++++++++ .../Refiners/CSharpLanguageRefinerTests.cs | 79 ++++++++++++++ 2 files changed, 180 insertions(+) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 234b73d4a9..8b4dec09cf 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -107,6 +107,8 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken AbstractionsNamespaceName ); AddConstructorsForDefaultValues(generatedCode, false); + AddMessageConstructorForErrorClasses(generatedCode); + AddMessageFactoryMethodForErrorClasses(generatedCode); AddDiscriminatorMappingsUsingsToParentClasses( generatedCode, "IParseNode" @@ -268,4 +270,103 @@ private void SetTypeAccessModifiers(CodeElement currentElement) CrawlTree(currentElement, SetTypeAccessModifiers); } + + private static void AddMessageConstructorForErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && + codeClass.IsErrorDefinition && + !codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase)))) + { + codeClass.AddMethod(new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + IsAsync = false, + IsStatic = false, + Documentation = new(new() { + {"TypeName", new CodeType { + IsExternal = false, + TypeDefinition = codeClass, + }} + }) + { + DescriptionTemplate = "Instantiates a new {TypeName} with the specified error message.", + }, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = codeClass, + }); + + // Add message parameter + codeClass.Methods.Last().AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "string", IsExternal = true }, + Optional = false, + Documentation = new() + { + DescriptionTemplate = "The error message" + } + }); + } + CrawlTree(currentElement, AddMessageConstructorForErrorClasses); + } + + private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && + codeClass.IsErrorDefinition) + { + codeClass.AddMethod(new CodeMethod + { + Name = "CreateFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.Factory, + IsAsync = false, + IsStatic = true, + Documentation = new(new() { + {"TypeName", new CodeType { + IsExternal = false, + TypeDefinition = codeClass, + }} + }) + { + DescriptionTemplate = "Creates a new instance of the appropriate class based on discriminator value with a custom error message.", + }, + Access = AccessModifier.Public, + ReturnType = new CodeType + { + Name = codeClass.Name, + TypeDefinition = codeClass, + }, + Parent = codeClass, + }); + + var method = codeClass.Methods.Last(); + + // Add parseNode parameter + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Type = new CodeType { Name = "IParseNode", IsExternal = true }, + Optional = false, + Documentation = new() + { + DescriptionTemplate = "The parse node to use to read the discriminator value and create the object" + } + }); + + // Add message parameter + method.AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "string", IsExternal = true }, + Optional = false, + Documentation = new() + { + DescriptionTemplate = "The error message to set on the created object" + } + }); + } + CrawlTree(currentElement, AddMessageFactoryMethodForErrorClasses); + } } diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index a7e497ded7..5116156f65 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -961,5 +961,84 @@ public async Task SetTypeAccessModifierAsync(AccessModifier accessModifier) Assert.Equal(codeClass.Access, accessModifier); Assert.Equal(codeEnum.Access, accessModifier); } + + [Fact] + public async Task AddsMessageConstructorToErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + + // Then + var messageConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.NotNull(messageConstructor); + Assert.Single(messageConstructor.Parameters); + Assert.Equal("message", messageConstructor.Parameters.First().Name); + Assert.Equal("string", messageConstructor.Parameters.First().Type.Name); + Assert.False(messageConstructor.Parameters.First().Optional); + } + + [Fact] + public async Task DoesNotAddMessageConstructorToNonErrorClasses() + { + // Given + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularModel", + IsErrorDefinition = false + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + + // Then + var messageConstructor = regularClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.Null(messageConstructor); + } + + [Fact] + public async Task AddsMessageFactoryMethodToErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + + // Then + var messageFactoryMethod = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Factory) && + m.Name.Equals("CreateFromDiscriminatorValueWithMessage", StringComparison.OrdinalIgnoreCase)); + + Assert.NotNull(messageFactoryMethod); + Assert.Equal(2, messageFactoryMethod.Parameters.Count()); + + var parseNodeParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("parseNode", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(parseNodeParam); + Assert.Equal("IParseNode", parseNodeParam.Type.Name); + + var messageParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(messageParam); + Assert.Equal("string", messageParam.Type.Name); + + Assert.True(messageFactoryMethod.IsStatic); + Assert.Equal(AccessModifier.Public, messageFactoryMethod.Access); + } #endregion } From b4c5f470edc4453c12cc8fa81c6302262e44b89d Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Wed, 24 Sep 2025 13:28:28 +0200 Subject: [PATCH 03/15] Have the error mapping include the descriptions Hopefully human-readable and helpful --- .../Writers/CSharp/CodeMethodWriter.cs | 32 ++++++++++++++++++- .../Writers/CSharp/CodeMethodWriterTests.cs | 28 ++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 7347cc3727..391505191d 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -203,6 +203,15 @@ private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClas WriteFactoryMethodBodyForUnionModel(codeElement, parentClass, parseNodeParameter, writer); else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) WriteFactoryMethodBodyForIntersectionModel(codeElement, parentClass, parseNodeParameter, writer); + else if (codeElement.Name == "CreateFromDiscriminatorValueWithMessage" && parentClass.IsErrorDefinition) + { + // Special case: CreateFromDiscriminatorValueWithMessage for error classes + var messageParam = codeElement.Parameters.FirstOrDefault(p => p.Name == "message"); + if (messageParam != null) + writer.WriteLine($"return new {parentClass.GetFullName()}({messageParam.Name});"); + else + writer.WriteLine($"return new {parentClass.GetFullName()}();"); + } else writer.WriteLine($"return new {parentClass.GetFullName()}();"); } @@ -401,7 +410,21 @@ protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams re writer.StartBlock(); foreach (var errorMapping in codeElement.ErrorMappings.Where(errorMapping => errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass)) { - writer.WriteLine($"{{ \"{errorMapping.Key.ToUpperInvariant()}\", {conventions.GetTypeString(errorMapping.Value, codeElement, false)}.CreateFromDiscriminatorValue }},"); + var errorClass = errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition as CodeClass; + var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); + + if (errorClass?.IsErrorDefinition == true) + { + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + var statusCodeAndDescription = !string.IsNullOrEmpty(errorDescription) + ? $"{errorMapping.Key} {errorDescription}" + : errorMapping.Key; + writer.WriteLine($"{{ \"{errorMapping.Key.ToUpperInvariant()}\", (parseNode) => {typeName}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{statusCodeAndDescription}\") }},"); + } + else + { + writer.WriteLine($"{{ \"{errorMapping.Key.ToUpperInvariant()}\", {typeName}.CreateFromDiscriminatorValue }},"); + } } writer.CloseBlock("};"); } @@ -606,6 +629,13 @@ private static string GetBaseSuffix(bool isConstructor, bool inherits, CodeClass return $" : base({urlTemplateProperty.DefaultValue}{thirdParameterName})"; } } + // For error classes with message constructor, pass the message to base constructor + else if (parentClass.IsErrorDefinition && + currentMethod.Parameters.Any(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase) && + p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase))) + { + return " : base(message)"; + } return " : base()"; } diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs index 2dfc2d6f04..e513796041 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs @@ -2100,4 +2100,32 @@ public void WritesRequestGeneratorContentTypeQuotes() var result = tw.ToString(); Assert.Contains("\"application/json; profile=\\\"CamelCase\\\"\"", result); } + + [Fact] + public void WritesRequestExecutorWithEnhancedErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + IsErrorDefinition = true + }).First(); + var error401 = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client Error"); + method.AddErrorMapping("401", new CodeType { Name = "Error401", TypeDefinition = error401 }, "Unauthorized"); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo", result); + Assert.Contains("var errorMapping = new Dictionary>", result); + Assert.Contains("{ \"4XX\", (parseNode) => Error4XX.CreateFromDiscriminatorValueWithMessage(parseNode, \"4XX Client Error\") }", result); + Assert.Contains("{ \"401\", (parseNode) => Error401.CreateFromDiscriminatorValueWithMessage(parseNode, \"401 Unauthorized\") }", result); + Assert.Contains("send", result.ToLower()); + } } From 9bd10a8aa22d8f54caffae9a12dac4f354da9220 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Wed, 24 Sep 2025 14:15:35 +0200 Subject: [PATCH 04/15] cleanup / deduplicate --- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 66 ++++++++----------- .../Refiners/CommonLanguageRefiner.cs | 47 +++++++------ 2 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 8b4dec09cf..94cb3e7866 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -107,7 +107,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken AbstractionsNamespaceName ); AddConstructorsForDefaultValues(generatedCode, false); - AddMessageConstructorForErrorClasses(generatedCode); + AddConstructorsForErrorClasses(generatedCode); AddMessageFactoryMethodForErrorClasses(generatedCode); AddDiscriminatorMappingsUsingsToParentClasses( generatedCode, @@ -271,53 +271,44 @@ private void SetTypeAccessModifiers(CodeElement currentElement) CrawlTree(currentElement, SetTypeAccessModifiers); } - private static void AddMessageConstructorForErrorClasses(CodeElement currentElement) + private static void AddConstructorsForErrorClasses(CodeElement currentElement) { - if (currentElement is CodeClass codeClass && - codeClass.IsErrorDefinition && - !codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase)))) + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) { - codeClass.AddMethod(new CodeMethod + // Add parameterless constructor if not already present + if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) { - Name = "constructor", - Kind = CodeMethodKind.Constructor, - IsAsync = false, - IsStatic = false, - Documentation = new(new() { - {"TypeName", new CodeType { - IsExternal = false, - TypeDefinition = codeClass, - }} - }) - { - DescriptionTemplate = "Instantiates a new {TypeName} with the specified error message.", - }, - Access = AccessModifier.Public, - ReturnType = new CodeType { Name = "void", IsExternal = true }, - Parent = codeClass, - }); + codeClass.AddMethod(CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values.")); + } - // Add message parameter - codeClass.Methods.Last().AddParameter(new CodeParameter + // Add message constructor if not already present + if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase)))) { - Name = "message", - Type = new CodeType { Name = "string", IsExternal = true }, - Optional = false, - Documentation = new() + var messageConstructor = codeClass.AddMethod(CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message.")).Single(); + + // Add message parameter + messageConstructor.AddParameter(new CodeParameter { - DescriptionTemplate = "The error message" - } - }); + Name = "message", + Type = new CodeType { Name = "string", IsExternal = true }, + Optional = false, + Documentation = new() + { + DescriptionTemplate = "The error message" + } + }); + } } - CrawlTree(currentElement, AddMessageConstructorForErrorClasses); + CrawlTree(currentElement, AddConstructorsForErrorClasses); } private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentElement) { if (currentElement is CodeClass codeClass && - codeClass.IsErrorDefinition) + codeClass.IsErrorDefinition && + !codeClass.Methods.Any(m => m.Name == "CreateFromDiscriminatorValueWithMessage")) { - codeClass.AddMethod(new CodeMethod + var method = codeClass.AddMethod(new CodeMethod { Name = "CreateFromDiscriminatorValueWithMessage", Kind = CodeMethodKind.Factory, @@ -339,15 +330,14 @@ private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentEl TypeDefinition = codeClass, }, Parent = codeClass, - }); - - var method = codeClass.Methods.Last(); + }).Single(); // Add parseNode parameter method.AddParameter(new CodeParameter { Name = "parseNode", Type = new CodeType { Name = "IParseNode", IsExternal = true }, + Kind = CodeParameterKind.ParseNode, Optional = false, Documentation = new() { diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index d5ae1ff21c..122be809d2 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -243,28 +243,37 @@ protected static void AddConstructorsForDefaultValues(CodeElement current, bool currentClass.Properties.Any(static x => !string.IsNullOrEmpty(x.DefaultValue)) || addIfInherited && DoesAnyParentHaveAPropertyWithDefaultValue(currentClass)) && !currentClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.ClientConstructor))) - currentClass.AddMethod(new CodeMethod - { - Name = "constructor", - Kind = CodeMethodKind.Constructor, - ReturnType = new CodeType - { - Name = "void" - }, - IsAsync = false, - Documentation = new(new() { - { "TypeName", new CodeType() { - IsExternal = false, - TypeDefinition = current, - }} - }) - { - DescriptionTemplate = "Instantiates a new {TypeName} and sets the default values.", - }, - }); + currentClass.AddMethod(CreateConstructor(currentClass, "Instantiates a new {TypeName} and sets the default values.")); CrawlTree(current, x => AddConstructorsForDefaultValues(x, addIfInherited, forceAdd, classKindsToExclude)); } + protected static CodeMethod CreateConstructor(CodeClass parentClass, string descriptionTemplate, AccessModifier access = AccessModifier.Public) + { + return new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType + { + Name = "void", + IsExternal = true + }, + IsAsync = false, + IsStatic = false, + Access = access, + Documentation = new(new() { + { "TypeName", new CodeType() { + IsExternal = false, + TypeDefinition = parentClass, + }} + }) + { + DescriptionTemplate = descriptionTemplate, + }, + Parent = parentClass, + }; + } + protected static void ReplaceReservedModelTypes(CodeElement current, IReservedNamesProvider provider, Func replacement) => ReplaceReservedNames(current, provider, From 3d409349bd360e4e59e61917c05b89aa5981a769 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Thu, 25 Sep 2025 09:42:58 +0200 Subject: [PATCH 05/15] Don't add without parameters and add parameters later, as it will overwrite any previous constructor. --- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 94cb3e7866..96bb8592ae 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -278,13 +278,14 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) // Add parameterless constructor if not already present if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) { - codeClass.AddMethod(CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values.")); + var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); + codeClass.AddMethod(parameterlessConstructor); } // Add message constructor if not already present if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase)))) { - var messageConstructor = codeClass.AddMethod(CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message.")).Single(); + var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); // Add message parameter messageConstructor.AddParameter(new CodeParameter @@ -297,6 +298,8 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) DescriptionTemplate = "The error message" } }); + + codeClass.AddMethod(messageConstructor); } } CrawlTree(currentElement, AddConstructorsForErrorClasses); From 21311006fc644f7cc0ed89d914607b1375e6a300 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Thu, 25 Sep 2025 10:24:43 +0200 Subject: [PATCH 06/15] THough all error definitions inherit from ApiException, the inherits flag is false --- .../Writers/CSharp/CodeMethodWriter.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 391505191d..4b536118c6 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -6,6 +6,7 @@ using Kiota.Builder.OrderComparers; namespace Kiota.Builder.Writers.CSharp; + public class CodeMethodWriter : BaseElementWriter { public CodeMethodWriter(CSharpConventionService conventionService) : base(conventionService) @@ -629,18 +630,19 @@ private static string GetBaseSuffix(bool isConstructor, bool inherits, CodeClass return $" : base({urlTemplateProperty.DefaultValue}{thirdParameterName})"; } } - // For error classes with message constructor, pass the message to base constructor - else if (parentClass.IsErrorDefinition && - currentMethod.Parameters.Any(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase) && - p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase))) - { - return " : base(message)"; - } return " : base()"; } + // For error classes with message constructor, pass the message to base constructor + else if (isConstructor && parentClass.IsErrorDefinition && + currentMethod.Parameters.Any(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase) && + p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase))) + { + return " : base(message)"; + } return string.Empty; } + private void WriteMethodPrototype(CodeMethod code, CodeClass parentClass, LanguageWriter writer, string returnType, bool inherits, bool isVoid) { var staticModifier = code.IsStatic ? "static " : string.Empty; From e287fd2a34fdac14561a3bafc62a3260f933e692 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Thu, 2 Oct 2025 11:46:57 +0200 Subject: [PATCH 07/15] Partially apply suggestions from code review static lambdas, simplification. Co-authored-by: Vincent Biret --- src/Kiota.Builder/CodeDOM/CodeMethod.cs | 4 ++-- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 4 ++-- src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 37f7ba24a7..d087483d7e 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -274,7 +274,7 @@ public IOrderedEnumerable> ErrorDescriptions { get { - return errorDescriptions.OrderBy(static x => x.Key); + return errorDescriptions.OrderBy(static x => x.Key, StringComparer.Ordinal); } } public bool HasErrorMappingCode(string code) @@ -316,7 +316,7 @@ public object Clone() Parent = Parent, OriginalIndexer = OriginalIndexer, errorMappings = new(errorMappings), - errorDescriptions = new(errorDescriptions), + errorDescriptions = new(errorDescriptions, StringComparer.Ordinal), AcceptedResponseTypes = new List(AcceptedResponseTypes), PagingInformation = PagingInformation?.Clone() as PagingInformation, Documentation = (CodeDocumentation)Documentation.Clone(), diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 96bb8592ae..8f44d2034a 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -276,14 +276,14 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) { // Add parameterless constructor if not already present - if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) { var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); codeClass.AddMethod(parameterlessConstructor); } // Add message constructor if not already present - if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase)))) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => "string".Equals(p.Type.Name, StringComparison.OrdinalIgnoreCase)))) { var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index 122be809d2..742373d4bd 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -270,7 +270,6 @@ protected static CodeMethod CreateConstructor(CodeClass parentClass, string desc { DescriptionTemplate = descriptionTemplate, }, - Parent = parentClass, }; } From 19491e59c3525a192bc1efb6444a4a354fbe75de Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Thu, 2 Oct 2025 13:57:28 +0200 Subject: [PATCH 08/15] Prefer enums over magic strings --- src/Kiota.Builder/CodeDOM/CodeMethod.cs | 6 ++- src/Kiota.Builder/CodeDOM/CodeParameter.cs | 4 ++ src/Kiota.Builder/Refiners/CSharpRefiner.cs | 47 +++++++++---------- .../Writers/CSharp/CodeMethodWriter.cs | 17 ++----- .../Refiners/CSharpLanguageRefinerTests.cs | 3 +- 5 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index d087483d7e..80eee26789 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -42,7 +42,11 @@ public enum CodeMethodKind /// /// The override for the error message for the error/exception type. /// - ErrorMessageOverride + ErrorMessageOverride, + /// + /// Factory method for error classes that accepts an error message parameter. + /// + FactoryWithErrorMessage, } public enum HttpMethod { diff --git a/src/Kiota.Builder/CodeDOM/CodeParameter.cs b/src/Kiota.Builder/CodeDOM/CodeParameter.cs index 9a23b66659..5f2cc42cd8 100644 --- a/src/Kiota.Builder/CodeDOM/CodeParameter.cs +++ b/src/Kiota.Builder/CodeDOM/CodeParameter.cs @@ -63,6 +63,10 @@ public enum CodeParameterKind /// This is only used for languages that use static functions for the serialization as opposed to instance methods since the OOP inheritance correctly handles that case. /// SerializingDerivedType, + /// + /// Error message parameter for error/exception class constructors and factory methods. + /// + ErrorMessage, } public class CodeParameter : CodeTerminalWithKind, ICloneable, IDocumentedElement, IDeprecableElement diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 8f44d2034a..70efe5cd6f 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -271,6 +271,21 @@ private void SetTypeAccessModifiers(CodeElement currentElement) CrawlTree(currentElement, SetTypeAccessModifiers); } + private static CodeParameter CreateErrorMessageParameter(string descriptionTemplate = "The error message") + { + return new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "string", IsExternal = true }, + Kind = CodeParameterKind.ErrorMessage, + Optional = false, + Documentation = new() + { + DescriptionTemplate = descriptionTemplate + } + }; + } + private static void AddConstructorsForErrorClasses(CodeElement currentElement) { if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) @@ -286,19 +301,7 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => "string".Equals(p.Type.Name, StringComparison.OrdinalIgnoreCase)))) { var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); - - // Add message parameter - messageConstructor.AddParameter(new CodeParameter - { - Name = "message", - Type = new CodeType { Name = "string", IsExternal = true }, - Optional = false, - Documentation = new() - { - DescriptionTemplate = "The error message" - } - }); - + messageConstructor.AddParameter(CreateErrorMessageParameter()); codeClass.AddMethod(messageConstructor); } } @@ -307,14 +310,15 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentElement) { + const string MethodName = "CreateFromDiscriminatorValueWithMessage"; if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition && - !codeClass.Methods.Any(m => m.Name == "CreateFromDiscriminatorValueWithMessage")) + !codeClass.Methods.Any(m => m.Name == MethodName)) { var method = codeClass.AddMethod(new CodeMethod { - Name = "CreateFromDiscriminatorValueWithMessage", - Kind = CodeMethodKind.Factory, + Name = MethodName, + Kind = CodeMethodKind.FactoryWithErrorMessage, IsAsync = false, IsStatic = true, Documentation = new(new() { @@ -349,16 +353,7 @@ private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentEl }); // Add message parameter - method.AddParameter(new CodeParameter - { - Name = "message", - Type = new CodeType { Name = "string", IsExternal = true }, - Optional = false, - Documentation = new() - { - DescriptionTemplate = "The error message to set on the created object" - } - }); + method.AddParameter(CreateErrorMessageParameter("The error message to set on the created object")); } CrawlTree(currentElement, AddMessageFactoryMethodForErrorClasses); } diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 4b536118c6..93371bf536 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -204,14 +204,10 @@ private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClas WriteFactoryMethodBodyForUnionModel(codeElement, parentClass, parseNodeParameter, writer); else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) WriteFactoryMethodBodyForIntersectionModel(codeElement, parentClass, parseNodeParameter, writer); - else if (codeElement.Name == "CreateFromDiscriminatorValueWithMessage" && parentClass.IsErrorDefinition) + else if (codeElement.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)) { - // Special case: CreateFromDiscriminatorValueWithMessage for error classes - var messageParam = codeElement.Parameters.FirstOrDefault(p => p.Name == "message"); - if (messageParam != null) - writer.WriteLine($"return new {parentClass.GetFullName()}({messageParam.Name});"); - else - writer.WriteLine($"return new {parentClass.GetFullName()}();"); + var messageParam = codeElement.Parameters.FirstOrDefault(p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return new {parentClass.GetFullName()}({messageParam.Name});"); } else writer.WriteLine($"return new {parentClass.GetFullName()}();"); @@ -411,10 +407,8 @@ protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams re writer.StartBlock(); foreach (var errorMapping in codeElement.ErrorMappings.Where(errorMapping => errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass)) { - var errorClass = errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition as CodeClass; var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); - - if (errorClass?.IsErrorDefinition == true) + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true } errorClass) { var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); var statusCodeAndDescription = !string.IsNullOrEmpty(errorDescription) @@ -634,8 +628,7 @@ private static string GetBaseSuffix(bool isConstructor, bool inherits, CodeClass } // For error classes with message constructor, pass the message to base constructor else if (isConstructor && parentClass.IsErrorDefinition && - currentMethod.Parameters.Any(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase) && - p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase))) + currentMethod.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))) { return " : base(message)"; } diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 5116156f65..512df73cf3 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -1023,8 +1023,7 @@ public async Task AddsMessageFactoryMethodToErrorClasses() // Then var messageFactoryMethod = errorClass.Methods - .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Factory) && - m.Name.Equals("CreateFromDiscriminatorValueWithMessage", StringComparison.OrdinalIgnoreCase)); + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); Assert.NotNull(messageFactoryMethod); Assert.Equal(2, messageFactoryMethod.Parameters.Count()); From 159e96a597e0decc045b85074aff07d58e4441b0 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 3 Oct 2025 16:19:59 +0200 Subject: [PATCH 09/15] Simplify, address more review comments --- src/Kiota.Builder/CodeDOM/CodeMethod.cs | 21 ++++++++----------- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 2 +- .../Writers/CSharp/CodeMethodWriter.cs | 4 ++-- .../Refiners/CSharpLanguageRefinerTests.cs | 10 +++++---- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 80eee26789..6ef21d4ffb 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -341,26 +341,23 @@ public void AddParameter(params CodeParameter[] methodParameters) EnsureElementsAreChildren(methodParameters); methodParameters.ToList().ForEach(x => parameters.TryAdd(x.Name, x)); } - public void AddErrorMapping(string errorCode, CodeTypeBase type) + public void AddErrorMapping(string errorCode, CodeTypeBase type, string? description = null) { ArgumentNullException.ThrowIfNull(type); ArgumentException.ThrowIfNullOrEmpty(errorCode); - errorMappings.TryAdd(errorCode, type); - } - - public void AddErrorMapping(string errorCode, CodeTypeBase type, string description) - { - ArgumentNullException.ThrowIfNull(type); - ArgumentException.ThrowIfNullOrEmpty(errorCode); - errorMappings.TryAdd(errorCode, type); - if (!string.IsNullOrEmpty(description)) + if (errorMappings.TryAdd(errorCode, type) && !string.IsNullOrEmpty(description)) + { errorDescriptions.TryAdd(errorCode, description); + } } public string? GetErrorDescription(string errorCode) { ArgumentException.ThrowIfNullOrEmpty(errorCode); - errorDescriptions.TryGetValue(errorCode, out var description); - return description; + if (errorMappings.ContainsKey(errorCode) && errorDescriptions.TryGetValue(errorCode, out var description)) + { + return description; + } + return null; } } diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 70efe5cd6f..ffcf3b6822 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -298,7 +298,7 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) } // Add message constructor if not already present - if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => "string".Equals(p.Type.Name, StringComparison.OrdinalIgnoreCase)))) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) { var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); messageConstructor.AddParameter(CreateErrorMessageParameter()); diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 93371bf536..427ee060ef 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -206,7 +206,7 @@ private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClas WriteFactoryMethodBodyForIntersectionModel(codeElement, parentClass, parseNodeParameter, writer); else if (codeElement.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)) { - var messageParam = codeElement.Parameters.FirstOrDefault(p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); writer.WriteLine($"return new {parentClass.GetFullName()}({messageParam.Name});"); } else @@ -628,7 +628,7 @@ private static string GetBaseSuffix(bool isConstructor, bool inherits, CodeClass } // For error classes with message constructor, pass the message to base constructor else if (isConstructor && parentClass.IsErrorDefinition && - currentMethod.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))) + currentMethod.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage))) { return " : base(message)"; } diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 512df73cf3..0ae48bd618 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -978,13 +978,15 @@ public async Task AddsMessageConstructorToErrorClasses() // Then var messageConstructor = errorClass.Methods .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && - m.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + m.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))); Assert.NotNull(messageConstructor); Assert.Single(messageConstructor.Parameters); - Assert.Equal("message", messageConstructor.Parameters.First().Name); - Assert.Equal("string", messageConstructor.Parameters.First().Type.Name); - Assert.False(messageConstructor.Parameters.First().Optional); + var parameter = messageConstructor.Parameters.First(); + Assert.Equal("message", parameter.Name); + Assert.Equal("string", parameter.Type.Name); + Assert.False(parameter.Optional); + Assert.Equal(CodeParameterKind.ErrorMessage, parameter.Kind); } [Fact] From 5856ca0a9a95de19e709f0ebbcd41b6b5a04a0d8 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Fri, 3 Oct 2025 16:49:55 +0200 Subject: [PATCH 10/15] Don't include the template status code to avoid confusion with 4xx 5xx generic codes --- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 2 +- .../Writers/CSharp/CodeMethodWriter.cs | 20 ++++++++----------- .../Refiners/CSharpLanguageRefinerTests.cs | 8 +++++--- .../Writers/CSharp/CodeMethodWriterTests.cs | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index ffcf3b6822..acf2bffac1 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -313,7 +313,7 @@ private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentEl const string MethodName = "CreateFromDiscriminatorValueWithMessage"; if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition && - !codeClass.Methods.Any(m => m.Name == MethodName)) + !codeClass.Methods.Any(m => m.Name.Equals(MethodName, StringComparison.Ordinal))) { var method = codeClass.AddMethod(new CodeMethod { diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 427ee060ef..14b139a6ed 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -405,21 +405,17 @@ protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams re errorMappingVarName = "errorMapping"; writer.WriteLine($"var {errorMappingVarName} = new Dictionary>"); writer.StartBlock(); - foreach (var errorMapping in codeElement.ErrorMappings.Where(errorMapping => errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass)) + foreach (var errorMapping in codeElement.ErrorMappings) { + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass codeClass)) continue; var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); - if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true } errorClass) - { - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - var statusCodeAndDescription = !string.IsNullOrEmpty(errorDescription) - ? $"{errorMapping.Key} {errorDescription}" - : errorMapping.Key; - writer.WriteLine($"{{ \"{errorMapping.Key.ToUpperInvariant()}\", (parseNode) => {typeName}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{statusCodeAndDescription}\") }},"); - } + var errorKey = errorMapping.Key.ToUpperInvariant(); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && codeClass.IsErrorDefinition) + writer.WriteLine($"{{ \"{errorKey}\", (parseNode) => {typeName}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription}\") }},"); else - { - writer.WriteLine($"{{ \"{errorMapping.Key.ToUpperInvariant()}\", {typeName}.CreateFromDiscriminatorValue }},"); - } + writer.WriteLine($"{{ \"{errorKey}\", {typeName}.CreateFromDiscriminatorValue }},"); } writer.CloseBlock("};"); } diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 0ae48bd618..5e31a95a31 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -1005,7 +1005,7 @@ public async Task DoesNotAddMessageConstructorToNonErrorClasses() // Then var messageConstructor = regularClass.Methods .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && - m.Parameters.Any(p => p.Type.Name.Equals("string", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + m.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))); Assert.Null(messageConstructor); } @@ -1030,12 +1030,14 @@ public async Task AddsMessageFactoryMethodToErrorClasses() Assert.NotNull(messageFactoryMethod); Assert.Equal(2, messageFactoryMethod.Parameters.Count()); - var parseNodeParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("parseNode", StringComparison.OrdinalIgnoreCase)); + var parseNodeParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.IsOfKind(CodeParameterKind.ParseNode)); Assert.NotNull(parseNodeParam); + Assert.Equal("parseNode", parseNodeParam.Name); Assert.Equal("IParseNode", parseNodeParam.Type.Name); - var messageParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase)); + var messageParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.IsOfKind(CodeParameterKind.ErrorMessage)); Assert.NotNull(messageParam); + Assert.Equal("message", messageParam.Name); Assert.Equal("string", messageParam.Type.Name); Assert.True(messageFactoryMethod.IsStatic); diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs index e513796041..0462a1692a 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs @@ -2124,8 +2124,8 @@ public void WritesRequestExecutorWithEnhancedErrorMapping() var result = tw.ToString(); Assert.Contains("var requestInfo", result); Assert.Contains("var errorMapping = new Dictionary>", result); - Assert.Contains("{ \"4XX\", (parseNode) => Error4XX.CreateFromDiscriminatorValueWithMessage(parseNode, \"4XX Client Error\") }", result); - Assert.Contains("{ \"401\", (parseNode) => Error401.CreateFromDiscriminatorValueWithMessage(parseNode, \"401 Unauthorized\") }", result); + Assert.Contains("{ \"4XX\", (parseNode) => Error4XX.CreateFromDiscriminatorValueWithMessage(parseNode, \"Client Error\") }", result); + Assert.Contains("{ \"401\", (parseNode) => Error401.CreateFromDiscriminatorValueWithMessage(parseNode, \"Unauthorized\") }", result); Assert.Contains("send", result.ToLower()); } } From c9b42ee63937b8bed036fe50a983e4dced629cf5 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Sun, 14 Dec 2025 16:27:22 +0100 Subject: [PATCH 11/15] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit c3bc04138a70ada5386c5c55e9ac5d6574075a95 Author: Koen van Leeuwen Date: Sun Dec 14 15:53:41 2025 +0100 Fix python and php commit 0bd60a02cc5540ff6d115ef0860caac630ee298c Author: Koen van Leeuwen Date: Sun Dec 14 10:53:47 2025 +0100 fix php commit 52d3a9453edef2384ef34fd80d4e67569a4e0d2c Author: Koen van Leeuwen Date: Sun Dec 14 10:10:12 2025 +0100 base CreateConstructor messes up the naming commit 8dac31f152b27da72d30fb4207f66cf4b2c9b4c9 Author: Koen van Leeuwen Date: Sat Dec 13 12:14:15 2025 +0100 debugging the generated code commit 6138205d41c9c70eb2e22a1f9663b5736153bd43 Author: Koen van Leeuwen Date: Sat Dec 13 11:01:29 2025 +0100 The Go writer has the last word on constructor names commit de7416fb82c6759112b4bd20894b072adfb6388e Author: Koen van Leeuwen Date: Sat Dec 13 10:20:07 2025 +0100 update tests to reflect the new proper type enums commit 87bd2b6108d5cdf451dff3dcf8688696b80cab51 Author: Koen van Leeuwen Date: Sat Dec 13 09:17:39 2025 +0100 pass the parameter directly commit 271b4b0912cc931e5c3376beab137f7504696268 Author: Koen van Leeuwen Date: Fri Dec 12 21:47:53 2025 +0100 refactor very similar factory creation commit 67dc07502c7d440f3939d744577900c62b5bae05 Author: Koen van Leeuwen Date: Fri Dec 12 21:24:32 2025 +0100 remove switch/if antipattern commit e9d5bc9eedea9525fe719cdc55d19b4b587e214f Author: Koen van Leeuwen Date: Fri Dec 12 20:36:44 2025 +0100 Simplify param creation commit 4783f30e2e27d752247d4d40f94d1ce4b717694d Merge: 988bce96b 5a492c092 Author: Koen van Leeuwen Date: Fri Dec 12 20:08:49 2025 +0100 Merge remote-tracking branch 'upstream/main' into OpenAPI-description-in-exceptions-other-languages commit 988bce96bf6eb8290d59c47dae90776efac69cf5 Author: Koen van Leeuwen Date: Fri Dec 12 20:06:51 2025 +0100 Inline factory method generation to align with other languages commit 56c37ae1c5571efee5b0de6a7d28697ef291ee7e Author: Koen van Leeuwen Date: Fri Dec 12 19:58:13 2025 +0100 fix other tests commit 4eb5959a48d3f79b1ceaaced83a91a08ef2fe282 Author: Koen van Leeuwen Date: Fri Dec 12 15:30:55 2025 +0100 fix go commit 12f463995bc3b55d34596a0b50c4f3e2cbee05ad Author: Koen van Leeuwen Date: Fri Dec 12 15:16:17 2025 +0100 update tests commit 7a5b2953997bfc71a8d4ead1f836906422eabe51 Merge: 9ffe8c4dc 206e170c2 Author: Koen van Leeuwen Date: Fri Dec 12 08:27:18 2025 +0100 Merge remote-tracking branch 'upstream/main' into OpenAPI-description-in-exceptions-other-languages commit 9ffe8c4dc906dd1b6a4478e59d0d6a34a280db3d Author: Koen van Leeuwen Date: Fri Dec 12 08:01:25 2025 +0100 restore merge artifact commit 7a61a840c992ba7987847a6cd9de1fd4226428db Author: Koen van Leeuwen Date: Fri Dec 12 07:50:51 2025 +0100 extract error message parameter creation commit da6b4ed7655132eed8ebb670f7f680d15846c663 Author: Koen van Leeuwen Date: Thu Dec 11 20:38:05 2025 +0100 Add Ruby commit 38bf2eefc06f879b70ac74c24a278da485714cc4 Author: Koen van Leeuwen Date: Thu Dec 11 20:32:23 2025 +0100 cleanup Python commit 4485b78809f1269ffe8b1d86a6030d8aac982449 Author: Koen van Leeuwen Date: Thu Dec 11 20:24:34 2025 +0100 cleanup PHP commit 877ad2b3e0cbe7a104b1807041a20ecb7d839a64 Author: Koen van Leeuwen Date: Thu Dec 11 20:04:47 2025 +0100 Cleanup Go commit e39a9324a95d83f68436ed9a1c844eee1f9b7b80 Author: Koen van Leeuwen Date: Thu Dec 11 19:31:12 2025 +0100 Cleanup Typescript commit fc11b7ff90f381939fa2fee5912581fa91f79ed5 Author: Koen van Leeuwen Date: Thu Dec 11 19:26:13 2025 +0100 Cleanup Java commit 133a78d75c7470f7a944eff67acaabc574360b15 Author: Koen van Leeuwen Date: Thu Sep 25 14:03:54 2025 +0200 feat: add OpenAPI error description enhancements for Python - Add parameterless and message constructors to Python error classes - Add create_from_discriminator_value_with_message factory method - Enhance error mappings to include human-readable descriptions - Support constructor inheritance with super().__init__(message) - Add comprehensive unit tests for refiner and method writer - Maintain backward compatibility with existing error mappings 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude commit f3cc7bef9ba2d13a6b71aa4e108025bdbc8d0c52 Author: Koen van Leeuwen Date: Fri Sep 26 13:46:05 2025 +0200 Implement TypeScript error description enhancements - Add constructors for error classes with optional message parameter - Add createFromDiscriminatorValueWithMessage factory methods for error classes - Enhance error mappings in CodeConstantWriter with human-readable descriptions - Add comprehensive tests for refiner and writer enhancements - Fix test expectations for non-error class factory method behavior - Correct call order for AddConstructorsForErrorClasses in TypeScript refiner pipeline 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude commit ae746e5f4e7180c5678ac4f737e0ecf90375e653 Author: Koen van Leeuwen Date: Fri Sep 26 10:21:05 2025 +0200 Implement OpenAPI error description enhancements for PHP - Add constructors for error classes (__construct with parameterless and message variants) - Add createFromDiscriminatorValueWithMessage factory method for enhanced error handling - Enhance error mapping generation with human-readable descriptions using PHP function syntax - Add special handling for error factory methods in method writer - Add comprehensive tests for PHP error enhancements - Follow PHP conventions with camelCase method names and constructor patterns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude commit eba3aa324db63839cd8b37f8cc5927a7ba2f80a2 Author: Koen van Leeuwen Date: Fri Sep 26 10:00:24 2025 +0200 Implement OpenAPI error description enhancements for Go - Add factory methods for error classes (NewErrorClass, NewErrorClassWithMessage, CreateFromDiscriminatorValueWithMessage) - Enhance error mapping generation with human-readable descriptions using Go function syntax - Add special handling for error factory methods in method writer - Add comprehensive tests for Go error enhancements - Follow Go conventions with factory functions and pointer returns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude commit 98728fd91475ca4fb65669ffe743c532fbf8b784 Author: Koen van Leeuwen Date: Thu Sep 25 13:36:38 2025 +0200 Add Dart commit 24ffc6e4d088b9086f21cad8a19dcef52cc1c5c4 Author: Koen van Leeuwen Date: Thu Sep 25 13:24:41 2025 +0200 Add Java --- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 75 +----- .../Refiners/CommonLanguageRefiner.cs | 114 ++++++++ src/Kiota.Builder/Refiners/DartRefiner.cs | 64 +++-- src/Kiota.Builder/Refiners/GoRefiner.cs | 33 +++ src/Kiota.Builder/Refiners/JavaRefiner.cs | 52 ++++ src/Kiota.Builder/Refiners/PhpRefiner.cs | 31 +++ src/Kiota.Builder/Refiners/PythonRefiner.cs | 54 ++++ src/Kiota.Builder/Refiners/RubyRefiner.cs | 34 +++ .../Refiners/TypeScriptRefiner.cs | 54 ++++ .../Writers/CSharp/CodeMethodWriter.cs | 15 +- .../Writers/Dart/CodeMethodWriter.cs | 59 +++- .../Writers/Go/CodeMethodWriter.cs | 77 ++++-- .../Writers/Java/CodeMethodWriter.cs | 28 +- .../Writers/Php/CodeMethodWriter.cs | 28 +- .../Writers/Python/CodeMethodWriter.cs | 39 ++- .../Writers/Ruby/CodeMethodWriter.cs | 24 +- .../Writers/TypeScript/CodeConstantWriter.cs | 14 +- .../Writers/TypeScript/CodeFunctionWriter.cs | 16 ++ .../GenerateSample.cs | 1 + .../Refiners/DartLanguageRefinerTests.cs | 121 +++++++++ .../Refiners/GoLanguageRefinerTests.cs | 100 +++++++ .../Refiners/JavaLanguageRefinerTests.cs | 121 +++++++++ .../Refiners/PhpLanguageRefinerTests.cs | 102 +++++++ .../Refiners/PythonLanguageRefinerTests.cs | 124 +++++++++ .../TypeScriptLanguageRefinerTests.cs | 95 +++++++ .../Writers/Dart/CodeMethodWriterTests.cs | 167 ++++++++++++ .../Writers/Go/CodeMethodWriterTests.cs | 242 +++++++++++++++++ .../Writers/Java/CodeMethodWriterTests.cs | 107 ++++++++ .../Writers/Php/CodeMethodWriterTests.cs | 251 ++++++++++++++++++ .../Writers/Python/CodeMethodWriterTests.cs | 136 ++++++++++ .../TypeScript/CodeConstantWriterTests.cs | 74 ++++++ .../TypeScript/CodeFunctionWriterTests.cs | 170 ++++++++++++ 32 files changed, 2499 insertions(+), 123 deletions(-) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index acf2bffac1..48fb020994 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -108,7 +108,6 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken ); AddConstructorsForDefaultValues(generatedCode, false); AddConstructorsForErrorClasses(generatedCode); - AddMessageFactoryMethodForErrorClasses(generatedCode); AddDiscriminatorMappingsUsingsToParentClasses( generatedCode, "IParseNode" @@ -271,21 +270,6 @@ private void SetTypeAccessModifiers(CodeElement currentElement) CrawlTree(currentElement, SetTypeAccessModifiers); } - private static CodeParameter CreateErrorMessageParameter(string descriptionTemplate = "The error message") - { - return new CodeParameter - { - Name = "message", - Type = new CodeType { Name = "string", IsExternal = true }, - Kind = CodeParameterKind.ErrorMessage, - Optional = false, - Documentation = new() - { - DescriptionTemplate = descriptionTemplate - } - }; - } - private static void AddConstructorsForErrorClasses(CodeElement currentElement) { if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) @@ -297,64 +281,21 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) codeClass.AddMethod(parameterlessConstructor); } + var messageParameter = CreateErrorMessageParameter("string"); // Add message constructor if not already present if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) { var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); - messageConstructor.AddParameter(CreateErrorMessageParameter()); + messageConstructor.AddParameter(messageParameter); codeClass.AddMethod(messageConstructor); } - } - CrawlTree(currentElement, AddConstructorsForErrorClasses); - } - - private static void AddMessageFactoryMethodForErrorClasses(CodeElement currentElement) - { - const string MethodName = "CreateFromDiscriminatorValueWithMessage"; - if (currentElement is CodeClass codeClass && - codeClass.IsErrorDefinition && - !codeClass.Methods.Any(m => m.Name.Equals(MethodName, StringComparison.Ordinal))) - { - var method = codeClass.AddMethod(new CodeMethod - { - Name = MethodName, - Kind = CodeMethodKind.FactoryWithErrorMessage, - IsAsync = false, - IsStatic = true, - Documentation = new(new() { - {"TypeName", new CodeType { - IsExternal = false, - TypeDefinition = codeClass, - }} - }) - { - DescriptionTemplate = "Creates a new instance of the appropriate class based on discriminator value with a custom error message.", - }, - Access = AccessModifier.Public, - ReturnType = new CodeType - { - Name = codeClass.Name, - TypeDefinition = codeClass, - }, - Parent = codeClass, - }).Single(); - // Add parseNode parameter - method.AddParameter(new CodeParameter - { - Name = "parseNode", - Type = new CodeType { Name = "IParseNode", IsExternal = true }, - Kind = CodeParameterKind.ParseNode, - Optional = false, - Documentation = new() - { - DescriptionTemplate = "The parse node to use to read the discriminator value and create the object" - } - }); - - // Add message parameter - method.AddParameter(CreateErrorMessageParameter("The error message to set on the created object")); + var method = TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "CreateFromDiscriminatorValueWithMessage", + messageParameter: messageParameter, + parseNodeTypeName: "IParseNode"); } - CrawlTree(currentElement, AddMessageFactoryMethodForErrorClasses); + CrawlTree(currentElement, AddConstructorsForErrorClasses); } } diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index 742373d4bd..355af32714 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -1626,4 +1626,118 @@ protected static void DeduplicateErrorMappings(CodeElement codeElement) } CrawlTree(codeElement, DeduplicateErrorMappings); } + + /// + /// Creates a CodeParameter for error messages with language-specific configuration. + /// + /// The type name for the message parameter (e.g., "string", "String", "str") + /// Whether the parameter is optional + /// The default value if optional (e.g., "None", "nil") + /// The documentation description template + /// A configured CodeParameter for error messages + protected static CodeParameter CreateErrorMessageParameter( + string typeName, + bool optional = false, + string defaultValue = "") + { + return new CodeParameter + { + Name = "message", + Type = new CodeType { Name = typeName, IsExternal = true }, + Kind = CodeParameterKind.ErrorMessage, + Optional = optional, + DefaultValue = defaultValue, + Documentation = new() + { + DescriptionTemplate = "The error message to set" + } + }; + } + + /// + /// Creates a factory method for error classes that accepts both parseNode and message parameters, + /// and adds it to the class if it doesn't already exist. + /// This factory method is used to create error instances with custom error messages from discriminator values. + /// + /// The error class to create the factory method for + /// The name of the factory method (e.g., "createFromDiscriminatorValueWithMessage") + /// The type name for the ParseNode parameter (e.g., "ParseNode", "IParseNode") + /// The type name for the message parameter (e.g., "string", "String") + /// Optional custom return type name; if null, uses codeClass.Name + /// Whether the return type should be nullable + /// Whether the message parameter should be optional + /// Whether to set the Parent property on the method + /// Whether the return type is external (e.g., for Go's "Parsable") + /// True if the method was created and added; false if it already existed + protected static bool TryAddErrorMessageFactoryMethod( + CodeClass codeClass, + string methodName, + string parseNodeTypeName, + CodeParameter messageParameter, + string? returnTypeName = null, + bool returnTypeIsNullable = false, + bool setParent = true, + bool returnTypeIsExternal = false, + string parseNodeParameterName = "parseNode") + { + ArgumentNullException.ThrowIfNull(codeClass); + + // Check if method already exists + if (codeClass.Methods.Any(m => m.Name.Equals(methodName, StringComparison.Ordinal))) + { + return false; + } + + var method = new CodeMethod + { + Name = methodName, + Kind = CodeMethodKind.FactoryWithErrorMessage, + IsAsync = false, + IsStatic = true, + Documentation = new(new Dictionary + { + { + "TypeName", new CodeType + { + IsExternal = false, + TypeDefinition = codeClass, + } + } + }) + { + DescriptionTemplate = "Creates a new instance of the appropriate class based on discriminator value with a custom error message.", + }, + Access = AccessModifier.Public, + ReturnType = new CodeType + { + Name = returnTypeName ?? codeClass.Name, + TypeDefinition = returnTypeIsExternal ? null : codeClass, + IsNullable = returnTypeIsNullable, + IsExternal = returnTypeIsExternal + } + }; + + if (setParent) + { + method.Parent = codeClass; + } + + method.AddParameter(new CodeParameter + { + Name = parseNodeParameterName, + Kind = CodeParameterKind.ParseNode, + Type = new CodeType { Name = parseNodeTypeName, IsExternal = true }, + Optional = false, + Documentation = new() + { + DescriptionTemplate = "The parse node to use to read the discriminator value and create the object" + } + }); + + // Add message parameter + method.AddParameter(messageParameter); + + codeClass.AddMethod(method); + return true; + } } diff --git a/src/Kiota.Builder/Refiners/DartRefiner.cs b/src/Kiota.Builder/Refiners/DartRefiner.cs index fc74705673..5b3d39389d 100644 --- a/src/Kiota.Builder/Refiners/DartRefiner.cs +++ b/src/Kiota.Builder/Refiners/DartRefiner.cs @@ -173,32 +173,56 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken ///error classes should always have a constructor for the copyWith method private void AddConstructorForErrorClass(CodeElement currentElement) { - if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition && !codeClass.Methods.Where(static x => x.IsOfKind(CodeMethodKind.Constructor)).Any()) + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) { - codeClass.AddMethod(new CodeMethod + // Add parameterless constructor if not already present + if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) { - Name = "constructor", - Kind = CodeMethodKind.Constructor, - IsAsync = false, - IsStatic = false, - Documentation = new(new() { - {"TypeName", new CodeType { - IsExternal = false, - TypeDefinition = codeClass, - } - } - }) - { - DescriptionTemplate = "Instantiates a new {TypeName} and sets the default values.", - }, - Access = AccessModifier.Public, - ReturnType = new CodeType { Name = "void", IsExternal = true }, - Parent = codeClass, - }); + var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); + codeClass.AddMethod(parameterlessConstructor); + } + var messageParameter = CreateErrorMessageParameter("String"); + // Add message constructor if not already present + if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + { + var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); + messageConstructor.AddParameter(messageParameter); + codeClass.AddMethod(messageConstructor); + } + + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "createFromDiscriminatorValueWithMessage", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter, + setParent: false); } CrawlTree(currentElement, element => AddConstructorForErrorClass(element)); } + private static CodeMethod CreateConstructor(CodeClass codeClass, string descriptionTemplate) + { + return new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + IsAsync = false, + IsStatic = false, + Documentation = new(new() { + {"TypeName", new CodeType { + IsExternal = false, + TypeDefinition = codeClass, + }} + }) + { + DescriptionTemplate = descriptionTemplate, + }, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = codeClass, + }; + } + /// /// Corrects common names so they can be used with Dart. /// This normally comes down to changing the first character to lower case. diff --git a/src/Kiota.Builder/Refiners/GoRefiner.cs b/src/Kiota.Builder/Refiners/GoRefiner.cs index b7c2e93589..8882b5dd74 100644 --- a/src/Kiota.Builder/Refiners/GoRefiner.cs +++ b/src/Kiota.Builder/Refiners/GoRefiner.cs @@ -186,6 +186,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken "ApiError", "github.com/microsoft/kiota-abstractions-go" ); + AddConstructorsForErrorClasses(generatedCode); AddDiscriminatorMappingsUsingsToParentClasses( generatedCode, "ParseNode", @@ -1009,4 +1010,36 @@ private void NormalizeNamespaceNames(CodeElement currentElement) } CrawlTree(currentElement, NormalizeNamespaceNames); } + + private static void AddConstructorsForErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + // Add parameterless constructor if not already present (Go writer generates New* names) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) + { + var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); + codeClass.AddMethod(parameterlessConstructor); + } + + var messageParameter = CreateErrorMessageParameter("string"); + // Add message constructor if not already present (Go writer generates NewWithMessage names) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + { + var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); + messageConstructor.AddParameter(messageParameter); + codeClass.AddMethod(messageConstructor); + } + + // Add discriminator-based factory with message + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "CreateFromDiscriminatorValueWithMessage", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter, + returnTypeName: "Parsable", + returnTypeIsExternal: true); + } + CrawlTree(currentElement, AddConstructorsForErrorClasses); + } } diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs index 123a2e8fc6..21df2c1a38 100644 --- a/src/Kiota.Builder/Refiners/JavaRefiner.cs +++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs @@ -148,6 +148,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken "ApiException", AbstractionsNamespaceName ); + AddConstructorsForErrorClasses(generatedCode); AddDiscriminatorMappingsUsingsToParentClasses( generatedCode, "ParseNode", @@ -552,4 +553,55 @@ private void AddQueryParameterExtractorMethod(CodeElement currentElement, string } CrawlTree(currentElement, x => AddQueryParameterExtractorMethod(x, methodName)); } + + private static void AddConstructorsForErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + // Add parameterless constructor if not already present + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) + { + var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); + codeClass.AddMethod(parameterlessConstructor); + } + var messageParameter = CreateErrorMessageParameter("String"); + // Add message constructor if not already present + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + { + var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); + messageConstructor.AddParameter(messageParameter); + codeClass.AddMethod(messageConstructor); + } + + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "createFromDiscriminatorValueWithMessage", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter); + } + CrawlTree(currentElement, AddConstructorsForErrorClasses); + } + + private static CodeMethod CreateConstructor(CodeClass codeClass, string descriptionTemplate) + { + return new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + IsAsync = false, + IsStatic = false, + Documentation = new(new() { + {"TypeName", new CodeType { + IsExternal = false, + TypeDefinition = codeClass, + }} + }) + { + DescriptionTemplate = descriptionTemplate, + }, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = codeClass, + }; + } } diff --git a/src/Kiota.Builder/Refiners/PhpRefiner.cs b/src/Kiota.Builder/Refiners/PhpRefiner.cs index 9c68210629..a9ba1132a3 100644 --- a/src/Kiota.Builder/Refiners/PhpRefiner.cs +++ b/src/Kiota.Builder/Refiners/PhpRefiner.cs @@ -80,6 +80,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken ); MoveClassesWithNamespaceNamesUnderNamespace(generatedCode); AddConstructorsForDefaultValues(generatedCode, true); + AddConstructorsForErrorClasses(generatedCode); // Run after AddConstructorsForDefaultValues so we can add message parameter to existing constructor cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested(); CorrectParameterType(generatedCode); @@ -471,5 +472,35 @@ private static void AddQueryParameterFactoryMethod(CodeElement codeElement) } CrawlTree(codeElement, AddQueryParameterFactoryMethod); } + + private static void AddConstructorsForErrorClasses(CodeElement codeElement) + { + if (codeElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + var messageParameter = CreateErrorMessageParameter("string", optional: true, defaultValue: "''"); + // PHP only allows one __construct method, so we add an optional message parameter to the existing constructor + // The constructor may already exist from AddConstructorsForDefaultValues + var existingConstructor = codeClass.Methods.FirstOrDefault(static m => m.IsOfKind(CodeMethodKind.Constructor)); + if (existingConstructor == null) + { + existingConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); + codeClass.AddMethod(existingConstructor); + } + if (!existingConstructor.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage))) + { + existingConstructor.AddParameter(messageParameter); + } + + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "createFromDiscriminatorValueWithMessage", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter, + returnTypeIsNullable: true, + setParent: false); + } + + CrawlTree(codeElement, AddConstructorsForErrorClasses); + } } diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index a42229717b..e33b50be2d 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -99,6 +99,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken "APIError", $"{AbstractionsPackageName}.api_error" ); + AddConstructorsForErrorClasses(generatedCode); AddGetterAndSetterMethods(generatedCode, new() { CodePropertyKind.Custom, @@ -390,4 +391,57 @@ public static IEnumerable codeTypeFilter(IEnumerable }, })}, }; + + private static void AddConstructorsForErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + // Add parameterless constructor if not already present + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) + { + var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); + codeClass.AddMethod(parameterlessConstructor); + } + + // Add message constructor if not already present + var messageParameter = CreateErrorMessageParameter("str", optional: true, defaultValue: "None"); + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + { + var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); + messageConstructor.AddParameter(messageParameter); + codeClass.AddMethod(messageConstructor); + } + + TryAddErrorMessageFactoryMethod(codeClass, + "create_from_discriminator_value_with_message", + "ParseNode", + messageParameter, + parseNodeParameterName: "parse_node" + ); + } + CrawlTree(currentElement, AddConstructorsForErrorClasses); + } + + private static CodeMethod CreateConstructor(CodeClass codeClass, string descriptionTemplate) + { + return new CodeMethod + { + Name = "__init__", + Kind = CodeMethodKind.Constructor, + IsAsync = false, + IsStatic = false, + Documentation = new(new() { + {"TypeName", new CodeType { + IsExternal = false, + TypeDefinition = codeClass, + }} + }) + { + DescriptionTemplate = descriptionTemplate, + }, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "None", IsExternal = true }, + Parent = codeClass, + }; + } } diff --git a/src/Kiota.Builder/Refiners/RubyRefiner.cs b/src/Kiota.Builder/Refiners/RubyRefiner.cs index b2165d289a..c9c83c52a3 100644 --- a/src/Kiota.Builder/Refiners/RubyRefiner.cs +++ b/src/Kiota.Builder/Refiners/RubyRefiner.cs @@ -79,6 +79,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken "MicrosoftKiotaAbstractions", true ); + AddConstructorsForErrorClasses(generatedCode); ReplaceReservedNames(generatedCode, reservedNamesProvider, x => $"{x}_escaped"); AddGetterAndSetterMethods(generatedCode, [ @@ -336,4 +337,37 @@ private static void CorrectImplements(ProprietableBlockDeclaration block) .ToList() .ForEach(static x => x.Name = "MicrosoftKiotaAbstractions::AdditionalDataHolder"); } + + private static void AddConstructorsForErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + var messageParameter = CreateErrorMessageParameter("String", optional: true, defaultValue: "nil"); + // Add initialize method (Ruby's constructor) with message parameter if not exists + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + { + var messageConstructor = new CodeMethod + { + Name = "initialize", + Kind = CodeMethodKind.Constructor, + Access = AccessModifier.Public, + IsAsync = false, + Documentation = new() + { + DescriptionTemplate = "Instantiates a new {TypeName} with an optional error message." + }, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }; + messageConstructor.AddParameter(messageParameter); + codeClass.AddMethod(messageConstructor); + } + + TryAddErrorMessageFactoryMethod(codeClass, + "create_from_discriminator_value_with_message", + "parse_node", + messageParameter: messageParameter + ); + } + CrawlTree(currentElement, AddConstructorsForErrorClasses); + } } diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs index 155a3e7031..b98b2e4163 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs @@ -161,6 +161,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken CodePropertyKind.QueryParameter, ], static s => s.ToCamelCase(UnderscoreArray)); + AddConstructorsForErrorClasses(generatedCode); IntroducesInterfacesAndFunctions(generatedCode, factoryNameCallbackFromType); GenerateEnumObjects(generatedCode); AliasUsingsWithSameSymbol(generatedCode); @@ -1562,4 +1563,57 @@ private static void AddDeserializerUsingToDiscriminatorFactory(CodeElement codeE } CrawlTree(codeElement, AddDeserializerUsingToDiscriminatorFactory); } + + private static void AddConstructorsForErrorClasses(CodeElement codeElement) + { + if (codeElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + // Add parameterless constructor if not exists + if (!codeClass.Methods.Any(static m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any())) + { + var parameterlessConstructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + Access = AccessModifier.Public, + IsAsync = false, + Documentation = new() + { + DescriptionTemplate = "Instantiates a new {TypeName}." + }, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }; + codeClass.AddMethod(parameterlessConstructor); + } + var messageParameter = CreateErrorMessageParameter("string", optional: true); + // Add constructor with message parameter if not exists + if (!codeClass.Methods.Any(static m => m.IsOfKind(CodeMethodKind.Constructor) && m.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + { + var messageConstructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + Access = AccessModifier.Public, + IsAsync = false, + Documentation = new() + { + DescriptionTemplate = "Instantiates a new {TypeName} with an error message." + }, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }; + messageConstructor.AddParameter(messageParameter); + codeClass.AddMethod(messageConstructor); + } + + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "createFromDiscriminatorValueWithMessage", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter, + returnTypeIsNullable: true, + setParent: false); + } + + CrawlTree(codeElement, AddConstructorsForErrorClasses); + } } diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 14b139a6ed..5e35c5e4ea 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -95,6 +95,9 @@ protected virtual void HandleMethodKind(CodeMethod codeElement, LanguageWriter w case CodeMethodKind.Factory: WriteFactoryMethodBody(codeElement, parentClass, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + break; case CodeMethodKind.ComposedTypeMarker: throw new InvalidOperationException("ComposedTypeMarker is not required as interface is explicitly implemented."); default: @@ -191,6 +194,11 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, writer.WriteLine($"return {ResultVarName};"); } private const string DiscriminatorMappingVarName = "mappingValue"; + private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return new {parentClass.GetFullName()}({messageParam.Name});"); + } private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); @@ -204,11 +212,6 @@ private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClas WriteFactoryMethodBodyForUnionModel(codeElement, parentClass, parseNodeParameter, writer); else if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) WriteFactoryMethodBodyForIntersectionModel(codeElement, parentClass, parseNodeParameter, writer); - else if (codeElement.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)) - { - var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); - writer.WriteLine($"return new {parentClass.GetFullName()}({messageParam.Name});"); - } else writer.WriteLine($"return new {parentClass.GetFullName()}();"); } @@ -573,7 +576,7 @@ protected string GetSendRequestMethodName(bool isVoid, CodeElement currentElemen private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) { conventions.WriteLongDescription(code, writer); - if (!"void".Equals(code.ReturnType.Name, StringComparison.OrdinalIgnoreCase) && code.Kind is not CodeMethodKind.ClientConstructor or CodeMethodKind.Constructor) + if (!"void".Equals(code.ReturnType.Name, StringComparison.OrdinalIgnoreCase) && code.Kind is not (CodeMethodKind.ClientConstructor or CodeMethodKind.Constructor)) conventions.WriteAdditionalDescriptionItem($"A {conventions.GetTypeStringForDocumentation(code.ReturnType, code)}", writer); foreach (var paramWithDescription in code.Parameters .Where(static x => x.Documentation.DescriptionAvailable) diff --git a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs index 2a9ddca836..f77b839b83 100644 --- a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs @@ -125,6 +125,9 @@ protected virtual void HandleMethodKind(CodeMethod codeElement, LanguageWriter w case CodeMethodKind.Factory: WriteFactoryMethodBody(codeElement, parentClass, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + break; case CodeMethodKind.Custom: WriteCustomMethodBody(codeElement, parentClass, writer); break; @@ -230,6 +233,26 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, } private const string DiscriminatorMappingVarName = "mappingValue"; + private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.OfKind(CodeParameterKind.ErrorMessage); + if (messageParam != null) + { + if (parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.AdditionalData)).Any() && !parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any()) + { + writer.WriteLine($"return {parentClass.Name}(message: {messageParam.Name}, additionalData: {{}});"); + } + else + { + writer.WriteLine($"return {parentClass.Name}(message: {messageParam.Name});"); + } + } + else + { + writer.WriteLine($"return {parentClass.Name}();"); + } + } + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); @@ -291,7 +314,7 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho { if (parentClass.IsErrorDefinition) { - WriteErrorClassConstructor(parentClass, writer); + WriteErrorClassConstructor(parentClass, currentMethod, writer); } else { @@ -344,12 +367,26 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho } } - private void WriteErrorClassConstructor(CodeClass parentClass, LanguageWriter writer) + private void WriteErrorClassConstructor(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer) { - foreach (string prop in DartConventionService.ErrorClassProperties) + // Check if this constructor has a message parameter + var hasMessageParameter = currentMethod.Parameters.Any(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase) && p.Type.Name.Equals("String", StringComparison.OrdinalIgnoreCase)); + + // For error class inheritance, pass message to super if present + if (hasMessageParameter) + { + writer.WriteLine("super(message: message),"); + } + else + { + writer.WriteLine("super(),"); + } + + foreach (string prop in DartConventionService.ErrorClassProperties.Where(p => !p.Equals("message", StringComparison.OrdinalIgnoreCase))) { writer.WriteLine($"super.{prop},"); } + if (!parentClass.Properties.Where(static x => x.IsOfKind(CodePropertyKind.BackingStore)).Any()) { foreach (CodeProperty prop in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom, CodePropertyKind.AdditionalData)) @@ -489,7 +526,21 @@ protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams re writer.StartBlock($"final {errorMappingVarName} = >{{"); foreach (var errorMapping in codeElement.ErrorMappings.Where(errorMapping => errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass)) { - writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : {conventions.GetTypeString(errorMapping.Value, codeElement, false)}.createFromDiscriminatorValue,"); + var errorClass = errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition as CodeClass; + var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); + + if (errorClass?.IsErrorDefinition == true) + { + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + var statusCodeAndDescription = !string.IsNullOrEmpty(errorDescription) + ? $"{errorMapping.Key} {errorDescription}" + : errorMapping.Key; + writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : (parseNode) => {typeName}.createFromDiscriminatorValueWithMessage(parseNode, '{statusCodeAndDescription}'),"); + } + else + { + writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : {typeName}.createFromDiscriminatorValue,"); + } } writer.CloseBlock("};"); } diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index 479f7e01a3..e3ef466bf5 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -82,6 +82,9 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri case CodeMethodKind.Factory: WriteFactoryMethodBody(codeElement, parentClass, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + break; case CodeMethodKind.ComposedTypeMarker: WriteComposedTypeMarkerBody(writer); break; @@ -115,9 +118,16 @@ private void WriteComposedTypeMarkerBody(LanguageWriter writer) { writer.WriteLine("return true"); } + private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return New{parentClass.Name}WithMessage({messageParam.Name}), nil"); + } + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); + if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForUnionType || parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) writer.WriteLine($"{ResultVarName} := New{parentClass.Name.ToFirstCharacterUpperCase()}()"); if (parentClass.DiscriminatorInformation.ShouldWriteParseNodeCheck) @@ -444,7 +454,14 @@ private void WriteMethodPrototype(CodeMethod code, CodeElement parentBlock, Lang var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); var methodName = code.Kind switch { - CodeMethodKind.Constructor when parentBlock is CodeClass parentClass && parentClass.IsOfKind(CodeClassKind.RequestBuilder) => $"New{parentClass.Name.ToFirstCharacterUpperCase()}Internal", // internal instantiation with url template parameters + // Error class constructors: generate names based on parameters + CodeMethodKind.Constructor when parentBlock is CodeClass { IsErrorDefinition: true } errorClass && code.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage)) + => $"New{errorClass.Name.ToFirstCharacterUpperCase()}WithMessage", + CodeMethodKind.Constructor when parentBlock is CodeClass { IsErrorDefinition: true } + => $"New{parentBlock.Name.ToFirstCharacterUpperCase()}", + // RequestBuilder constructors with parameters + CodeMethodKind.Constructor when parentBlock is CodeClass parentClass && parentClass.IsOfKind(CodeClassKind.RequestBuilder) + => $"New{parentClass.Name.ToFirstCharacterUpperCase()}Internal", CodeMethodKind.Factory => $"Create{parentBlock.Name.ToFirstCharacterUpperCase()}FromDiscriminatorValue", _ when isConstructor => $"New{parentBlock.Name.ToFirstCharacterUpperCase()}", _ when code.Access == AccessModifier.Public => code.Name.ToFirstCharacterUpperCase(), @@ -486,27 +503,27 @@ private void WriteGetterBody(CodeMethod codeElement, LanguageWriter writer, Code !(codeElement.AccessedProperty.Type?.IsNullable ?? true) && !codeElement.AccessedProperty.ReadOnly && !string.IsNullOrEmpty(codeElement.AccessedProperty.DefaultValue)) - { - writer.WriteLines( - $"val , err := m.{backingStore.NamePrefix}{backingStore.Name.ToFirstCharacterLowerCase()}.Get(\"{codeElement.AccessedProperty.Name.ToFirstCharacterLowerCase()}\")"); - writer.WriteBlock("if err != nil {", "}", "panic(err)"); - writer.WriteBlock("if val == nil {", "}", - $"var value = {codeElement.AccessedProperty.DefaultValue};", - $"m.Set{codeElement.AccessedProperty.Name?.ToFirstCharacterUpperCase()}(value);"); + { + writer.WriteLines( + $"val , err := m.{backingStore.NamePrefix}{backingStore.Name.ToFirstCharacterLowerCase()}.Get(\"{codeElement.AccessedProperty.Name.ToFirstCharacterLowerCase()}\")"); + writer.WriteBlock("if err != nil {", "}", "panic(err)"); + writer.WriteBlock("if val == nil {", "}", + $"var value = {codeElement.AccessedProperty.DefaultValue};", + $"m.Set{codeElement.AccessedProperty.Name?.ToFirstCharacterUpperCase()}(value);"); - writer.WriteLine($"return val.({conventions.GetTypeString(codeElement.AccessedProperty.Type, parentClass)})"); - } - else - { - var returnType = conventions.GetTypeString(codeElement.ReturnType, parentClass); + writer.WriteLine($"return val.({conventions.GetTypeString(codeElement.AccessedProperty.Type, parentClass)})"); + } + else + { + var returnType = conventions.GetTypeString(codeElement.ReturnType, parentClass); - writer.WriteLine($"val, err := m.Get{backingStore.Name.ToFirstCharacterUpperCase()}().Get(\"{codeElement.AccessedProperty?.Name?.ToFirstCharacterLowerCase()}\")"); + writer.WriteLine($"val, err := m.Get{backingStore.Name.ToFirstCharacterUpperCase()}().Get(\"{codeElement.AccessedProperty?.Name?.ToFirstCharacterLowerCase()}\")"); - writer.WriteBlock("if err != nil {", "}", "panic(err)"); - writer.WriteBlock("if val != nil {", "}", $"return val.({returnType})"); + writer.WriteBlock("if err != nil {", "}", "panic(err)"); + writer.WriteBlock("if val != nil {", "}", $"return val.({returnType})"); - writer.WriteLine("return nil"); - } + writer.WriteLine("return nil"); + } } private void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) { @@ -560,6 +577,17 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho writer.DecreaseIndent(); } writer.CloseBlock(decreaseIndent: false); + + // Handle error message parameter for error classes - set the Message field on the parent ApiError + if (parentClass.IsErrorDefinition && currentMethod.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) is CodeParameter messageParam) + { + var parentClassName = parentClass.StartBlock.Inherits?.Name.ToFirstCharacterUpperCase(); + if (!string.IsNullOrEmpty(parentClassName)) + { + writer.WriteLine($"m.{parentClassName}.Message = *{messageParam.Name.ToFirstCharacterLowerCase()}"); + } + } + foreach (var propWithDefault in parentClass.GetPropertiesOfKind(CodePropertyKind.BackingStore, CodePropertyKind.RequestBuilder) .Where(static x => !string.IsNullOrEmpty(x.DefaultValue)) @@ -800,7 +828,18 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.IncreaseIndent(); foreach (var errorMapping in codeElement.ErrorMappings) { - writer.WriteLine($"\"{errorMapping.Key.ToUpperInvariant()}\": {conventions.GetImportedStaticMethodName(errorMapping.Value, parentClass, "Create", "FromDiscriminatorValue", "able")},"); + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; + var errorKey = errorMapping.Key.ToUpperInvariant(); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + { + writer.WriteLine($"\"{errorKey}\": func(parseNode {conventions.SerializationHash}.ParseNode) error {{ return {conventions.GetTypeString(errorMapping.Value, parentClass, false, false, false)}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription}\") }},"); + } + else + { + writer.WriteLine($"\"{errorKey}\": {conventions.GetImportedStaticMethodName(errorMapping.Value, parentClass, "Create", "FromDiscriminatorValue", "able")},"); + } } writer.CloseBlock(); } diff --git a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs index ca9385f3db..a35ed1ecaf 100644 --- a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs @@ -84,6 +84,9 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri case CodeMethodKind.Factory when codeElement.IsOverload: WriteFactoryOverloadMethod(codeElement, parentClass, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + break; case CodeMethodKind.ErrorMessageOverride: WriteErrorMethodOverride(parentClass, writer); break; @@ -116,9 +119,16 @@ private void WriteRawUrlBuilderBody(CodeClass parentClass, CodeMethod codeElemen writer.WriteLine($"return new {parentClass.Name}({rawUrlParameter.Name}, {requestAdapterProperty.Name});"); } private const string ResultVarName = "result"; + private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return new {parentClass.Name}({messageParam.Name});"); + } + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); + if (parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForUnionType || parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType) writer.WriteLine($"final {parentClass.Name} {ResultVarName} = new {parentClass.Name}();"); var writeDiscriminatorValueRead = parentClass.DiscriminatorInformation.ShouldWriteParseNodeCheck && !parentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForIntersectionType; @@ -325,6 +335,7 @@ private static void WriteSerializationRegistration(HashSet serialization private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer, bool inherits) { if (inherits) + { if (parentClass.IsOfKind(CodeClassKind.RequestBuilder) && currentMethod.Parameters.OfKind(CodeParameterKind.RequestAdapter) is CodeParameter requestAdapterParameter && parentClass.Properties.FirstOrDefaultOfKind(CodePropertyKind.UrlTemplate) is CodeProperty urlTemplateProperty && @@ -339,6 +350,13 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho } else writer.WriteLine("super();"); + } + // For error classes with message constructor, pass the message to base constructor + else if (parentClass.IsErrorDefinition && + currentMethod.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage))) + { + writer.WriteLine("super(message);"); + } foreach (var propWithDefault in parentClass.GetPropertiesOfKind(CodePropertyKind.BackingStore, CodePropertyKind.RequestBuilder, CodePropertyKind.PathParameters) @@ -527,7 +545,15 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.WriteLine($"final HashMap> {errorMappingVarName} = new HashMap>();"); foreach (var errorMapping in codeElement.ErrorMappings) { - writer.WriteLine($"{errorMappingVarName}.put(\"{errorMapping.Key.ToUpperInvariant()}\", {errorMapping.Value.Name}::{FactoryMethodName});"); + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass codeClass)) continue; + var typeName = errorMapping.Value.Name; + var errorKey = errorMapping.Key.ToUpperInvariant(); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && codeClass.IsErrorDefinition) + writer.WriteLine($"{errorMappingVarName}.put(\"{errorKey}\", (parseNode) -> {typeName}.createFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription}\"));"); + else + writer.WriteLine($"{errorMappingVarName}.put(\"{errorKey}\", {typeName}::{FactoryMethodName});"); } } var factoryParameter = GetSendRequestFactoryParam(returnType, codeElement.ReturnType.AllTypes.First().TypeDefinition is CodeEnum); diff --git a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs index 1270a62541..317e6c3239 100644 --- a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs @@ -79,6 +79,9 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri case CodeMethodKind.Factory: WriteFactoryMethodBody(codeElement, parentClass, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + break; } writer.CloseBlock(); writer.WriteLine(); @@ -122,6 +125,7 @@ private static void WriteConstructorParentCall(CodeClass parentClass, CodeMethod var requestHeadersParameter = currentMethod.Parameters.OfKind(CodeParameterKind.Headers); var pathParametersProperty = parentClass.Properties.FirstOrDefaultOfKind(CodePropertyKind.PathParameters); var urlTemplateProperty = parentClass.Properties.FirstOrDefaultOfKind(CodePropertyKind.UrlTemplate); + var messageParameter = currentMethod.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)); if (parentClass.IsOfKind(CodeClassKind.RequestBuilder)) { @@ -129,6 +133,11 @@ private static void WriteConstructorParentCall(CodeClass parentClass, CodeMethod } else if (parentClass.IsOfKind(CodeClassKind.RequestConfiguration)) writer.WriteLine($"parent::__construct(${(requestHeadersParameter?.Name ?? "headers")} ?? [], ${(requestOptionParameter?.Name ?? "options")} ?? []);"); + else if (parentClass.IsErrorDefinition && messageParameter != null) + { + // For error classes with optional message parameter, pass it to parent only if provided + writer.WriteLine($"parent::__construct(${messageParameter.Name} ?? '');"); + } else writer.WriteLine("parent::__construct();"); @@ -771,7 +780,18 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, CodeClass parentCl writer.IncreaseIndent(2); errorMappings.ToList().ForEach(errorMapping => { - writer.WriteLine($"'{errorMapping.Key}' => [{errorMapping.Value.Name.ToFirstCharacterUpperCase()}::class, '{CreateDiscriminatorMethodName}'],"); + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) return; + var typeName = errorMapping.Value.Name.ToFirstCharacterUpperCase(); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + { + writer.WriteLine($"'{errorMapping.Key}' => function($parseNode) {{ return {typeName}::createFromDiscriminatorValueWithMessage($parseNode, '{errorDescription}'); }},"); + } + else + { + writer.WriteLine($"'{errorMapping.Key}' => [{typeName}::class, '{CreateDiscriminatorMethodName}'],"); + } }); writer.DecreaseIndent(); writer.WriteLine("];"); @@ -848,6 +868,12 @@ protected string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCol private const string DiscriminatorMappingVarName = "$mappingValue"; private const string ResultVarName = "$result"; + private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return new {parentClass.Name.ToFirstCharacterUpperCase()}(${messageParam.Name});"); + } + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { switch (parentClass.Kind) diff --git a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs index 01b1fbcbe4..44a482d9df 100644 --- a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs @@ -102,6 +102,10 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri WriteFactoryMethodBody(codeElement, parentClass, writer); writer.CloseBlock(string.Empty); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + writer.CloseBlock(string.Empty); + break; case CodeMethodKind.ComposedTypeMarker: throw new InvalidOperationException("ComposedTypeMarker is not required as interface is explicitly implemented."); case CodeMethodKind.RawUrlConstructor: @@ -207,6 +211,12 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, } writer.WriteLine($"return {ResultVarName}"); } + private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return {parentClass.Name}(message)"); + } + private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); @@ -329,7 +339,14 @@ private CodePropertyKind[] SetterAccessProperties } private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer, bool inherits) { - if (inherits && !parentClass.IsOfKind(CodeClassKind.Model)) + // For error classes with message constructor, pass the message to base constructor + if (parentClass.IsErrorDefinition && + currentMethod.Parameters.Any(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase) && + p.Type.Name.Equals("str", StringComparison.OrdinalIgnoreCase))) + { + writer.WriteLine("super().__init__(message)"); + } + else if (inherits && !parentClass.IsOfKind(CodeClassKind.Model)) { if (parentClass.IsOfKind(CodeClassKind.RequestBuilder) && currentMethod.Parameters.OfKind(CodeParameterKind.RequestAdapter) is CodeParameter requestAdapterParameter && @@ -359,6 +376,11 @@ private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMetho else writer.WriteLine("super().__init__()"); } + else if (parentClass.IsErrorDefinition) + { + // For error classes without message constructor, call super without arguments + writer.WriteLine("super().__init__()"); + } if (parentClass.IsOfKind(CodeClassKind.Model)) { writer.DecreaseIndent(); @@ -553,7 +575,6 @@ private void WriteDeserializerBodyForIntersectionModel(CodeClass parentClass, La } private void WriteDeserializerBodyForInheritedModel(bool inherits, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { - _codeUsingWriter.WriteInternalImports(parentClass, writer); writer.StartBlock($"fields: dict[str, Callable[[Any], {NoneKeyword}]] = {{"); foreach (var otherProp in parentClass .GetPropertiesOfKind(CodePropertyKind.Custom) @@ -603,7 +624,19 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.StartBlock($"{errorMappingVarName}: dict[str, type[ParsableFactory]] = {{"); foreach (var errorMapping in codeElement.ErrorMappings) { - writer.WriteLine($"\"{errorMapping.Key.ToUpperInvariant()}\": {errorMapping.Value.Name},"); + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; + var typeName = errorMapping.Value.Name; + var errorKey = errorMapping.Key.ToUpperInvariant(); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + { + writer.WriteLine($"\"{errorKey}\": lambda parse_node: {typeName}.create_from_discriminator_value_with_message(parse_node, \"{errorDescription}\"),"); + } + else + { + writer.WriteLine($"\"{errorKey}\": {typeName},"); + } } writer.CloseBlock(); } diff --git a/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs index d74c6c71a2..e02c466c97 100644 --- a/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs @@ -68,6 +68,9 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri case CodeMethodKind.Factory: WriteFactoryMethodBody(codeElement, parentClass, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodBodyForErrorClassWithMessage(codeElement, parentClass, writer); + break; case CodeMethodKind.RequestBuilderBackwardCompatibility: throw new InvalidOperationException("RequestBuilderBackwardCompatibility is not supported as the request builders are implemented by properties."); default: @@ -84,6 +87,13 @@ private void WriteRawUrlBuilderBody(CodeClass parentClass, CodeMethod codeElemen } private const string DiscriminatorMappingVarName = "mapping_value"; private const string NodeVarName = "mapping_value_node"; + + private static void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) + { + var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); + writer.WriteLine($"return {parentClass.Name.ToFirstCharacterUpperCase()}.new({messageParam.Name.ToSnakeCase()})"); + } + private static void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); @@ -272,7 +282,19 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.WriteLine($"{errorMappingVarName} = Hash.new"); foreach (var errorMapping in codeElement.ErrorMappings) { - writer.WriteLine($"{errorMappingVarName}[\"{errorMapping.Key.ToUpperInvariant()}\"] = {getDeserializationLambda(errorMapping.Value)}"); + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; + var typeName = errorMapping.Value.Name; + var errorKey = errorMapping.Key.ToUpperInvariant(); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + { + writer.WriteLine($"{errorMappingVarName}[\"{errorKey}\"] = lambda {{|parse_node| {typeName}.create_from_discriminator_value_with_message(parse_node, \"{errorDescription}\")}}"); + } + else + { + writer.WriteLine($"{errorMappingVarName}[\"{errorKey}\"] = {getDeserializationLambda(errorMapping.Value)}"); + } } } writer.WriteLine($"return @request_adapter.{genericTypeForSendMethod}(request_info, {returnType}, {errorMappingVarName})"); diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs index c9e7b90453..b0570e8426 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs @@ -116,7 +116,19 @@ private void WriteRequestsMetadataConstant(CodeConstant codeElement, LanguageWri writer.StartBlock("errorMappings: {"); foreach (var errorMapping in executorMethod.ErrorMappings) { - writer.WriteLine($"{GetErrorMappingKey(errorMapping.Key)}: {GetFactoryMethodName(errorMapping.Value, codeElement, writer)} as ParsableFactory,"); + if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; + var errorKey = GetErrorMappingKey(errorMapping.Key); + var errorDescription = executorMethod.GetErrorDescription(errorMapping.Key); + + if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + { + var enhancedFactoryMethodName = GetFactoryMethodName(errorMapping.Value, codeElement, writer).Replace("FromDiscriminatorValue", "FromDiscriminatorValueWithMessage", StringComparison.Ordinal); + writer.WriteLine($"{errorKey}: (parseNode: ParseNode) => {enhancedFactoryMethodName}(parseNode, \"{errorDescription}\") as ParsableFactory,"); + } + else + { + writer.WriteLine($"{errorKey}: {GetFactoryMethodName(errorMapping.Value, codeElement, writer)} as ParsableFactory,"); + } } writer.CloseBlock("},"); } diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs index 72aa88dfcf..df30ae0370 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs @@ -120,6 +120,9 @@ public override void WriteCodeElement(CodeFunction codeElement, LanguageWriter w case CodeMethodKind.Factory: WriteFactoryMethod(codeElement, codeFile, writer); break; + case CodeMethodKind.FactoryWithErrorMessage: + WriteFactoryMethodForErrorClassWithMessage(codeElement, writer); + break; case CodeMethodKind.ClientConstructor: WriteApiConstructorBody(parentFile, codeMethod, writer); break; @@ -332,6 +335,19 @@ private static void WriteSerializationRegistration(HashSet serialization writer.WriteLine($"{objectName}.{methodName}({module}{(additionalParam != null && additionalParam.Length > 0 ? ", " + string.Join(", ", additionalParam) : string.Empty)});"); } + private void WriteFactoryMethodForErrorClassWithMessage(CodeFunction codeElement, LanguageWriter writer) + { + var messageParam = codeElement.OriginalLocalMethod.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)); + if (messageParam != null) + { + writer.WriteLine($"return new {codeElement.OriginalMethodParentClass.Name.ToFirstCharacterUpperCase()}({messageParam.Name});"); + } + else + { + writer.WriteLine($"return new {codeElement.OriginalMethodParentClass.Name.ToFirstCharacterUpperCase()}();"); + } + } + private void WriteFactoryMethod(CodeFunction codeElement, CodeFile codeFile, LanguageWriter writer) { var returnType = conventions.GetTypeString(codeElement.OriginalLocalMethod.ReturnType, codeElement); diff --git a/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs b/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs index ddfe68903e..f2daab2dd8 100644 --- a/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs +++ b/tests/Kiota.Builder.IntegrationTests/GenerateSample.cs @@ -121,6 +121,7 @@ public async Task GeneratesErrorsInliningParentsAsync(GenerationLanguage languag Language = language, OpenAPIFilePath = GetAbsolutePath("InheritingErrors.yaml"), OutputPath = $".\\Generated\\ErrorInlineParents\\{language}", + CleanOutput = true }; await new KiotaBuilder(logger, configuration, _httpClient).GenerateClientAsync(new()); } diff --git a/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs index fd9dcc6be2..c27f2481ec 100644 --- a/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/DartLanguageRefinerTests.cs @@ -474,5 +474,126 @@ public async Task DoesntOverwriteSerializationNameIfAlreadySet() Assert.Equal("customType", model.Properties.First().Name); Assert.Equal("\\$type", model.Properties.First().WireName); } + + [Fact] + public async Task AddsConstructorsForErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + + // Then + var parameterlessConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + + Assert.NotNull(parameterlessConstructor); + Assert.Equal("constructor", parameterlessConstructor.Name); + Assert.Equal(AccessModifier.Public, parameterlessConstructor.Access); + + var messageConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("String", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.NotNull(messageConstructor); + Assert.Single(messageConstructor.Parameters); + Assert.Equal("message", messageConstructor.Parameters.First().Name); + Assert.Equal("String", messageConstructor.Parameters.First().Type.Name); + Assert.False(messageConstructor.Parameters.First().Optional); + } + + [Fact] + public async Task DoesNotAddConstructorsToNonErrorClasses() + { + // Given + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularModel", + IsErrorDefinition = false + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + + // Then + var messageConstructor = regularClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("String", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.Null(messageConstructor); + } + + [Fact] + public async Task AddsMessageFactoryMethodToErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + + // Then + var messageFactoryMethod = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage) && + m.Name.Equals("createFromDiscriminatorValueWithMessage", StringComparison.OrdinalIgnoreCase)); + + Assert.NotNull(messageFactoryMethod); + Assert.Equal(2, messageFactoryMethod.Parameters.Count()); + + var parseNodeParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("parseNode", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(parseNodeParam); + Assert.Equal("ParseNode", parseNodeParam.Type.Name); + + var messageParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(messageParam); + Assert.Equal("String", messageParam.Type.Name); + + Assert.True(messageFactoryMethod.IsStatic); + Assert.Equal(AccessModifier.Public, messageFactoryMethod.Access); + } + + [Fact] + public async Task DoesNotDuplicateExistingConstructors() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // Add an existing message constructor + var existingConstructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }; + existingConstructor.AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "String", IsExternal = true } + }); + errorClass.AddMethod(existingConstructor); + + var initialConstructorCount = errorClass.Methods.Count(m => m.IsOfKind(CodeMethodKind.Constructor)); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Dart }, root); + + // Then - should add only the parameterless constructor, not duplicate the message one + var finalConstructorCount = errorClass.Methods.Count(m => m.IsOfKind(CodeMethodKind.Constructor)); + Assert.Equal(initialConstructorCount + 1, finalConstructorCount); // Only parameterless constructor added + } #endregion } diff --git a/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs index 652e1580b8..4312e3a986 100644 --- a/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs @@ -1344,6 +1344,106 @@ public async Task ImportsStrConvForRelevantTypesOnly(string pathParameterType, b await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); Assert.Equal(isImported, model.StartBlock.Usings.Any(static x => x.Declaration.Name.Equals("strconv", StringComparison.OrdinalIgnoreCase))); } + [Fact] + public async Task AddsConstructorsForErrorClassesAsync() + { + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + root.AddNamespace("ApiSdk/models"); // so the interface copy refiner goes through + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + + // Check that the factory methods were created for error classes + var parameterlessConstructor = errorClass.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + var messageConstructor = errorClass.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && m.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))); + var discriminatorMessageFactory = errorClass.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + + Assert.NotNull(parameterlessConstructor); + Assert.NotNull(messageConstructor); + Assert.NotNull(discriminatorMessageFactory); + + // Check parameter counts + Assert.Single(messageConstructor.Parameters); + Assert.Equal(2, discriminatorMessageFactory.Parameters.Count()); + } + + [Fact] + public async Task DoesNotAddConstructorsForNonErrorClassesAsync() + { + var regularClass = root.AddClass(new CodeClass + { + Name = "SomeModel", + Kind = CodeClassKind.Model, + IsErrorDefinition = false + }).First(); + + root.AddNamespace("ApiSdk/models"); // so the interface copy refiner goes through + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + + // Should not have error-specific factory methods + Assert.DoesNotContain(regularClass.Methods, m => m.Name == "NewSomeModel" && m.IsOfKind(CodeMethodKind.Factory)); + Assert.DoesNotContain(regularClass.Methods, m => m.Name == "NewSomeModelWithMessage" && m.IsOfKind(CodeMethodKind.Factory)); + Assert.DoesNotContain(regularClass.Methods, m => m.Name == "CreateFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + } + + [Fact] + public async Task AddsConstructorsOnlyOnceForErrorClassesAsync() + { + var errorClass = root.AddClass(new CodeClass + { + Name = "DuplicateError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Add existing factory method to simulate already having one + errorClass.AddMethod(new CodeMethod + { + Name = "NewDuplicateError", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType { Name = "*DuplicateError", IsNullable = true } + }); + + root.AddNamespace("ApiSdk/models"); // so the interface copy refiner goes through + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + + // Should have only one of each factory method + Assert.Single(errorClass.Methods, m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + Assert.Single(errorClass.Methods, m => m.IsOfKind(CodeMethodKind.Constructor) && m.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))); + Assert.Single(errorClass.Methods, m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + } + + [Fact] + public async Task AddsConstructorsWithCorrectDescriptionForErrorClassesAsync() + { + var errorClassWithDescription = root.AddClass(new CodeClass + { + Name = "DetailedError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + root.AddNamespace("ApiSdk/models"); // so the interface copy refiner goes through + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + + // Check factory methods are created + var parameterlessConstructor = errorClassWithDescription.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + Assert.NotNull(parameterlessConstructor); + Assert.NotEmpty(parameterlessConstructor.Documentation.DescriptionTemplate); + + var messageConstructor = errorClassWithDescription.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && m.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage))); + Assert.NotNull(messageConstructor); + Assert.NotEmpty(messageConstructor.Documentation.DescriptionTemplate); + + var discriminatorMessageFactory = errorClassWithDescription.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + Assert.NotNull(discriminatorMessageFactory); + Assert.NotEmpty(discriminatorMessageFactory.Documentation.DescriptionTemplate); + } + [Fact] public async Task EscapesReservedKeywordsInMethodParametersAsync() { diff --git a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs index ec23487015..87f91b5861 100644 --- a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs @@ -768,5 +768,126 @@ public async Task AddsUsingForUntypedNodeInMethodParameterAsync() Assert.Single(nodeUsing); Assert.Equal("com.microsoft.kiota.serialization", nodeUsing[0].Declaration.Name); } + + [Fact] + public async Task AddsConstructorsForErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + + // Then + var parameterlessConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + + Assert.NotNull(parameterlessConstructor); + Assert.Equal("constructor", parameterlessConstructor.Name); + Assert.Equal(AccessModifier.Public, parameterlessConstructor.Access); + + var messageConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("String", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.NotNull(messageConstructor); + Assert.Single(messageConstructor.Parameters); + Assert.Equal("message", messageConstructor.Parameters.First().Name); + Assert.Equal("String", messageConstructor.Parameters.First().Type.Name); + Assert.False(messageConstructor.Parameters.First().Optional); + } + + [Fact] + public async Task DoesNotAddConstructorsToNonErrorClasses() + { + // Given + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularModel", + IsErrorDefinition = false + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + + // Then + var messageConstructor = regularClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("String", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.Null(messageConstructor); + } + + [Fact] + public async Task AddsMessageFactoryMethodToErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + + // Then + var messageFactoryMethod = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage) && + m.Name.Equals("createFromDiscriminatorValueWithMessage", StringComparison.OrdinalIgnoreCase)); + + Assert.NotNull(messageFactoryMethod); + Assert.Equal(2, messageFactoryMethod.Parameters.Count()); + + var parseNodeParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("parseNode", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(parseNodeParam); + Assert.Equal("ParseNode", parseNodeParam.Type.Name); + + var messageParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(messageParam); + Assert.Equal("String", messageParam.Type.Name); + + Assert.True(messageFactoryMethod.IsStatic); + Assert.Equal(AccessModifier.Public, messageFactoryMethod.Access); + } + + [Fact] + public async Task DoesNotDuplicateExistingConstructors() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // Add an existing message constructor + var existingConstructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }; + existingConstructor.AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "String", IsExternal = true } + }); + errorClass.AddMethod(existingConstructor); + + var initialConstructorCount = errorClass.Methods.Count(m => m.IsOfKind(CodeMethodKind.Constructor)); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + + // Then - should add only the parameterless constructor, not duplicate the message one + var finalConstructorCount = errorClass.Methods.Count(m => m.IsOfKind(CodeMethodKind.Constructor)); + Assert.Equal(initialConstructorCount + 1, finalConstructorCount); // Only parameterless constructor added + } #endregion } diff --git a/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs index 7651e1d416..ee2ac38596 100644 --- a/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/PhpLanguageRefinerTests.cs @@ -299,4 +299,106 @@ public async Task DoesNotCreateDuplicateComposedTypeWrapperIfOneAlreadyExistsAsy Assert.True(root.FindChildByName("Union", false) is CodeClass unionTypeWrapper && unionTypeWrapper.OriginalComposedType != null); Assert.True(root.FindChildByName("UnionWrapper", false) is null); } + + [Fact] + public async Task AddsConstructorsForErrorClassesAsync() + { + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.PHP }, root); + + // PHP only allows one __construct method, so check for single constructor with optional message parameter + var constructor = errorClass.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor)); + Assert.NotNull(constructor); + Assert.Single(constructor.Parameters); + var constructorMessageParam = constructor.Parameters.First(); + Assert.Equal("message", constructorMessageParam.Name); + Assert.Equal("string", constructorMessageParam.Type.Name); + Assert.True(constructorMessageParam.Optional); + + // Check for CreateFromDiscriminatorValueWithMessage factory method + var discriminatorMessageFactory = errorClass.Methods.FirstOrDefault(m => + m.Name == "createFromDiscriminatorValueWithMessage" && + m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + Assert.NotNull(discriminatorMessageFactory); + Assert.Equal(2, discriminatorMessageFactory.Parameters.Count()); + var parseNodeParam = discriminatorMessageFactory.Parameters.FirstOrDefault(p => p.Name == "parseNode"); + var factoryMessageParam = discriminatorMessageFactory.Parameters.FirstOrDefault(p => p.Name == "message"); + Assert.NotNull(parseNodeParam); + Assert.NotNull(factoryMessageParam); + Assert.Equal("string", factoryMessageParam.Type.Name); + } + + [Fact] + public async Task DoesNotAddConstructorsForNonErrorClassesAsync() + { + var regularClass = root.AddClass(new CodeClass + { + Name = "SomeModel", + Kind = CodeClassKind.Model, + IsErrorDefinition = false + }).First(); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.PHP }, root); + + // Should not have error-specific constructors or factory methods + Assert.DoesNotContain(regularClass.Methods, m => m.Name == "createFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + } + + [Fact] + public async Task AddsConstructorsOnlyOnceForErrorClassesAsync() + { + var errorClass = root.AddClass(new CodeClass + { + Name = "DuplicateError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Add existing constructor to simulate already having one + errorClass.AddMethod(new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.PHP }, root); + + // Should have only one constructor (with optional message parameter added to existing constructor) + Assert.Single(errorClass.Methods, m => m.IsOfKind(CodeMethodKind.Constructor)); + var constructor = errorClass.Methods.First(m => m.IsOfKind(CodeMethodKind.Constructor)); + Assert.Single(constructor.Parameters); // Should have message parameter added + Assert.True(constructor.Parameters.First().Optional); + Assert.Single(errorClass.Methods, m => m.Name == "createFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + } + + [Fact] + public async Task AddsConstructorsWithCorrectDescriptionForErrorClassesAsync() + { + var errorClassWithDescription = root.AddClass(new CodeClass + { + Name = "DetailedError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.PHP }, root); + + // Check single constructor with optional message parameter is created + var constructor = errorClassWithDescription.Methods.FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor)); + Assert.NotNull(constructor); + Assert.NotEmpty(constructor.Documentation.DescriptionTemplate); + Assert.Single(constructor.Parameters); + Assert.True(constructor.Parameters.First().Optional); + + var discriminatorMessageFactory = errorClassWithDescription.Methods.FirstOrDefault(m => m.Name == "createFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + Assert.NotNull(discriminatorMessageFactory); + Assert.NotEmpty(discriminatorMessageFactory.Documentation.DescriptionTemplate); + } } diff --git a/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs index f0635cfb4e..af3ebc45c8 100644 --- a/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/PythonLanguageRefinerTests.cs @@ -651,5 +651,129 @@ public async Task ReplacesUntypedNodeInMethodParameterAndReturnTypeAsync() Assert.Equal("bytes", method.Parameters.First().Type.Name);// type is renamed to use the stream type Assert.Equal("bytes", method.ReturnType.Name);// return type is renamed to use the stream type } + + [Fact] + public async Task AddsConstructorsForErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root); + + // Then + var parameterlessConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + + Assert.NotNull(parameterlessConstructor); + Assert.Equal("__init__", parameterlessConstructor.Name); + Assert.Equal(AccessModifier.Public, parameterlessConstructor.Access); + + var messageConstructor = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("str", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.NotNull(messageConstructor); + Assert.Single(messageConstructor.Parameters); + Assert.Equal("message", messageConstructor.Parameters.First().Name); + Assert.Equal("str", messageConstructor.Parameters.First().Type.Name); + Assert.True(messageConstructor.Parameters.First().Optional); + Assert.Equal("None", messageConstructor.Parameters.First().DefaultValue); + } + + [Fact] + public async Task DoesNotAddConstructorsToNonErrorClasses() + { + // Given + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularModel", + IsErrorDefinition = false + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root); + + // Then + var messageConstructor = regularClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Any(p => p.Type.Name.Equals("str", StringComparison.OrdinalIgnoreCase) && p.Name.Equals("message", StringComparison.OrdinalIgnoreCase))); + + Assert.Null(messageConstructor); + } + + [Fact] + public async Task AddsMessageFactoryMethodToErrorClasses() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root); + + // Then + var messageFactoryMethod = errorClass.Methods + .FirstOrDefault(m => m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage) && + m.Name.Equals("create_from_discriminator_value_with_message", StringComparison.OrdinalIgnoreCase)); + + Assert.NotNull(messageFactoryMethod); + Assert.Equal(2, messageFactoryMethod.Parameters.Count()); + + var parseNodeParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("parse_node", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(parseNodeParam); + Assert.Equal("ParseNode", parseNodeParam.Type.Name); + + var messageParam = messageFactoryMethod.Parameters.FirstOrDefault(p => p.Name.Equals("message", StringComparison.OrdinalIgnoreCase)); + Assert.NotNull(messageParam); + Assert.Equal("str", messageParam.Type.Name); + Assert.True(messageParam.Optional); + Assert.Equal("None", messageParam.DefaultValue); + + Assert.True(messageFactoryMethod.IsStatic); + Assert.Equal(AccessModifier.Public, messageFactoryMethod.Access); + } + + [Fact] + public async Task DoesNotDuplicateExistingConstructors() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + + // Add an existing message constructor + var existingConstructor = new CodeMethod + { + Name = "__init__", + Kind = CodeMethodKind.Constructor, + Access = AccessModifier.Public, + ReturnType = new CodeType { Name = "None", IsExternal = true } + }; + existingConstructor.AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "str", IsExternal = true } + }); + errorClass.AddMethod(existingConstructor); + + var initialConstructorCount = errorClass.Methods.Count(m => m.IsOfKind(CodeMethodKind.Constructor)); + + // When + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.Python }, root); + + // Then - should add only the parameterless constructor, not duplicate the message one + var finalConstructorCount = errorClass.Methods.Count(m => m.IsOfKind(CodeMethodKind.Constructor)); + Assert.Equal(initialConstructorCount + 1, finalConstructorCount); // Only parameterless constructor added + } #endregion } diff --git a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs index f0b68dd3a7..13de9906d3 100644 --- a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs @@ -1120,5 +1120,100 @@ public async Task AddsUsingForUntypedNodeInMethodParameterAsync() Assert.Single(nodeUsing); Assert.Equal("@microsoft/kiota-abstractions", nodeUsing[0].Declaration.Name); } + + [Fact] + public async Task AddsConstructorsForErrorClassesAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var errorClass = TestHelper.CreateModelClassInModelsNamespace(generationConfiguration, root, "SomeError"); + errorClass.IsErrorDefinition = true; + + await ILanguageRefiner.RefineAsync(generationConfiguration, root); + + // Check for parameterless constructor + var parameterlessConstructor = errorClass.Methods.FirstOrDefault(m => m.Name == "constructor" && m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + Assert.NotNull(parameterlessConstructor); + + // Check for constructor with message parameter + var messageConstructor = errorClass.Methods.FirstOrDefault(m => + m.Name == "constructor" && + m.IsOfKind(CodeMethodKind.Constructor) && + m.Parameters.Count() == 1 && + m.Parameters.First().Name == "message"); + Assert.NotNull(messageConstructor); + Assert.Equal("string", messageConstructor.Parameters.First().Type.Name); + Assert.True(messageConstructor.Parameters.First().Optional); + + // Check for createFromDiscriminatorValueWithMessage factory method + var discriminatorMessageFactory = errorClass.Methods.FirstOrDefault(m => + m.Name == "createFromDiscriminatorValueWithMessage" && + m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + Assert.NotNull(discriminatorMessageFactory); + Assert.Equal(2, discriminatorMessageFactory.Parameters.Count()); + var parseNodeParam = discriminatorMessageFactory.Parameters.FirstOrDefault(p => p.Name == "parseNode"); + var messageParam = discriminatorMessageFactory.Parameters.FirstOrDefault(p => p.Name == "message"); + Assert.NotNull(parseNodeParam); + Assert.NotNull(messageParam); + Assert.Equal("string", messageParam.Type.Name); + Assert.True(messageParam.Optional); + } + + [Fact] + public async Task DoesNotAddConstructorsForNonErrorClassesAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var regularClass = TestHelper.CreateModelClassInModelsNamespace(generationConfiguration, root, "SomeModel"); + + await ILanguageRefiner.RefineAsync(generationConfiguration, root); + + // Should not have error-specific constructors or factory methods + Assert.DoesNotContain(regularClass.Methods, m => m.Name == "createFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.Factory)); + } + + [Fact] + public async Task AddsConstructorsOnlyOnceForErrorClassesAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var errorClass = TestHelper.CreateModelClassInModelsNamespace(generationConfiguration, root, "DuplicateError"); + errorClass.IsErrorDefinition = true; + + // Add existing constructor to simulate already having one + errorClass.AddMethod(new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "void", IsExternal = true } + }); + + await ILanguageRefiner.RefineAsync(generationConfiguration, root); + + // Should have only one of each constructor/method + Assert.Single(errorClass.Methods, m => m.Name == "constructor" && m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + Assert.Single(errorClass.Methods, m => m.Name == "constructor" && m.IsOfKind(CodeMethodKind.Constructor) && m.Parameters.Count() == 1); + Assert.Single(errorClass.Methods, m => m.Name == "createFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + } + + [Fact] + public async Task AddsConstructorsWithCorrectDescriptionForErrorClassesAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var errorClassWithDescription = TestHelper.CreateModelClassInModelsNamespace(generationConfiguration, root, "DetailedError"); + errorClassWithDescription.IsErrorDefinition = true; + + await ILanguageRefiner.RefineAsync(generationConfiguration, root); + + // Check constructors/methods are created + var parameterlessConstructor = errorClassWithDescription.Methods.FirstOrDefault(m => m.Name == "constructor" && m.IsOfKind(CodeMethodKind.Constructor) && !m.Parameters.Any()); + Assert.NotNull(parameterlessConstructor); + Assert.NotEmpty(parameterlessConstructor.Documentation.DescriptionTemplate); + + var messageConstructor = errorClassWithDescription.Methods.FirstOrDefault(m => m.Name == "constructor" && m.IsOfKind(CodeMethodKind.Constructor) && m.Parameters.Count() == 1); + Assert.NotNull(messageConstructor); + Assert.NotEmpty(messageConstructor.Documentation.DescriptionTemplate); + + var discriminatorMessageFactory = errorClassWithDescription.Methods.FirstOrDefault(m => m.Name == "createFromDiscriminatorValueWithMessage" && m.IsOfKind(CodeMethodKind.FactoryWithErrorMessage)); + Assert.NotNull(discriminatorMessageFactory); + Assert.NotEmpty(discriminatorMessageFactory.Documentation.DescriptionTemplate); + } #endregion } diff --git a/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs index d4058d9483..9044b8767c 100644 --- a/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Dart/CodeMethodWriterTests.cs @@ -1620,4 +1620,171 @@ public void WritesRequestGeneratorContentTypeQuotes() var result = tw.ToString(); Assert.Contains("'application/json; profile=\"CamelCase\"'", result); } + + [Fact] + public void WritesRequestExecutorWithEnhancedErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + IsErrorDefinition = true + }).First(); + var error401 = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client Error"); + method.AddErrorMapping("401", new CodeType { Name = "Error401", TypeDefinition = error401 }, "Unauthorized"); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("final errorMapping = >{", result); + Assert.Contains("'4XX' : (parseNode) => Error4XX.createFromDiscriminatorValueWithMessage(parseNode, '4XX Client Error'),", result); + Assert.Contains("'401' : (parseNode) => Error401.createFromDiscriminatorValueWithMessage(parseNode, '401 Unauthorized'),", result); + } + + [Fact] + public void WritesRequestExecutorWithRegularErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularError", + IsErrorDefinition = false + }).First(); + method.AddErrorMapping("500", new CodeType { Name = "RegularError", TypeDefinition = regularClass }); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("final errorMapping = >{", result); + Assert.Contains("'500' : RegularError.createFromDiscriminatorValue,", result); + Assert.DoesNotContain("createFromDiscriminatorValueWithMessage", result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessage() + { + setup(); + parentClass.IsErrorDefinition = true; + method.Kind = CodeMethodKind.FactoryWithErrorMessage; + method.Name = "createFromDiscriminatorValueWithMessage"; + method.IsStatic = true; + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType { Name = "ParseNode", IsExternal = true } + }); + method.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType { Name = "String", IsExternal = true } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"return {parentClass.Name}(message: message);", result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessageAndAdditionalData() + { + setup(); + parentClass.IsErrorDefinition = true; + parentClass.AddProperty(new CodeProperty + { + Name = "additionalData", + Kind = CodePropertyKind.AdditionalData, + Type = new CodeType { Name = "Map", IsExternal = true } + }); + method.Kind = CodeMethodKind.FactoryWithErrorMessage; + method.Name = "createFromDiscriminatorValueWithMessage"; + method.IsStatic = true; + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType { Name = "ParseNode", IsExternal = true } + }); + method.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType { Name = "String", IsExternal = true } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"return {parentClass.Name}(message: message, additionalData: {{}});", result); + } + + [Fact] + public void WritesConstructorWithMessageInheritance() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "TestError", + IsErrorDefinition = true + }).First(); + + var constructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = errorClass + }; + constructor.AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "String", IsExternal = true } + }); + + parentClass = errorClass; + method = constructor; + + // When + writer.Write(method); + var result = tw.ToString(); + + // Then + Assert.Contains("super(message: message),", result); + } + + [Fact] + public void WritesConstructorWithoutMessageInheritance() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "TestError", + IsErrorDefinition = true + }).First(); + + var constructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = errorClass + }; + // No message parameter + + parentClass = errorClass; + method = constructor; + + // When + writer.Write(method); + var result = tw.ToString(); + + // Then + Assert.Contains("super(),", result); + Assert.DoesNotContain("super(message:", result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs index ca98e97656..dd477227fd 100644 --- a/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs @@ -2331,4 +2331,246 @@ public void WritesRequestGeneratorContentTypeQuotes() var result = tw.ToString(); Assert.Contains("\"application/json; profile=\\\"CamelCase\\\"\"", result); } + + [Fact] + public void WritesErrorMappingWithDescriptions() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + + // Create error classes with descriptions + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + var error5XX = root.AddClass(new CodeClass + { + Name = "Error5XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Add error mappings with descriptions + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client error response"); + method.AddErrorMapping("5XX", new CodeType { Name = "Error5XX", TypeDefinition = error5XX }, "Server error response"); + + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + + // Check for enhanced error mapping with descriptions + Assert.Contains($"errorMapping := {AbstractionsPackageHash}.ErrorMappings", result); + Assert.Contains("Client error response", result); + Assert.Contains("Server error response", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + + [Fact] + public void WritesErrorMappingWithoutDescriptions() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + + // Create error classes without descriptions + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Add error mapping without description + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }); + + // No error descriptions set up + + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + + // Should use original factory method when no description is provided + Assert.Contains($"errorMapping := {AbstractionsPackageHash}.ErrorMappings", result); + Assert.Contains("\"4XX\": CreateError4XXFromDiscriminatorValue", result); + Assert.DoesNotContain("CreateError4XXFromDiscriminatorValueWithMessage", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessage() + { + setup(); + + // Create an error class + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Create the factory method with message parameter + var factoryMethod = errorClass.AddMethod(new CodeMethod + { + Name = "CreateFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.FactoryWithErrorMessage, + ReturnType = new CodeType + { + Name = "*SomeError", + TypeDefinition = errorClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add parseNode parameter + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + // Add message parameter + factoryMethod.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType + { + Name = "string", + IsExternal = true, + }, + Optional = false, + }); + + parentClass = errorClass; // Set parentClass for the test + writer.Write(factoryMethod); + var result = tw.ToString(); + + // Should call the factory method with message + Assert.Contains("return NewSomeErrorWithMessage(message), nil", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithoutMessage() + { + setup(); + + // Create an error class + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Create the factory method without message parameter + var factoryMethod = errorClass.AddMethod(new CodeMethod + { + Name = "CreateFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "*SomeError", + TypeDefinition = errorClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add only parseNode parameter (no message parameter) + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + parentClass = errorClass; // Set parentClass for the test + writer.Write(factoryMethod); + var result = tw.ToString(); + + // Should call the factory method without message + Assert.Contains("return NewSomeError(), nil", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + + [Fact] + public void DoesNotWriteSpecialFactoryMethodForNonErrorClasses() + { + setup(); + + // Create a regular class + var regularClass = root.AddClass(new CodeClass + { + Name = "SomeModel", + Kind = CodeClassKind.Model, + IsErrorDefinition = false + }).First(); + + // Create the factory method + var factoryMethod = regularClass.AddMethod(new CodeMethod + { + Name = "CreateFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "*SomeModel", + TypeDefinition = regularClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add parameters + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + factoryMethod.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.RequestBodyContentType, + Type = new CodeType + { + Name = "string", + IsExternal = true, + }, + Optional = false, + }); + + parentClass = regularClass; + writer.Write(factoryMethod); + var result = tw.ToString(); + + // Should not use special error handling for non-error classes + Assert.DoesNotContain("NewSomeModelWithMessage", result); + // Should use normal factory method logic + Assert.Contains("return NewSomeModel(), nil", result); + AssertExtensions.CurlyBracesAreClosed(result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs index 5f21b81ff2..1f6d72f2d5 100644 --- a/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs @@ -2192,4 +2192,111 @@ public void WritesRequestGeneratorContentTypeQuotes() var result = tw.ToString(); Assert.Contains("\"application/json; profile=\\\"CamelCase\\\"\"", result); } + + [Fact] + public void WritesRequestExecutorWithEnhancedErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + IsErrorDefinition = true + }).First(); + var error401 = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client Error"); + method.AddErrorMapping("401", new CodeType { Name = "Error401", TypeDefinition = error401 }, "Unauthorized"); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("final HashMap> errorMapping", result); + Assert.Contains("(parseNode) -> Error4XX.createFromDiscriminatorValueWithMessage(parseNode, \"Client Error\")", result); + Assert.Contains("(parseNode) -> Error401.createFromDiscriminatorValueWithMessage(parseNode, \"Unauthorized\")", result); + Assert.Contains("send", result.ToLower()); + } + + [Fact] + public void WritesRequestExecutorWithRegularErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularError", + IsErrorDefinition = false + }).First(); + method.AddErrorMapping("500", new CodeType { Name = "RegularError", TypeDefinition = regularClass }); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("final HashMap> errorMapping", result); + Assert.Contains("RegularError::createFromDiscriminatorValue", result); + Assert.DoesNotContain("createFromDiscriminatorValueWithMessage", result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessage() + { + setup(); + parentClass.IsErrorDefinition = true; + method.Kind = CodeMethodKind.FactoryWithErrorMessage; + method.Name = "createFromDiscriminatorValueWithMessage"; + method.IsStatic = true; + method.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType { Name = "ParseNode", IsExternal = true } + }); + method.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType { Name = "String", IsExternal = true } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"return new {parentClass.Name}(message);", result); + } + + [Fact] + public void WritesConstructorWithMessageInheritance() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "TestError", + IsErrorDefinition = true + }).First(); + + var constructor = new CodeMethod + { + Name = "constructor", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "void", IsExternal = true }, + Parent = errorClass + }; + constructor.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType { Name = "String", IsExternal = true } + }); + + parentClass = errorClass; + method = constructor; + + // When + writer.Write(method); + var result = tw.ToString(); + + // Then + Assert.Contains("super(message);", result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs index fdf000c00e..1747e6451b 100644 --- a/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Php/CodeMethodWriterTests.cs @@ -2558,4 +2558,255 @@ public async Task WritesRequestGeneratorBodyForMultipartAsync() Assert.Contains("MultiPartBody $body", result); Assert.Contains("$requestInfo->setContentFromParsable($this->requestAdapter, \"multipart/form-data\", $body);", result); } + + [Fact] + public void WritesErrorMappingWithDescriptions() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + + // Add required properties for request executor + parentClass.AddProperty(new CodeProperty + { + Name = "requestAdapter", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType { Name = "RequestAdapter" } + }); + + // Create error classes + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + var error5XX = root.AddClass(new CodeClass + { + Name = "Error5XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Add error mappings with descriptions + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client error response"); + method.AddErrorMapping("5XX", new CodeType { Name = "Error5XX", TypeDefinition = error5XX }, "Server error response"); + + AddRequestBodyParameters(); + languageWriter.Write(method); + var result = stringWriter.ToString(); + + // Check for enhanced error mapping with descriptions and PHP function syntax + Assert.Contains("$errorMappings = [", result); + Assert.Contains("'4XX' => function($parseNode) { return Error4XX::createFromDiscriminatorValueWithMessage($parseNode, 'Client error response'); }", result); + Assert.Contains("'5XX' => function($parseNode) { return Error5XX::createFromDiscriminatorValueWithMessage($parseNode, 'Server error response'); }", result); + } + + [Fact] + public void WritesErrorMappingWithoutDescriptions() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + + // Add required properties for request executor + parentClass.AddProperty(new CodeProperty + { + Name = "requestAdapter", + Kind = CodePropertyKind.RequestAdapter, + Type = new CodeType { Name = "RequestAdapter" } + }); + + // Create error classes without descriptions + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Add error mapping without description + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }); + + AddRequestBodyParameters(); + languageWriter.Write(method); + var result = stringWriter.ToString(); + + // Should use original factory method when no description is provided + Assert.Contains("$errorMappings = [", result); + Assert.Contains("'4XX' => [Error4XX::class, 'createFromDiscriminatorValue']", result); + Assert.DoesNotContain("createFromDiscriminatorValueWithMessage", result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessage() + { + setup(); + + // Create an error class + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Create the factory method with message parameter + var factoryMethod = errorClass.AddMethod(new CodeMethod + { + Name = "createFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.FactoryWithErrorMessage, + ReturnType = new CodeType + { + Name = "SomeError", + TypeDefinition = errorClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add parseNode parameter + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + // Add message parameter + factoryMethod.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType + { + Name = "string", + IsExternal = true, + }, + Optional = false, + }); + + parentClass = errorClass; // Set parentClass for the test + languageWriter.Write(factoryMethod); + var result = stringWriter.ToString(); + + // Should call the constructor with message + Assert.Contains("return new SomeError($message);", result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithoutMessage() + { + setup(); + + // Create an error class + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Create the factory method without message parameter + var factoryMethod = errorClass.AddMethod(new CodeMethod + { + Name = "createFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "SomeError", + TypeDefinition = errorClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add only parseNode parameter (no message parameter) + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + parentClass = errorClass; // Set parentClass for the test + languageWriter.Write(factoryMethod); + var result = stringWriter.ToString(); + + // Should call the parameterless constructor + Assert.Contains("return new SomeError();", result); + } + + [Fact] + public void DoesNotWriteSpecialFactoryMethodForNonErrorClasses() + { + setup(); + + // Create a regular class + var regularClass = root.AddClass(new CodeClass + { + Name = "SomeModel", + Kind = CodeClassKind.Model, + IsErrorDefinition = false + }).First(); + + // Create the factory method + var factoryMethod = regularClass.AddMethod(new CodeMethod + { + Name = "createFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "SomeModel", + TypeDefinition = regularClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add parameters + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + factoryMethod.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.RequestBodyContentType, + Type = new CodeType + { + Name = "string", + IsExternal = true, + }, + Optional = false, + }); + + parentClass = regularClass; + languageWriter.Write(factoryMethod); + var result = stringWriter.ToString(); + + // Should not use special error handling for non-error classes + Assert.DoesNotContain("new SomeModel($message)", result); + // Should continue with normal factory method logic or use parameterless constructor + Assert.Contains("new SomeModel();", result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs index c59c16a6b5..51320eb6db 100644 --- a/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Python/CodeMethodWriterTests.cs @@ -2298,4 +2298,140 @@ public void WritesRequestGeneratorContentTypeQuotes() var result = tw.ToString(); Assert.Contains("\"application/json; profile=\\\"CamelCase\\\"\"", result); } + + [Fact] + public void WritesRequestExecutorWithEnhancedErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + IsErrorDefinition = true + }).First(); + var error401 = root.AddClass(new CodeClass + { + Name = "Error401", + IsErrorDefinition = true + }).First(); + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client Error"); + method.AddErrorMapping("401", new CodeType { Name = "Error401", TypeDefinition = error401 }, "Unauthorized"); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("error_mapping: dict[str, type[ParsableFactory]] = {", result); + Assert.Contains("\"4XX\": lambda parse_node: Error4XX.create_from_discriminator_value_with_message(parse_node, \"Client Error\"),", result); + Assert.Contains("\"401\": lambda parse_node: Error401.create_from_discriminator_value_with_message(parse_node, \"Unauthorized\"),", result); + } + + [Fact] + public void WritesRequestExecutorWithRegularErrorMapping() + { + setup(); + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var regularClass = root.AddClass(new CodeClass + { + Name = "RegularError", + IsErrorDefinition = false + }).First(); + method.AddErrorMapping("500", new CodeType { Name = "RegularError", TypeDefinition = regularClass }); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("error_mapping: dict[str, type[ParsableFactory]] = {", result); + Assert.Contains("\"500\": RegularError,", result); + Assert.DoesNotContain("create_from_discriminator_value_with_message", result); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessage() + { + setup(); + parentClass.IsErrorDefinition = true; + method.Kind = CodeMethodKind.FactoryWithErrorMessage; + method.Name = "create_from_discriminator_value_with_message"; + method.IsStatic = true; + method.AddParameter(new CodeParameter + { + Name = "parse_node", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType { Name = "ParseNode", IsExternal = true } + }); + method.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType { Name = "str", IsExternal = true } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"return {parentClass.Name}(message)", result); + } + + [Fact] + public void WritesConstructorWithMessageInheritance() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "TestError", + IsErrorDefinition = true + }).First(); + + var constructor = new CodeMethod + { + Name = "__init__", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "None", IsExternal = true }, + Parent = errorClass + }; + constructor.AddParameter(new CodeParameter + { + Name = "message", + Type = new CodeType { Name = "str", IsExternal = true } + }); + + parentClass = errorClass; + method = constructor; + + // When + writer.Write(method); + var result = tw.ToString(); + + // Then + Assert.Contains("super().__init__(message)", result); + } + + [Fact] + public void WritesConstructorWithoutMessageInheritance() + { + // Given + var errorClass = root.AddClass(new CodeClass + { + Name = "TestError", + IsErrorDefinition = true + }).First(); + + var constructor = new CodeMethod + { + Name = "__init__", + Kind = CodeMethodKind.Constructor, + ReturnType = new CodeType { Name = "None", IsExternal = true }, + Parent = errorClass + }; + // No message parameter + + parentClass = errorClass; + method = constructor; + + // When + writer.Write(method); + var result = tw.ToString(); + + // Then + Assert.Contains("super().__init__()", result); + Assert.DoesNotContain("super().__init__(message)", result); + } } diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs index 6c65439a9f..30a8e34175 100644 --- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeConstantWriterTests.cs @@ -477,6 +477,80 @@ public void WritesRequestExecutorBody() Assert.Contains("_5XX: createError5XXFromDiscriminatorValue as ParsableFactory", result); Assert.Contains("403: createError403FromDiscriminatorValue as ParsableFactory", result); } + + [Fact] + public void WritesRequestExecutorBodyWithErrorDescriptions() + { + parentClass.Kind = CodeClassKind.RequestBuilder; + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + IsErrorDefinition = true + }).First(); + var error5XX = root.AddClass(new CodeClass + { + Name = "Error5XX", + IsErrorDefinition = true + }).First(); + + // Add error mappings with descriptions + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }, "Client error response"); + method.AddErrorMapping("5XX", new CodeType { Name = "Error5XX", TypeDefinition = error5XX }, "Server error response"); + + AddRequestBodyParameters(); + var constant = CodeConstant.FromRequestBuilderToRequestsMetadata(parentClass); + var codeFile = root.TryAddCodeFile("foo", constant); + codeFile.AddElements(new CodeConstant + { + Name = "UriTemplate", + Kind = CodeConstantKind.UriTemplate, + UriTemplate = "{baseurl+}/foo/bar" + }); + writer.Write(constant); + var result = tw.ToString(); + + // Check for enhanced error mapping with descriptions and TypeScript arrow function syntax + Assert.Contains("errorMappings: {", result); + Assert.Contains("_4XX: (parseNode: ParseNode) => createError4XXFromDiscriminatorValueWithMessage(parseNode, \"Client error response\") as ParsableFactory", result); + Assert.Contains("_5XX: (parseNode: ParseNode) => createError5XXFromDiscriminatorValueWithMessage(parseNode, \"Server error response\") as ParsableFactory", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + + [Fact] + public void WritesRequestExecutorBodyWithoutErrorDescriptions() + { + parentClass.Kind = CodeClassKind.RequestBuilder; + method.Kind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass + { + Name = "Error4XX", + IsErrorDefinition = true + }).First(); + + // Add error mapping without description + method.AddErrorMapping("4XX", new CodeType { Name = "Error4XX", TypeDefinition = error4XX }); + + AddRequestBodyParameters(); + var constant = CodeConstant.FromRequestBuilderToRequestsMetadata(parentClass); + var codeFile = root.TryAddCodeFile("foo", constant); + codeFile.AddElements(new CodeConstant + { + Name = "UriTemplate", + Kind = CodeConstantKind.UriTemplate, + UriTemplate = "{baseurl+}/foo/bar" + }); + writer.Write(constant); + var result = tw.ToString(); + + // Should use original factory method when no description is provided + Assert.Contains("errorMappings: {", result); + Assert.Contains("_4XX: createError4XXFromDiscriminatorValue as ParsableFactory", result); + Assert.DoesNotContain("createError4XXFromDiscriminatorValueWithMessage", result); + AssertExtensions.CurlyBracesAreClosed(result); + } [Fact] public void WritesIndexer() { diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs index 02e13813af..d513066c9a 100644 --- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs @@ -1718,6 +1718,176 @@ public void WritesByteArrayPropertyDeserialization() Assert.Contains("\"property\": n => { model.property = n.getByteArrayValue(); }", result, StringComparison.Ordinal); } + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithMessage() + { + // Create an error class + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Create the factory method with message parameter + var factoryMethod = errorClass.AddMethod(new CodeMethod + { + Name = "createFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.FactoryWithErrorMessage, + ReturnType = new CodeType + { + Name = "SomeError", + TypeDefinition = errorClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add parseNode parameter + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + // Add message parameter + factoryMethod.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType + { + Name = "string", + IsExternal = true, + }, + Optional = true, + }); + + var function = new CodeFunction(factoryMethod); + var codeFile = root.TryAddCodeFile("foo", function); + writer.Write(function); + var result = tw.ToString(); + + // Should call the constructor with message + Assert.Contains("return new SomeError(message);", result); + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + + [Fact] + public void WritesFactoryMethodBodyForErrorClassWithoutMessage() + { + // Create an error class + var errorClass = root.AddClass(new CodeClass + { + Name = "SomeError", + Kind = CodeClassKind.Model, + IsErrorDefinition = true + }).First(); + + // Create the factory method without message parameter + var factoryMethod = errorClass.AddMethod(new CodeMethod + { + Name = "createFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.FactoryWithErrorMessage, + ReturnType = new CodeType + { + Name = "SomeError", + TypeDefinition = errorClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add only parseNode parameter (no message parameter) + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + var function = new CodeFunction(factoryMethod); + var codeFile = root.TryAddCodeFile("foo", function); + writer.Write(function); + var result = tw.ToString(); + + // Should call the parameterless constructor + Assert.Contains("return new SomeError();", result); + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + + [Fact] + public void DoesNotWriteSpecialFactoryMethodForNonErrorClasses() + { + // Create a regular class + var regularClass = root.AddClass(new CodeClass + { + Name = "SomeModel", + Kind = CodeClassKind.Model, + IsErrorDefinition = false + }).First(); + + // Create the factory method + var factoryMethod = regularClass.AddMethod(new CodeMethod + { + Name = "createFromDiscriminatorValueWithMessage", + Kind = CodeMethodKind.Factory, + ReturnType = new CodeType + { + Name = "SomeModel", + TypeDefinition = regularClass, + IsNullable = true + }, + IsStatic = true, + }).First(); + + // Add parameters + factoryMethod.AddParameter(new CodeParameter + { + Name = "parseNode", + Kind = CodeParameterKind.ParseNode, + Type = new CodeType + { + Name = "ParseNode", + IsExternal = true, + }, + Optional = false, + }); + + factoryMethod.AddParameter(new CodeParameter + { + Name = "message", + Kind = CodeParameterKind.ErrorMessage, + Type = new CodeType + { + Name = "string", + IsExternal = true, + }, + Optional = true, + }); + + var function = new CodeFunction(factoryMethod); + var codeFile = root.TryAddCodeFile("foo", function); + writer.Write(function); + var result = tw.ToString(); + + // Should not use special error handling for non-error classes + Assert.DoesNotContain("new SomeModel(message)", result); + // Should continue with normal factory method logic - the exact output varies based on setup but should not be the error handling path + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + [Fact] public async Task WritesOneOfWithInheritanceDeserializationAsync() { From 3e226d6d37e0ce15c59055b26e8f50e5d2d3e71c Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Sun, 14 Dec 2025 17:54:09 +0100 Subject: [PATCH 12/15] Cleanup, sanitize input --- .claude/settings.local.json | 9 +++++++++ src/Kiota.Builder/CodeDOM/CodeMethod.cs | 6 +----- src/Kiota.Builder/Refiners/DartRefiner.cs | 4 ++-- .../Writers/CSharp/CodeMethodWriter.cs | 20 ++++++++++++++----- .../Writers/Dart/CodeMethodWriter.cs | 5 ++--- .../Writers/Go/CodeMethodWriter.cs | 16 ++++++++++----- .../Writers/Java/CodeMethodWriter.cs | 20 ++++++++++++++----- .../Writers/Php/CodeMethodWriter.cs | 16 ++++++++++----- .../Writers/Python/CodeMethodWriter.cs | 16 ++++++++++----- .../Writers/Ruby/CodeMethodWriter.cs | 16 ++++++++++----- 10 files changed, 88 insertions(+), 40 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000000..c631e629d6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(gh pr view:*)", + "Bash(gh pr diff:*)", + "Bash(gh api:*)" + ] + } +} diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 6ef21d4ffb..21699c1fe4 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -354,10 +354,6 @@ public void AddErrorMapping(string errorCode, CodeTypeBase type, string? descrip public string? GetErrorDescription(string errorCode) { ArgumentException.ThrowIfNullOrEmpty(errorCode); - if (errorMappings.ContainsKey(errorCode) && errorDescriptions.TryGetValue(errorCode, out var description)) - { - return description; - } - return null; + return errorDescriptions.TryGetValue(errorCode, out var description) ? description : null; } } diff --git a/src/Kiota.Builder/Refiners/DartRefiner.cs b/src/Kiota.Builder/Refiners/DartRefiner.cs index 5b3d39389d..98b4816982 100644 --- a/src/Kiota.Builder/Refiners/DartRefiner.cs +++ b/src/Kiota.Builder/Refiners/DartRefiner.cs @@ -176,14 +176,14 @@ private void AddConstructorForErrorClass(CodeElement currentElement) if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) { // Add parameterless constructor if not already present - if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) { var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); codeClass.AddMethod(parameterlessConstructor); } var messageParameter = CreateErrorMessageParameter("String"); // Add message constructor if not already present - if (!codeClass.Methods.Any(x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) { var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); messageConstructor.AddParameter(messageParameter); diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 5e35c5e4ea..c62da69060 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -410,15 +410,25 @@ protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams re writer.StartBlock(); foreach (var errorMapping in codeElement.ErrorMappings) { - if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass codeClass)) continue; var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); var errorKey = errorMapping.Key.ToUpperInvariant(); - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - if (!string.IsNullOrEmpty(errorDescription) && codeClass.IsErrorDefinition) - writer.WriteLine($"{{ \"{errorKey}\", (parseNode) => {typeName}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription}\") }},"); - else + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + if (!string.IsNullOrEmpty(errorDescription)) + { + writer.WriteLine($"{{ \"{errorKey}\", (parseNode) => {typeName}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription.SanitizeDoubleQuote()}\") }},"); + } + else + { + writer.WriteLine($"{{ \"{errorKey}\", {typeName}.CreateFromDiscriminatorValue }},"); + } + } + else if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) + { writer.WriteLine($"{{ \"{errorKey}\", {typeName}.CreateFromDiscriminatorValue }},"); + } } writer.CloseBlock("};"); } diff --git a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs index f77b839b83..07808c9a80 100644 --- a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs @@ -526,14 +526,13 @@ protected void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams re writer.StartBlock($"final {errorMappingVarName} = >{{"); foreach (var errorMapping in codeElement.ErrorMappings.Where(errorMapping => errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass)) { - var errorClass = errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition as CodeClass; var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); - if (errorClass?.IsErrorDefinition == true) + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) { var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); var statusCodeAndDescription = !string.IsNullOrEmpty(errorDescription) - ? $"{errorMapping.Key} {errorDescription}" + ? $"{errorMapping.Key} {errorDescription}".SanitizeSingleQuote() : errorMapping.Key; writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : (parseNode) => {typeName}.createFromDiscriminatorValueWithMessage(parseNode, '{statusCodeAndDescription}'),"); } diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index e3ef466bf5..20b3cfbed3 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -828,15 +828,21 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.IncreaseIndent(); foreach (var errorMapping in codeElement.ErrorMappings) { - if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; var errorKey = errorMapping.Key.ToUpperInvariant(); - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) { - writer.WriteLine($"\"{errorKey}\": func(parseNode {conventions.SerializationHash}.ParseNode) error {{ return {conventions.GetTypeString(errorMapping.Value, parentClass, false, false, false)}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription}\") }},"); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + if (!string.IsNullOrEmpty(errorDescription)) + { + writer.WriteLine($"\"{errorKey}\": func(parseNode {conventions.SerializationHash}.ParseNode) error {{ return {conventions.GetTypeString(errorMapping.Value, parentClass, false, false, false)}.CreateFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription.SanitizeDoubleQuote()}\") }},"); + } + else + { + writer.WriteLine($"\"{errorKey}\": {conventions.GetImportedStaticMethodName(errorMapping.Value, parentClass, "Create", "FromDiscriminatorValue", "able")},"); + } } - else + else if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) { writer.WriteLine($"\"{errorKey}\": {conventions.GetImportedStaticMethodName(errorMapping.Value, parentClass, "Create", "FromDiscriminatorValue", "able")},"); } diff --git a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs index a35ed1ecaf..6e53ba7c28 100644 --- a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs @@ -545,15 +545,25 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.WriteLine($"final HashMap> {errorMappingVarName} = new HashMap>();"); foreach (var errorMapping in codeElement.ErrorMappings) { - if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass codeClass)) continue; var typeName = errorMapping.Value.Name; var errorKey = errorMapping.Key.ToUpperInvariant(); - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - if (!string.IsNullOrEmpty(errorDescription) && codeClass.IsErrorDefinition) - writer.WriteLine($"{errorMappingVarName}.put(\"{errorKey}\", (parseNode) -> {typeName}.createFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription}\"));"); - else + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + if (!string.IsNullOrEmpty(errorDescription)) + { + writer.WriteLine($"{errorMappingVarName}.put(\"{errorKey}\", (parseNode) -> {typeName}.createFromDiscriminatorValueWithMessage(parseNode, \"{errorDescription.SanitizeDoubleQuote()}\"));"); + } + else + { + writer.WriteLine($"{errorMappingVarName}.put(\"{errorKey}\", {typeName}::{FactoryMethodName});"); + } + } + else if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) + { writer.WriteLine($"{errorMappingVarName}.put(\"{errorKey}\", {typeName}::{FactoryMethodName});"); + } } } var factoryParameter = GetSendRequestFactoryParam(returnType, codeElement.ReturnType.AllTypes.First().TypeDefinition is CodeEnum); diff --git a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs index 317e6c3239..362d15d456 100644 --- a/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Php/CodeMethodWriter.cs @@ -780,15 +780,21 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, CodeClass parentCl writer.IncreaseIndent(2); errorMappings.ToList().ForEach(errorMapping => { - if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) return; var typeName = errorMapping.Value.Name.ToFirstCharacterUpperCase(); - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) { - writer.WriteLine($"'{errorMapping.Key}' => function($parseNode) {{ return {typeName}::createFromDiscriminatorValueWithMessage($parseNode, '{errorDescription}'); }},"); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + if (!string.IsNullOrEmpty(errorDescription)) + { + writer.WriteLine($"'{errorMapping.Key}' => function($parseNode) {{ return {typeName}::createFromDiscriminatorValueWithMessage($parseNode, '{errorDescription.SanitizeSingleQuote()}'); }},"); + } + else + { + writer.WriteLine($"'{errorMapping.Key}' => [{typeName}::class, '{CreateDiscriminatorMethodName}'],"); + } } - else + else if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) { writer.WriteLine($"'{errorMapping.Key}' => [{typeName}::class, '{CreateDiscriminatorMethodName}'],"); } diff --git a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs index 44a482d9df..f37bc608d7 100644 --- a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs @@ -624,16 +624,22 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.StartBlock($"{errorMappingVarName}: dict[str, type[ParsableFactory]] = {{"); foreach (var errorMapping in codeElement.ErrorMappings) { - if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; var typeName = errorMapping.Value.Name; var errorKey = errorMapping.Key.ToUpperInvariant(); - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) { - writer.WriteLine($"\"{errorKey}\": lambda parse_node: {typeName}.create_from_discriminator_value_with_message(parse_node, \"{errorDescription}\"),"); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + if (!string.IsNullOrEmpty(errorDescription)) + { + writer.WriteLine($"\"{errorKey}\": lambda parse_node: {typeName}.create_from_discriminator_value_with_message(parse_node, \"{errorDescription.SanitizeDoubleQuote()}\"),"); + } + else + { + writer.WriteLine($"\"{errorKey}\": {typeName},"); + } } - else + else if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) { writer.WriteLine($"\"{errorKey}\": {typeName},"); } diff --git a/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs index e02c466c97..a335aceed7 100644 --- a/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs @@ -282,16 +282,22 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ writer.WriteLine($"{errorMappingVarName} = Hash.new"); foreach (var errorMapping in codeElement.ErrorMappings) { - if (!(errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass errorClass)) continue; var typeName = errorMapping.Value.Name; var errorKey = errorMapping.Key.ToUpperInvariant(); - var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); - if (!string.IsNullOrEmpty(errorDescription) && errorClass.IsErrorDefinition) + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) { - writer.WriteLine($"{errorMappingVarName}[\"{errorKey}\"] = lambda {{|parse_node| {typeName}.create_from_discriminator_value_with_message(parse_node, \"{errorDescription}\")}}"); + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + if (!string.IsNullOrEmpty(errorDescription)) + { + writer.WriteLine($"{errorMappingVarName}[\"{errorKey}\"] = lambda {{|parse_node| {typeName}.create_from_discriminator_value_with_message(parse_node, \"{errorDescription.SanitizeDoubleQuote()}\")}}"); + } + else + { + writer.WriteLine($"{errorMappingVarName}[\"{errorKey}\"] = {getDeserializationLambda(errorMapping.Value)}"); + } } - else + else if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) { writer.WriteLine($"{errorMappingVarName}[\"{errorKey}\"] = {getDeserializationLambda(errorMapping.Value)}"); } From 5c7a96123e04f0eeb97666f679d68c8d3e4e98df Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Sun, 14 Dec 2025 20:30:16 +0100 Subject: [PATCH 13/15] AddMethod already sets Parent --- src/Kiota.Builder/Refiners/DartRefiner.cs | 1 - src/Kiota.Builder/Refiners/JavaRefiner.cs | 1 - src/Kiota.Builder/Refiners/PythonRefiner.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Kiota.Builder/Refiners/DartRefiner.cs b/src/Kiota.Builder/Refiners/DartRefiner.cs index 98b4816982..f3059138a2 100644 --- a/src/Kiota.Builder/Refiners/DartRefiner.cs +++ b/src/Kiota.Builder/Refiners/DartRefiner.cs @@ -219,7 +219,6 @@ private static CodeMethod CreateConstructor(CodeClass codeClass, string descript }, Access = AccessModifier.Public, ReturnType = new CodeType { Name = "void", IsExternal = true }, - Parent = codeClass, }; } diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs index 21df2c1a38..b5c800394f 100644 --- a/src/Kiota.Builder/Refiners/JavaRefiner.cs +++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs @@ -601,7 +601,6 @@ private static CodeMethod CreateConstructor(CodeClass codeClass, string descript }, Access = AccessModifier.Public, ReturnType = new CodeType { Name = "void", IsExternal = true }, - Parent = codeClass, }; } } diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index e33b50be2d..9d9a31ba1c 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -441,7 +441,6 @@ private static CodeMethod CreateConstructor(CodeClass codeClass, string descript }, Access = AccessModifier.Public, ReturnType = new CodeType { Name = "None", IsExternal = true }, - Parent = codeClass, }; } } From cee1363cdc3fc819f2e326f5d551e4f79701911a Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Sun, 14 Dec 2025 20:47:54 +0100 Subject: [PATCH 14/15] cleanup, deduplicate CreateConstructor --- .claude/settings.local.json | 9 --------- src/Kiota.Builder/Refiners/DartRefiner.cs | 22 --------------------- src/Kiota.Builder/Refiners/JavaRefiner.cs | 22 --------------------- src/Kiota.Builder/Refiners/PythonRefiner.cs | 22 --------------------- 4 files changed, 75 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index c631e629d6..0000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(gh pr view:*)", - "Bash(gh pr diff:*)", - "Bash(gh api:*)" - ] - } -} diff --git a/src/Kiota.Builder/Refiners/DartRefiner.cs b/src/Kiota.Builder/Refiners/DartRefiner.cs index f3059138a2..05b3a7daa2 100644 --- a/src/Kiota.Builder/Refiners/DartRefiner.cs +++ b/src/Kiota.Builder/Refiners/DartRefiner.cs @@ -200,28 +200,6 @@ private void AddConstructorForErrorClass(CodeElement currentElement) CrawlTree(currentElement, element => AddConstructorForErrorClass(element)); } - private static CodeMethod CreateConstructor(CodeClass codeClass, string descriptionTemplate) - { - return new CodeMethod - { - Name = "constructor", - Kind = CodeMethodKind.Constructor, - IsAsync = false, - IsStatic = false, - Documentation = new(new() { - {"TypeName", new CodeType { - IsExternal = false, - TypeDefinition = codeClass, - }} - }) - { - DescriptionTemplate = descriptionTemplate, - }, - Access = AccessModifier.Public, - ReturnType = new CodeType { Name = "void", IsExternal = true }, - }; - } - /// /// Corrects common names so they can be used with Dart. /// This normally comes down to changing the first character to lower case. diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs index b5c800394f..d515427a25 100644 --- a/src/Kiota.Builder/Refiners/JavaRefiner.cs +++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs @@ -581,26 +581,4 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) } CrawlTree(currentElement, AddConstructorsForErrorClasses); } - - private static CodeMethod CreateConstructor(CodeClass codeClass, string descriptionTemplate) - { - return new CodeMethod - { - Name = "constructor", - Kind = CodeMethodKind.Constructor, - IsAsync = false, - IsStatic = false, - Documentation = new(new() { - {"TypeName", new CodeType { - IsExternal = false, - TypeDefinition = codeClass, - }} - }) - { - DescriptionTemplate = descriptionTemplate, - }, - Access = AccessModifier.Public, - ReturnType = new CodeType { Name = "void", IsExternal = true }, - }; - } } diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index 9d9a31ba1c..a819cf5ee1 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -421,26 +421,4 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) } CrawlTree(currentElement, AddConstructorsForErrorClasses); } - - private static CodeMethod CreateConstructor(CodeClass codeClass, string descriptionTemplate) - { - return new CodeMethod - { - Name = "__init__", - Kind = CodeMethodKind.Constructor, - IsAsync = false, - IsStatic = false, - Documentation = new(new() { - {"TypeName", new CodeType { - IsExternal = false, - TypeDefinition = codeClass, - }} - }) - { - DescriptionTemplate = descriptionTemplate, - }, - Access = AccessModifier.Public, - ReturnType = new CodeType { Name = "None", IsExternal = true }, - }; - } } From e77d204251fc83b39359766c5aa48af209173668 Mon Sep 17 00:00:00 2001 From: Koen van Leeuwen Date: Mon, 15 Dec 2025 15:35:52 +0100 Subject: [PATCH 15/15] Tweaking after output check --- src/Kiota.Builder/Refiners/PythonRefiner.cs | 46 ++++++++++--------- src/Kiota.Builder/Refiners/RubyRefiner.cs | 19 ++++++-- .../Writers/Dart/CodeMethodWriter.cs | 2 +- .../Writers/Python/CodeMethodWriter.cs | 20 ++++++-- 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/src/Kiota.Builder/Refiners/PythonRefiner.cs b/src/Kiota.Builder/Refiners/PythonRefiner.cs index a819cf5ee1..10dc96310b 100644 --- a/src/Kiota.Builder/Refiners/PythonRefiner.cs +++ b/src/Kiota.Builder/Refiners/PythonRefiner.cs @@ -99,7 +99,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken "APIError", $"{AbstractionsPackageName}.api_error" ); - AddConstructorsForErrorClasses(generatedCode); + AddErrorMessageFactoryMethodsToPython(generatedCode); AddGetterAndSetterMethods(generatedCode, new() { CodePropertyKind.Custom, @@ -111,6 +111,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken string.Empty, string.Empty); AddConstructorsForDefaultValues(generatedCode, true); + RemoveConstructorsFromErrorClasses(generatedCode); // Remove constructors added to error classes var defaultConfiguration = new GenerationConfiguration(); cancellationToken.ThrowIfCancellationRequested(); ReplaceDefaultSerializationModules( @@ -265,6 +266,20 @@ private static void CorrectCommonNames(CodeElement currentElement) CrawlTree(currentElement, CorrectCommonNames); } + private static void RemoveConstructorsFromErrorClasses(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + var constructorsToRemove = codeClass.Methods + .Where(static m => m.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor)) + .ToList(); + foreach (var constructor in constructorsToRemove) + { + codeClass.RemoveChildElement(constructor); + } + } + CrawlTree(currentElement, RemoveConstructorsFromErrorClasses); + } private static void CorrectImplements(ProprietableBlockDeclaration block) { block.Implements.Where(x => "IAdditionalDataHolder".Equals(x.Name, StringComparison.OrdinalIgnoreCase)).ToList().ForEach(x => x.Name = x.Name[1..]); // skipping the I @@ -392,33 +407,20 @@ public static IEnumerable codeTypeFilter(IEnumerable })}, }; - private static void AddConstructorsForErrorClasses(CodeElement currentElement) + private static void AddErrorMessageFactoryMethodsToPython(CodeElement currentElement) { if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) { - // Add parameterless constructor if not already present - if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && !x.Parameters.Any())) - { - var parameterlessConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} and sets the default values."); - codeClass.AddMethod(parameterlessConstructor); - } - - // Add message constructor if not already present - var messageParameter = CreateErrorMessageParameter("str", optional: true, defaultValue: "None"); - if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) - { - var messageConstructor = CreateConstructor(codeClass, "Instantiates a new {TypeName} with the specified error message."); - messageConstructor.AddParameter(messageParameter); - codeClass.AddMethod(messageConstructor); - } + var messageParameter = CreateErrorMessageParameter("str"); - TryAddErrorMessageFactoryMethod(codeClass, - "create_from_discriminator_value_with_message", - "ParseNode", - messageParameter, + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "create_from_discriminator_value_with_message", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter, parseNodeParameterName: "parse_node" ); } - CrawlTree(currentElement, AddConstructorsForErrorClasses); + CrawlTree(currentElement, AddErrorMessageFactoryMethodsToPython); } } diff --git a/src/Kiota.Builder/Refiners/RubyRefiner.cs b/src/Kiota.Builder/Refiners/RubyRefiner.cs index c9c83c52a3..3565878050 100644 --- a/src/Kiota.Builder/Refiners/RubyRefiner.cs +++ b/src/Kiota.Builder/Refiners/RubyRefiner.cs @@ -79,7 +79,6 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken "MicrosoftKiotaAbstractions", true ); - AddConstructorsForErrorClasses(generatedCode); ReplaceReservedNames(generatedCode, reservedNamesProvider, x => $"{x}_escaped"); AddGetterAndSetterMethods(generatedCode, [ @@ -98,6 +97,8 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken true, false, [CodeClassKind.RequestConfiguration]); + // Add constructors for error classes AFTER default constructors are added + AddConstructorsForErrorClasses(generatedCode); ShortenLongNamespaceNames(generatedCode); if (generatedCode.FindNamespaceByName(_configuration.ClientNamespaceName)?.Parent is CodeNamespace parentOfClientNS) AddNamespaceModuleImports(parentOfClientNS, generatedCode); @@ -343,9 +344,21 @@ private static void AddConstructorsForErrorClasses(CodeElement currentElement) if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) { var messageParameter = CreateErrorMessageParameter("String", optional: true, defaultValue: "nil"); - // Add initialize method (Ruby's constructor) with message parameter if not exists - if (!codeClass.Methods.Any(static x => x.IsOfKind(CodeMethodKind.Constructor) && x.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)))) + // Ruby only allows one initialize method, so we modify the existing constructor to add an optional message parameter + var existingConstructor = codeClass.Methods.FirstOrDefault(static m => m.IsOfKind(CodeMethodKind.Constructor)); + if (existingConstructor != null) + { + // Add message parameter to existing constructor if not already present + if (!existingConstructor.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage))) + { + existingConstructor.AddParameter(messageParameter); + // Update documentation to reflect the optional parameter + existingConstructor.Documentation.DescriptionTemplate = "Instantiates a new {TypeName} with an optional error message."; + } + } + else { + // If no constructor exists, create one with the message parameter var messageConstructor = new CodeMethod { Name = "initialize", diff --git a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs index 07808c9a80..4e12af9ee0 100644 --- a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs @@ -771,7 +771,7 @@ private void WriteMethodPrototype(CodeMethod code, CodeClass parentClass, Langua var methodName = GetMethodName(code, parentClass, isConstructor); var includeNullableReferenceType = code.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator); var openingBracket = baseSuffix.Equals(" : ", StringComparison.Ordinal) ? "" : "{"; - var closingparenthesis = (isConstructor && parentClass.IsErrorDefinition) ? string.Empty : ")"; + var closingparenthesis = ")"; // Constuctors (except for ClientConstructor) don't need a body but a closing statement if (HasEmptyConstructorBody(code, parentClass, isConstructor)) { diff --git a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs index f37bc608d7..5f6846f63e 100644 --- a/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Python/CodeMethodWriter.cs @@ -40,7 +40,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri var requestConfigParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestConfiguration); var requestContentType = codeElement.Parameters.OfKind(CodeParameterKind.RequestBodyContentType); var requestParams = new RequestParams(requestBodyParam, requestConfigParam, requestContentType); - if (!codeElement.IsOfKind(CodeMethodKind.Setter) && + if (!codeElement.IsOfKind(CodeMethodKind.Setter, CodeMethodKind.Factory, CodeMethodKind.FactoryWithErrorMessage) && !(codeElement.IsOfKind(CodeMethodKind.Constructor) && parentClass.IsOfKind(CodeClassKind.RequestBuilder))) foreach (var parameter in codeElement.Parameters.Where(static x => !x.Optional).OrderBy(static x => x.Name)) { @@ -213,8 +213,20 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, } private void WriteFactoryMethodBodyForErrorClassWithMessage(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { + var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method with message should have a ParseNode parameter"); var messageParam = codeElement.Parameters.FirstOrDefault(static p => p.IsOfKind(CodeParameterKind.ErrorMessage)) ?? throw new InvalidOperationException($"FactoryWithErrorMessage should have a message parameter"); - writer.WriteLine($"return {parentClass.Name}(message)"); + + // Validation + writer.StartBlock($"if {parseNodeParameter.Name} is None:"); + writer.WriteLine($"raise TypeError(\"{parseNodeParameter.Name} cannot be null.\")"); + writer.CloseBlock(string.Empty); + + // Create instance and set message + writer.WriteLine($"error = {parentClass.Name}()"); + writer.StartBlock($"if {messageParam.Name} is not None:"); + writer.WriteLine($"error.message = {messageParam.Name}"); + writer.CloseBlock(string.Empty); + writer.WriteLine("return error"); } private void WriteFactoryMethodBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) @@ -775,7 +787,7 @@ private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer, st private static readonly PythonCodeParameterOrderComparer parameterOrderComparer = new(); private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string returnType, bool isVoid) { - if (code.IsOfKind(CodeMethodKind.Factory)) + if (code.IsOfKind(CodeMethodKind.Factory, CodeMethodKind.FactoryWithErrorMessage)) writer.WriteLine("@staticmethod"); var accessModifier = conventions.GetAccessModifier(code.Access); var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor); @@ -786,7 +798,7 @@ private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string _ => code.Name, }; var asyncPrefix = code.IsAsync && code.Kind is CodeMethodKind.RequestExecutor ? "async " : string.Empty; - var instanceReference = code.IsOfKind(CodeMethodKind.Factory) ? string.Empty : "self,"; + var instanceReference = code.IsOfKind(CodeMethodKind.Factory, CodeMethodKind.FactoryWithErrorMessage) ? string.Empty : "self,"; var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer) .Select(p => new PythonConventionService() // requires a writer instance because method parameters use inline type definitions .GetParameterSignature(p, code, writer))