diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index 66b36b8fba..21699c1fe4 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 { @@ -254,6 +258,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 +270,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, StringComparer.Ordinal); + } + } public bool HasErrorMappingCode(string code) { ArgumentException.ThrowIfNullOrEmpty(code); @@ -304,6 +320,7 @@ public object Clone() Parent = Parent, OriginalIndexer = OriginalIndexer, errorMappings = new(errorMappings), + errorDescriptions = new(errorDescriptions, StringComparer.Ordinal), AcceptedResponseTypes = new List(AcceptedResponseTypes), PagingInformation = PagingInformation?.Clone() as PagingInformation, Documentation = (CodeDocumentation)Documentation.Clone(), @@ -324,10 +341,19 @@ 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); + if (errorMappings.TryAdd(errorCode, type) && !string.IsNullOrEmpty(description)) + { + errorDescriptions.TryAdd(errorCode, description); + } + } + + public string? GetErrorDescription(string errorCode) + { + ArgumentException.ThrowIfNullOrEmpty(errorCode); + return errorDescriptions.TryGetValue(errorCode, out var description) ? description : null; } } 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/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 94a77921f2..882d42ddff 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -1293,7 +1293,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); diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 234b73d4a9..48fb020994 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -107,6 +107,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken AbstractionsNamespaceName ); AddConstructorsForDefaultValues(generatedCode, false); + AddConstructorsForErrorClasses(generatedCode); AddDiscriminatorMappingsUsingsToParentClasses( generatedCode, "IParseNode" @@ -268,4 +269,33 @@ private void SetTypeAccessModifiers(CodeElement currentElement) CrawlTree(currentElement, SetTypeAccessModifiers); } + + 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); + } + + var method = TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "CreateFromDiscriminatorValueWithMessage", + messageParameter: messageParameter, + parseNodeTypeName: "IParseNode"); + } + CrawlTree(currentElement, AddConstructorsForErrorClasses); + } } diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index d5ae1ff21c..355af32714 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -243,28 +243,36 @@ 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, + }, + }; + } + protected static void ReplaceReservedModelTypes(CodeElement current, IReservedNamesProvider provider, Func replacement) => ReplaceReservedNames(current, provider, @@ -1618,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..05b3a7daa2 100644 --- a/src/Kiota.Builder/Refiners/DartRefiner.cs +++ b/src/Kiota.Builder/Refiners/DartRefiner.cs @@ -173,28 +173,29 @@ 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(static 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(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, + setParent: false); } CrawlTree(currentElement, element => AddConstructorForErrorClass(element)); } 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..d515427a25 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,32 @@ 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); + } } 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..10dc96310b 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" ); + AddErrorMessageFactoryMethodsToPython(generatedCode); AddGetterAndSetterMethods(generatedCode, new() { CodePropertyKind.Custom, @@ -110,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( @@ -264,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 @@ -390,4 +406,21 @@ public static IEnumerable codeTypeFilter(IEnumerable }, })}, }; + + private static void AddErrorMessageFactoryMethodsToPython(CodeElement currentElement) + { + if (currentElement is CodeClass codeClass && codeClass.IsErrorDefinition) + { + var messageParameter = CreateErrorMessageParameter("str"); + + TryAddErrorMessageFactoryMethod( + codeClass, + methodName: "create_from_discriminator_value_with_message", + parseNodeTypeName: "ParseNode", + messageParameter: messageParameter, + parseNodeParameterName: "parse_node" + ); + } + CrawlTree(currentElement, AddErrorMessageFactoryMethodsToPython); + } } diff --git a/src/Kiota.Builder/Refiners/RubyRefiner.cs b/src/Kiota.Builder/Refiners/RubyRefiner.cs index b2165d289a..3565878050 100644 --- a/src/Kiota.Builder/Refiners/RubyRefiner.cs +++ b/src/Kiota.Builder/Refiners/RubyRefiner.cs @@ -97,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); @@ -336,4 +338,49 @@ 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"); + // 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", + 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 f80d43ceb3..c62da69060 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"); @@ -400,9 +408,27 @@ 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) { - writer.WriteLine($"{{ \"{errorMapping.Key.ToUpperInvariant()}\", {conventions.GetTypeString(errorMapping.Value, codeElement, false)}.CreateFromDiscriminatorValue }},"); + var typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); + var errorKey = errorMapping.Key.ToUpperInvariant(); + + 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("};"); } @@ -560,7 +586,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) @@ -609,9 +635,16 @@ private static string GetBaseSuffix(bool isConstructor, bool inherits, CodeClass } return " : base()"; } + // For error classes with message constructor, pass the message to base constructor + else if (isConstructor && parentClass.IsErrorDefinition && + currentMethod.Parameters.Any(static p => p.IsOfKind(CodeParameterKind.ErrorMessage))) + { + 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; diff --git a/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Dart/CodeMethodWriter.cs index 2a9ddca836..4e12af9ee0 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,20 @@ 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 typeName = conventions.GetTypeString(errorMapping.Value, codeElement, false); + + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + var errorDescription = codeElement.GetErrorDescription(errorMapping.Key); + var statusCodeAndDescription = !string.IsNullOrEmpty(errorDescription) + ? $"{errorMapping.Key} {errorDescription}".SanitizeSingleQuote() + : errorMapping.Key; + writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : (parseNode) => {typeName}.createFromDiscriminatorValueWithMessage(parseNode, '{statusCodeAndDescription}'),"); + } + else + { + writer.WriteLine($"'{errorMapping.Key.ToUpperInvariant()}' : {typeName}.createFromDiscriminatorValue,"); + } } writer.CloseBlock("};"); } @@ -721,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/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index 479f7e01a3..20b3cfbed3 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,24 @@ 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")},"); + var errorKey = errorMapping.Key.ToUpperInvariant(); + + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + 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 if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) + { + 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..6e53ba7c28 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,25 @@ 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});"); + var typeName = errorMapping.Value.Name; + var errorKey = errorMapping.Key.ToUpperInvariant(); + + 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 1270a62541..362d15d456 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,24 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, CodeClass parentCl writer.IncreaseIndent(2); errorMappings.ToList().ForEach(errorMapping => { - writer.WriteLine($"'{errorMapping.Key}' => [{errorMapping.Value.Name.ToFirstCharacterUpperCase()}::class, '{CreateDiscriminatorMethodName}'],"); + var typeName = errorMapping.Value.Name.ToFirstCharacterUpperCase(); + + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + 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 if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) + { + writer.WriteLine($"'{errorMapping.Key}' => [{typeName}::class, '{CreateDiscriminatorMethodName}'],"); + } }); writer.DecreaseIndent(); writer.WriteLine("];"); @@ -848,6 +874,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..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)) { @@ -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,24 @@ private void WriteFactoryMethodBodyForIntersectionModel(CodeMethod codeElement, } writer.WriteLine($"return {ResultVarName}"); } + 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"); + + // 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) { var parseNodeParameter = codeElement.Parameters.OfKind(CodeParameterKind.ParseNode) ?? throw new InvalidOperationException("Factory method should have a ParseNode parameter"); @@ -329,7 +351,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 +388,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 +587,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 +636,25 @@ 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},"); + var typeName = errorMapping.Value.Name; + var errorKey = errorMapping.Key.ToUpperInvariant(); + + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + 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 if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) + { + writer.WriteLine($"\"{errorKey}\": {typeName},"); + } } writer.CloseBlock(); } @@ -736,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); @@ -747,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)) diff --git a/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Ruby/CodeMethodWriter.cs index d74c6c71a2..a335aceed7 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,25 @@ 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)}"); + var typeName = errorMapping.Value.Name; + var errorKey = errorMapping.Key.ToUpperInvariant(); + + if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass { IsErrorDefinition: true }) + { + 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 if (errorMapping.Value.AllTypes.FirstOrDefault()?.TypeDefinition is CodeClass) + { + 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/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 6919ef14bc..8015bf4c95 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -962,5 +962,87 @@ 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.IsOfKind(CodeParameterKind.ErrorMessage))); + + Assert.NotNull(messageConstructor); + Assert.Single(messageConstructor.Parameters); + 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] + 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.IsOfKind(CodeParameterKind.ErrorMessage))); + + 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.FactoryWithErrorMessage)); + + Assert.NotNull(messageFactoryMethod); + Assert.Equal(2, messageFactoryMethod.Parameters.Count()); + + 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.IsOfKind(CodeParameterKind.ErrorMessage)); + Assert.NotNull(messageParam); + Assert.Equal("message", messageParam.Name); + Assert.Equal("string", messageParam.Type.Name); + + Assert.True(messageFactoryMethod.IsStatic); + Assert.Equal(AccessModifier.Public, messageFactoryMethod.Access); + } #endregion } 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/CSharp/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs index 020e4cfae3..de76b2d59a 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs @@ -2101,4 +2101,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, \"Client Error\") }", result); + Assert.Contains("{ \"401\", (parseNode) => Error401.CreateFromDiscriminatorValueWithMessage(parseNode, \"Unauthorized\") }", result); + Assert.Contains("send", result.ToLower()); + } } 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() {