Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions src/Kiota.Builder/CodeDOM/CodeMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ public enum CodeMethodKind
/// <summary>
/// The override for the error message for the error/exception type.
/// </summary>
ErrorMessageOverride
ErrorMessageOverride,
/// <summary>
/// Factory method for error classes that accepts an error message parameter.
/// </summary>
FactoryWithErrorMessage,
}
public enum HttpMethod
{
Expand Down Expand Up @@ -254,6 +258,7 @@ public void DeduplicateErrorMappings()
public bool HasUrlTemplateOverride => !string.IsNullOrEmpty(UrlTemplateOverride);

private ConcurrentDictionary<string, CodeTypeBase> errorMappings = new(StringComparer.OrdinalIgnoreCase);
private ConcurrentDictionary<string, string> errorDescriptions = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Mapping of the error code and response types for this method.
Expand All @@ -265,6 +270,17 @@ public IOrderedEnumerable<KeyValuePair<string, CodeTypeBase>> ErrorMappings
return errorMappings.OrderBy(static x => x.Key);
}
}

/// <summary>
/// Mapping of the error code and response descriptions from OpenAPI spec for this method.
/// </summary>
public IOrderedEnumerable<KeyValuePair<string, string>> ErrorDescriptions
{
get
{
return errorDescriptions.OrderBy(static x => x.Key, StringComparer.Ordinal);
}
}
public bool HasErrorMappingCode(string code)
{
ArgumentException.ThrowIfNullOrEmpty(code);
Expand Down Expand Up @@ -304,6 +320,7 @@ public object Clone()
Parent = Parent,
OriginalIndexer = OriginalIndexer,
errorMappings = new(errorMappings),
errorDescriptions = new(errorDescriptions, StringComparer.Ordinal),
AcceptedResponseTypes = new List<string>(AcceptedResponseTypes),
PagingInformation = PagingInformation?.Clone() as PagingInformation,
Documentation = (CodeDocumentation)Documentation.Clone(),
Expand All @@ -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;
}
}
4 changes: 4 additions & 0 deletions src/Kiota.Builder/CodeDOM/CodeParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
SerializingDerivedType,
/// <summary>
/// Error message parameter for error/exception class constructors and factory methods.
/// </summary>
ErrorMessage,
}

public class CodeParameter : CodeTerminalWithKind<CodeParameterKind>, ICloneable, IDocumentedElement, IDeprecableElement
Expand Down
2 changes: 1 addition & 1 deletion src/Kiota.Builder/KiotaBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 30 additions & 0 deletions src/Kiota.Builder/Refiners/CSharpRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken
AbstractionsNamespaceName
);
AddConstructorsForDefaultValues(generatedCode, false);
AddConstructorsForErrorClasses(generatedCode);
AddDiscriminatorMappingsUsingsToParentClasses(
generatedCode,
"IParseNode"
Expand Down Expand Up @@ -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);
}
}
160 changes: 141 additions & 19 deletions src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> replacement) =>
ReplaceReservedNames(current,
provider,
Expand Down Expand Up @@ -1618,4 +1626,118 @@ protected static void DeduplicateErrorMappings(CodeElement codeElement)
}
CrawlTree(codeElement, DeduplicateErrorMappings);
}

/// <summary>
/// Creates a CodeParameter for error messages with language-specific configuration.
/// </summary>
/// <param name="typeName">The type name for the message parameter (e.g., "string", "String", "str")</param>
/// <param name="optional">Whether the parameter is optional</param>
/// <param name="defaultValue">The default value if optional (e.g., "None", "nil")</param>
/// <param name="descriptionTemplate">The documentation description template</param>
/// <returns>A configured CodeParameter for error messages</returns>
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"
}
};
}

/// <summary>
/// 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.
/// </summary>
/// <param name="codeClass">The error class to create the factory method for</param>
/// <param name="methodName">The name of the factory method (e.g., "createFromDiscriminatorValueWithMessage")</param>
/// <param name="parseNodeTypeName">The type name for the ParseNode parameter (e.g., "ParseNode", "IParseNode")</param>
/// <param name="messageParameterTypeName">The type name for the message parameter (e.g., "string", "String")</param>
/// <param name="returnTypeName">Optional custom return type name; if null, uses codeClass.Name</param>
/// <param name="returnTypeIsNullable">Whether the return type should be nullable</param>
/// <param name="messageParameterOptional">Whether the message parameter should be optional</param>
/// <param name="setParent">Whether to set the Parent property on the method</param>
/// <param name="returnTypeIsExternal">Whether the return type is external (e.g., for Go's "Parsable")</param>
/// <returns>True if the method was created and added; false if it already existed</returns>
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<string, CodeTypeBase>
{
{
"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;
}
}
41 changes: 21 additions & 20 deletions src/Kiota.Builder/Refiners/DartRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Loading