From 4bbef382df2a73acbb4fc6f9b36050f5d47229c1 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 1 Feb 2022 15:30:30 -0500 Subject: [PATCH 01/36] - adds support for builder getting error mappings Signed-off-by: Vincent Biret --- src/Kiota.Builder/CodeDOM/CodeClass.cs | 213 +- src/Kiota.Builder/CodeDOM/CodeMethod.cs | 226 +-- .../Extensions/OpenApiOperationExtensions.cs | 10 + .../OpenApiUrlTreeNodeExtensions.cs | 6 +- src/Kiota.Builder/KiotaBuilder.cs | 1768 +++++++++-------- .../Kiota.Builder.Tests/KiotaBuilderTests.cs | 215 ++ 6 files changed, 1345 insertions(+), 1093 deletions(-) diff --git a/src/Kiota.Builder/CodeDOM/CodeClass.cs b/src/Kiota.Builder/CodeDOM/CodeClass.cs index 9b9ed2a998..3d553f9581 100644 --- a/src/Kiota.Builder/CodeDOM/CodeClass.cs +++ b/src/Kiota.Builder/CodeDOM/CodeClass.cs @@ -2,128 +2,129 @@ using System.Collections.Generic; using System.Linq; -namespace Kiota.Builder +namespace Kiota.Builder; + +public enum CodeClassKind { + Custom, + RequestBuilder, + Model, + QueryParameters, + /// + /// A single parameter to be provided by the SDK user which will contain query parameters, request body, options, etc. + /// Only used for languages that do not support overloads or optional parameters like go. + /// + ParameterSet, +} +/// +/// CodeClass represents an instance of a Class to be generated in source code +/// +public class CodeClass : CodeBlock, IDocumentedElement, ITypeDefinition { - public enum CodeClassKind { - Custom, - RequestBuilder, - Model, - QueryParameters, - /// - /// A single parameter to be provided by the SDK user which will contain query parameters, request body, options, etc. - /// Only used for languages that do not support overloads or optional parameters like go. - /// - ParameterSet, + private string name; + public CodeClass():base() + { + StartBlock = new Declaration() { Parent = this}; + EndBlock = new End() { Parent = this }; } + public CodeClassKind ClassKind { get; set; } = CodeClassKind.Custom; + + public bool IsErrorDefinition { get; set; } + + public string Description {get; set;} /// - /// CodeClass represents an instance of a Class to be generated in source code + /// Name of Class /// - public class CodeClass : CodeBlock, IDocumentedElement, ITypeDefinition + public override string Name { - private string name; - public CodeClass():base() + get => name; + set { - StartBlock = new Declaration() { Parent = this}; - EndBlock = new End() { Parent = this }; - } - public CodeClassKind ClassKind { get; set; } = CodeClassKind.Custom; - - public string Description {get; set;} - /// - /// Name of Class - /// - public override string Name - { - get => name; - set - { - name = value; - StartBlock.Name = name; - } + name = value; + StartBlock.Name = name; } + } - public void SetIndexer(CodeIndexer indexer) - { - if(indexer == null) - throw new ArgumentNullException(nameof(indexer)); - if(InnerChildElements.Values.OfType().Any()) - throw new InvalidOperationException("this class already has an indexer, remove it first"); - AddRange(indexer); - } + public void SetIndexer(CodeIndexer indexer) + { + if(indexer == null) + throw new ArgumentNullException(nameof(indexer)); + if(InnerChildElements.Values.OfType().Any()) + throw new InvalidOperationException("this class already has an indexer, remove it first"); + AddRange(indexer); + } - public IEnumerable AddProperty(params CodeProperty[] properties) - { - if(properties == null || properties.Any(x => x == null)) - throw new ArgumentNullException(nameof(properties)); - if(!properties.Any()) - throw new ArgumentOutOfRangeException(nameof(properties)); - return AddRange(properties); - } - public CodeProperty GetPropertyOfKind(CodePropertyKind kind) => - Properties.FirstOrDefault(x => x.IsOfKind(kind)); - public IEnumerable Properties => InnerChildElements.Values.OfType(); - public IEnumerable Methods => InnerChildElements.Values.OfType(); + public IEnumerable AddProperty(params CodeProperty[] properties) + { + if(properties == null || properties.Any(x => x == null)) + throw new ArgumentNullException(nameof(properties)); + if(!properties.Any()) + throw new ArgumentOutOfRangeException(nameof(properties)); + return AddRange(properties); + } + public CodeProperty GetPropertyOfKind(CodePropertyKind kind) => + Properties.FirstOrDefault(x => x.IsOfKind(kind)); + public IEnumerable Properties => InnerChildElements.Values.OfType(); + public IEnumerable Methods => InnerChildElements.Values.OfType(); - public bool ContainsMember(string name) - { - return InnerChildElements.ContainsKey(name); - } + public bool ContainsMember(string name) + { + return InnerChildElements.ContainsKey(name); + } - public IEnumerable AddMethod(params CodeMethod[] methods) - { - if(methods == null || methods.Any(x => x == null)) - throw new ArgumentNullException(nameof(methods)); - if(!methods.Any()) - throw new ArgumentOutOfRangeException(nameof(methods)); - return AddRange(methods); - } + public IEnumerable AddMethod(params CodeMethod[] methods) + { + if(methods == null || methods.Any(x => x == null)) + throw new ArgumentNullException(nameof(methods)); + if(!methods.Any()) + throw new ArgumentOutOfRangeException(nameof(methods)); + return AddRange(methods); + } - public IEnumerable AddInnerClass(params CodeClass[] codeClasses) - { - if(codeClasses == null || codeClasses.Any(x => x == null)) - throw new ArgumentNullException(nameof(codeClasses)); - if(!codeClasses.Any()) - throw new ArgumentOutOfRangeException(nameof(codeClasses)); - return AddRange(codeClasses); - } - public CodeClass GetParentClass() { - if(StartBlock is Declaration declaration) - return declaration.Inherits?.TypeDefinition as CodeClass; - else return null; - } - - public CodeClass GetGreatestGrandparent(CodeClass startClassToSkip = null) { - var parentClass = GetParentClass(); - if(parentClass == null) - return startClassToSkip != null && startClassToSkip == this ? null : this; - // we don't want to return the current class if this is the start node in the inheritance tree and doesn't have parent - else - return parentClass.GetGreatestGrandparent(startClassToSkip); - } + public IEnumerable AddInnerClass(params CodeClass[] codeClasses) + { + if(codeClasses == null || codeClasses.Any(x => x == null)) + throw new ArgumentNullException(nameof(codeClasses)); + if(!codeClasses.Any()) + throw new ArgumentOutOfRangeException(nameof(codeClasses)); + return AddRange(codeClasses); + } + public CodeClass GetParentClass() { + if(StartBlock is Declaration declaration) + return declaration.Inherits?.TypeDefinition as CodeClass; + else return null; + } + + public CodeClass GetGreatestGrandparent(CodeClass startClassToSkip = null) { + var parentClass = GetParentClass(); + if(parentClass == null) + return startClassToSkip != null && startClassToSkip == this ? null : this; + // we don't want to return the current class if this is the start node in the inheritance tree and doesn't have parent + else + return parentClass.GetGreatestGrandparent(startClassToSkip); + } - public bool IsOfKind(params CodeClassKind[] kinds) { - return kinds?.Contains(ClassKind) ?? false; - } + public bool IsOfKind(params CodeClassKind[] kinds) { + return kinds?.Contains(ClassKind) ?? false; + } public class Declaration : BlockDeclaration - { - private CodeType inherits; - public CodeType Inherits { get => inherits; set { - EnsureElementsAreChildren(value); - inherits = value; - } } - private readonly List implements = new (); - public void AddImplements(params CodeType[] types) { - if(types == null || types.Any(x => x == null)) - throw new ArgumentNullException(nameof(types)); - EnsureElementsAreChildren(types); - implements.AddRange(types); - } - public IEnumerable Implements => implements; + { + private CodeType inherits; + public CodeType Inherits { get => inherits; set { + EnsureElementsAreChildren(value); + inherits = value; + } } + private readonly List implements = new (); + public void AddImplements(params CodeType[] types) { + if(types == null || types.Any(x => x == null)) + throw new ArgumentNullException(nameof(types)); + EnsureElementsAreChildren(types); + implements.AddRange(types); } + public IEnumerable Implements => implements; + } - public class End : BlockEnd - { - } + public class End : BlockEnd + { } } diff --git a/src/Kiota.Builder/CodeDOM/CodeMethod.cs b/src/Kiota.Builder/CodeDOM/CodeMethod.cs index a281a7fb5d..048b437b30 100644 --- a/src/Kiota.Builder/CodeDOM/CodeMethod.cs +++ b/src/Kiota.Builder/CodeDOM/CodeMethod.cs @@ -2,122 +2,126 @@ using System.Collections.Generic; using System.Linq; -namespace Kiota.Builder +namespace Kiota.Builder; + +public enum CodeMethodKind { - public enum CodeMethodKind - { - Custom, - IndexerBackwardCompatibility, - RequestExecutor, - RequestGenerator, - Serializer, - Deserializer, - Constructor, - Getter, - Setter, - ClientConstructor, - RequestBuilderBackwardCompatibility, - RequestBuilderWithParameters, - RawUrlConstructor, - NullCheck, + Custom, + IndexerBackwardCompatibility, + RequestExecutor, + RequestGenerator, + Serializer, + Deserializer, + Constructor, + Getter, + Setter, + ClientConstructor, + RequestBuilderBackwardCompatibility, + RequestBuilderWithParameters, + RawUrlConstructor, + NullCheck, +} +public enum HttpMethod { + Get, + Post, + Patch, + Put, + Delete, + Options, + Connect, + Head, + Trace +} + +public class CodeMethod : CodeTerminal, ICloneable, IDocumentedElement +{ + public HttpMethod? HttpMethod {get;set;} + public CodeMethodKind MethodKind {get;set;} = CodeMethodKind.Custom; + public string ContentType { get; set; } + public AccessModifier Access {get;set;} = AccessModifier.Public; + private CodeTypeBase returnType; + public CodeTypeBase ReturnType {get => returnType;set { + EnsureElementsAreChildren(value); + returnType = value; + }} + private readonly List parameters = new (); + public void RemoveParametersByKind(params CodeParameterKind[] kinds) { + parameters.RemoveAll(p => p.IsOfKind(kinds)); } - public enum HttpMethod { - Get, - Post, - Patch, - Put, - Delete, - Options, - Connect, - Head, - Trace + public IEnumerable Parameters { get => parameters; } + public bool IsStatic {get;set;} = false; + public bool IsAsync {get;set;} = true; + public string Description {get; set;} + /// + /// The property this method accesses to when it's a getter or setter. + /// + public CodeProperty AccessedProperty { get; set; } + public bool IsOfKind(params CodeMethodKind[] kinds) { + return kinds?.Contains(MethodKind) ?? false; } + public bool IsAccessor { + get => IsOfKind(CodeMethodKind.Getter, CodeMethodKind.Setter); + } + public bool IsSerializationMethod { + get => IsOfKind(CodeMethodKind.Serializer, CodeMethodKind.Deserializer); + } + public List SerializerModules { get; set; } + public List DeserializerModules { get; set; } + /// + /// Indicates whether this method is an overload for another method. + /// + public bool IsOverload { get { return OriginalMethod != null; } } + /// + /// Provides a reference to the original method that this method is an overload of. + /// + public CodeMethod OriginalMethod { get; set; } + /// + /// The original indexer codedom element this method replaces when it is of kind IndexerBackwardCompatibility. + /// + public CodeIndexer OriginalIndexer { get; set; } + /// + /// The base url for every request read from the servers property on the description. + /// Only provided for constructor on Api client + /// + public string BaseUrl { get; set; } + /// + /// Mapping of the error code and response types for this method. + /// + public Dictionary ErrorMappings { get; set; } = new (); - public class CodeMethod : CodeTerminal, ICloneable, IDocumentedElement + public object Clone() { - public HttpMethod? HttpMethod {get;set;} - public CodeMethodKind MethodKind {get;set;} = CodeMethodKind.Custom; - public string ContentType { get; set; } - public AccessModifier Access {get;set;} = AccessModifier.Public; - private CodeTypeBase returnType; - public CodeTypeBase ReturnType {get => returnType;set { - EnsureElementsAreChildren(value); - returnType = value; - }} - private readonly List parameters = new (); - public void RemoveParametersByKind(params CodeParameterKind[] kinds) { - parameters.RemoveAll(p => p.IsOfKind(kinds)); - } - public IEnumerable Parameters { get => parameters; } - public bool IsStatic {get;set;} = false; - public bool IsAsync {get;set;} = true; - public string Description {get; set;} - /// - /// The property this method accesses to when it's a getter or setter. - /// - public CodeProperty AccessedProperty { get; set; } - public bool IsOfKind(params CodeMethodKind[] kinds) { - return kinds?.Contains(MethodKind) ?? false; - } - public bool IsAccessor { - get => IsOfKind(CodeMethodKind.Getter, CodeMethodKind.Setter); - } - public bool IsSerializationMethod { - get => IsOfKind(CodeMethodKind.Serializer, CodeMethodKind.Deserializer); - } - public List SerializerModules { get; set; } - public List DeserializerModules { get; set; } - /// - /// Indicates whether this method is an overload for another method. - /// - public bool IsOverload { get { return OriginalMethod != null; } } - /// - /// Provides a reference to the original method that this method is an overload of. - /// - public CodeMethod OriginalMethod { get; set; } - /// - /// The original indexer codedom element this method replaces when it is of kind IndexerBackwardCompatibility. - /// - public CodeIndexer OriginalIndexer { get; set; } - /// - /// The base url for every request read from the servers property on the description. - /// Only provided for constructor on Api client - /// - public string BaseUrl { get; set; } - - public object Clone() - { - var method = new CodeMethod { - MethodKind = MethodKind, - ReturnType = ReturnType?.Clone() as CodeTypeBase, - Name = Name.Clone() as string, - HttpMethod = HttpMethod, - IsAsync = IsAsync, - Access = Access, - IsStatic = IsStatic, - Description = Description?.Clone() as string, - ContentType = ContentType?.Clone() as string, - BaseUrl = BaseUrl?.Clone() as string, - AccessedProperty = AccessedProperty, - SerializerModules = SerializerModules == null ? null : new (SerializerModules), - DeserializerModules = DeserializerModules == null ? null : new (DeserializerModules), - OriginalMethod = OriginalMethod, - Parent = Parent, - OriginalIndexer = OriginalIndexer, - }; - if(Parameters?.Any() ?? false) - method.AddParameter(Parameters.Select(x => x.Clone() as CodeParameter).ToArray()); - return method; - } + var method = new CodeMethod { + MethodKind = MethodKind, + ReturnType = ReturnType?.Clone() as CodeTypeBase, + Name = Name.Clone() as string, + HttpMethod = HttpMethod, + IsAsync = IsAsync, + Access = Access, + IsStatic = IsStatic, + Description = Description?.Clone() as string, + ContentType = ContentType?.Clone() as string, + BaseUrl = BaseUrl?.Clone() as string, + AccessedProperty = AccessedProperty, + SerializerModules = SerializerModules == null ? null : new (SerializerModules), + DeserializerModules = DeserializerModules == null ? null : new (DeserializerModules), + OriginalMethod = OriginalMethod, + Parent = Parent, + OriginalIndexer = OriginalIndexer, + ErrorMappings = ErrorMappings == null ? null : new (ErrorMappings) + }; + if(Parameters?.Any() ?? false) + method.AddParameter(Parameters.Select(x => x.Clone() as CodeParameter).ToArray()); + return method; + } - public void AddParameter(params CodeParameter[] methodParameters) - { - if(methodParameters == null || methodParameters.Any(x => x == null)) - throw new ArgumentNullException(nameof(methodParameters)); - if(!methodParameters.Any()) - throw new ArgumentOutOfRangeException(nameof(methodParameters)); - EnsureElementsAreChildren(methodParameters); - parameters.AddRange(methodParameters); - } + public void AddParameter(params CodeParameter[] methodParameters) + { + if(methodParameters == null || methodParameters.Any(x => x == null)) + throw new ArgumentNullException(nameof(methodParameters)); + if(!methodParameters.Any()) + throw new ArgumentOutOfRangeException(nameof(methodParameters)); + EnsureElementsAreChildren(methodParameters); + parameters.AddRange(methodParameters); } } diff --git a/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs index 81daafa970..6de35a4e79 100644 --- a/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiOperationExtensions.cs @@ -17,5 +17,15 @@ public static OpenApiSchema GetResponseSchema(this OpenApiOperation operation) return schemas.FirstOrDefault(); } + public static OpenApiSchema GetResponseSchema(this OpenApiResponse response) + { + // For the moment assume application/json + var schemas = response.Content + .Where(c => c.Key == "application/json") + .Select(co => co.Value.Schema) + .Where(s => s is not null); + + return schemas.FirstOrDefault(); + } } } diff --git a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs index 54e7f92667..929d30beb9 100644 --- a/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs +++ b/src/Kiota.Builder/Extensions/OpenApiUrlTreeNodeExtensions.cs @@ -62,8 +62,10 @@ public static IEnumerable GetPathParametersForCurrentSegment(t /// /// Returns the class name for the node with more or less precision depending on the provided arguments /// - public static string GetClassName(this OpenApiUrlTreeNode currentNode, string suffix = default, string prefix = default, OpenApiOperation operation = default) { - var rawClassName = (operation?.GetResponseSchema()?.Reference?.GetClassName() ?? + public static string GetClassName(this OpenApiUrlTreeNode currentNode, string suffix = default, string prefix = default, OpenApiOperation operation = default, OpenApiResponse response = default, OpenApiSchema schema = default) { + var rawClassName = (schema?.Reference?.GetClassName() ?? + response?.GetResponseSchema()?.Reference?.GetClassName() ?? + operation?.GetResponseSchema()?.Reference?.GetClassName() ?? CleanupParametersFromPath(currentNode.Segment)?.ReplaceValueIdentifier()) .TrimEnd(requestParametersEndChar) .TrimStart(requestParametersChar) diff --git a/src/Kiota.Builder/KiotaBuilder.cs b/src/Kiota.Builder/KiotaBuilder.cs index 24cc559418..79a7a73bdc 100644 --- a/src/Kiota.Builder/KiotaBuilder.cs +++ b/src/Kiota.Builder/KiotaBuilder.cs @@ -15,955 +15,975 @@ using System.Security; using Microsoft.OpenApi.Services; -namespace Kiota.Builder +namespace Kiota.Builder; + +public class KiotaBuilder { - public class KiotaBuilder + private readonly ILogger logger; + private readonly GenerationConfiguration config; + + public KiotaBuilder(ILogger logger, GenerationConfiguration config) { - private readonly ILogger logger; - private readonly GenerationConfiguration config; + this.logger = logger; + this.config = config; + } - public KiotaBuilder(ILogger logger, GenerationConfiguration config) - { - this.logger = logger; - this.config = config; - } + public async Task GenerateSDK() + { + var sw = new Stopwatch(); + // Step 1 - Read input stream + string inputPath = config.OpenAPIFilePath; + + try { + // doing this verification at the begining to give immediate feedback to the user + Directory.CreateDirectory(config.OutputPath); + } catch (Exception ex) { + logger.LogError($"Could not open/create output directory {config.OutputPath}, reason: {ex.Message}"); + return; + } + + sw.Start(); + using var input = await LoadStream(inputPath); + if(input == null) + return; + StopLogAndReset(sw, "step 1 - reading the stream - took"); + + // Step 2 - Parse OpenAPI + sw.Start(); + var doc = CreateOpenApiDocument(input); + StopLogAndReset(sw, "step 2 - parsing the document - took"); + + SetApiRootUrl(doc); + + // Step 3 - Create Uri Space of API + sw.Start(); + var openApiTree = CreateUriSpace(doc); + StopLogAndReset(sw, "step 3 - create uri space - took"); + + // Step 4 - Create Source Model + sw.Start(); + var generatedCode = CreateSourceModel(openApiTree); + StopLogAndReset(sw, "step 4 - create source model - took"); + + // Step 5 - RefineByLanguage + sw.Start(); + ApplyLanguageRefinement(config, generatedCode); + StopLogAndReset(sw, "step 5 - refine by language - took"); + + // Step 6 - Write language source + sw.Start(); + await CreateLanguageSourceFilesAsync(config.Language, generatedCode); + StopLogAndReset(sw, "step 6 - writing files - took"); + } + private void SetApiRootUrl(OpenApiDocument doc) { + config.ApiRootUrl = doc.Servers.FirstOrDefault()?.Url.TrimEnd('/'); + if(string.IsNullOrEmpty(config.ApiRootUrl)) + throw new InvalidOperationException("A servers entry (v3) or host + basePath + schems properties (v2) must be present in the OpenAPI description."); + } + private void StopLogAndReset(Stopwatch sw, string prefix) { + sw.Stop(); + logger.LogDebug($"{prefix} {sw.Elapsed}"); + sw.Reset(); + } - public async Task GenerateSDK() - { - var sw = new Stopwatch(); - // Step 1 - Read input stream - string inputPath = config.OpenAPIFilePath; + private async Task LoadStream(string inputPath) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + Stream input; + if (inputPath.StartsWith("http")) try { - // doing this verification at the begining to give immediate feedback to the user - Directory.CreateDirectory(config.OutputPath); - } catch (Exception ex) { - logger.LogError($"Could not open/create output directory {config.OutputPath}, reason: {ex.Message}"); - return; + using var httpClient = new HttpClient(); + input = await httpClient.GetStreamAsync(inputPath); + } catch (HttpRequestException ex) { + logger.LogError($"Could not download the file at {inputPath}, reason: {ex.Message}"); + return null; } - - sw.Start(); - using var input = await LoadStream(inputPath); - if(input == null) - return; - StopLogAndReset(sw, "step 1 - reading the stream - took"); - - // Step 2 - Parse OpenAPI - sw.Start(); - var doc = CreateOpenApiDocument(input); - StopLogAndReset(sw, "step 2 - parsing the document - took"); - - SetApiRootUrl(doc); - - // Step 3 - Create Uri Space of API - sw.Start(); - var openApiTree = CreateUriSpace(doc); - StopLogAndReset(sw, "step 3 - create uri space - took"); - - // Step 4 - Create Source Model - sw.Start(); - var generatedCode = CreateSourceModel(openApiTree); - StopLogAndReset(sw, "step 4 - create source model - took"); - - // Step 5 - RefineByLanguage - sw.Start(); - ApplyLanguageRefinement(config, generatedCode); - StopLogAndReset(sw, "step 5 - refine by language - took"); - - // Step 6 - Write language source - sw.Start(); - await CreateLanguageSourceFilesAsync(config.Language, generatedCode); - StopLogAndReset(sw, "step 6 - writing files - took"); - } - private void SetApiRootUrl(OpenApiDocument doc) { - config.ApiRootUrl = doc.Servers.FirstOrDefault()?.Url.TrimEnd('/'); - if(string.IsNullOrEmpty(config.ApiRootUrl)) - throw new InvalidOperationException("A servers entry (v3) or host + basePath + schems properties (v2) must be present in the OpenAPI description."); - } - private void StopLogAndReset(Stopwatch sw, string prefix) { - sw.Stop(); - logger.LogDebug($"{prefix} {sw.Elapsed}"); - sw.Reset(); - } + else + try { + input = new FileStream(inputPath, FileMode.Open); + } catch (Exception ex) when (ex is FileNotFoundException || + ex is PathTooLongException || + ex is DirectoryNotFoundException || + ex is IOException || + ex is UnauthorizedAccessException || + ex is SecurityException || + ex is NotSupportedException) { + logger.LogError($"Could not open the file at {inputPath}, reason: {ex.Message}"); + return null; + } + stopwatch.Stop(); + logger.LogTrace("{timestamp}ms: Read OpenAPI file {file}", stopwatch.ElapsedMilliseconds, inputPath); + return input; + } - private async Task LoadStream(string inputPath) + public OpenApiDocument CreateOpenApiDocument(Stream input) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + logger.LogTrace("Parsing OpenAPI file"); + var reader = new OpenApiStreamReader(); + var doc = reader.Read(input, out var diag); + stopwatch.Stop(); + if (diag.Errors.Count > 0) { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - Stream input; - if (inputPath.StartsWith("http")) - try { - using var httpClient = new HttpClient(); - input = await httpClient.GetStreamAsync(inputPath); - } catch (HttpRequestException ex) { - logger.LogError($"Could not download the file at {inputPath}, reason: {ex.Message}"); - return null; - } - else - try { - input = new FileStream(inputPath, FileMode.Open); - } catch (Exception ex) when (ex is FileNotFoundException || - ex is PathTooLongException || - ex is DirectoryNotFoundException || - ex is IOException || - ex is UnauthorizedAccessException || - ex is SecurityException || - ex is NotSupportedException) { - logger.LogError($"Could not open the file at {inputPath}, reason: {ex.Message}"); - return null; - } - stopwatch.Stop(); - logger.LogTrace("{timestamp}ms: Read OpenAPI file {file}", stopwatch.ElapsedMilliseconds, inputPath); - return input; + logger.LogError($"{stopwatch.ElapsedMilliseconds}ms: OpenApi Parsing errors {string.Join(Environment.NewLine, diag.Errors.Select(e => e.Message))}"); } - - - public OpenApiDocument CreateOpenApiDocument(Stream input) + else { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - logger.LogTrace("Parsing OpenAPI file"); - var reader = new OpenApiStreamReader(); - var doc = reader.Read(input, out var diag); - stopwatch.Stop(); - if (diag.Errors.Count > 0) - { - logger.LogError($"{stopwatch.ElapsedMilliseconds}ms: OpenApi Parsing errors {string.Join(Environment.NewLine, diag.Errors.Select(e => e.Message))}"); - } - else - { - logger.LogTrace("{timestamp}ms: Parsed OpenAPI successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, doc.Paths.Count); - } - - return doc; + logger.LogTrace("{timestamp}ms: Parsed OpenAPI successfully. {count} paths found.", stopwatch.ElapsedMilliseconds, doc.Paths.Count); } - /// - /// Translate OpenApi PathItems into a tree structure that will define the classes - /// - /// OpenAPI Document of the API to be processed - /// Root node of the API URI space - public OpenApiUrlTreeNode CreateUriSpace(OpenApiDocument doc) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); - var node = OpenApiUrlTreeNode.Create(doc, Constants.DefaultOpenApiLabel); - stopwatch.Stop(); - logger.LogTrace("{timestamp}ms: Created UriSpace tree", stopwatch.ElapsedMilliseconds); - return node; - } - private CodeNamespace rootNamespace; - private CodeNamespace modelsNamespace; - - /// - /// Convert UriSpace of OpenApiPathItems into conceptual SDK Code model - /// - /// Root OpenApiUriSpaceNode of API to be generated - /// - public CodeNamespace CreateSourceModel(OpenApiUrlTreeNode root) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); + return doc; + } - rootNamespace = CodeNamespace.InitRootNamespace(); - var codeNamespace = rootNamespace.AddNamespace(config.ClientNamespaceName); - modelsNamespace = rootNamespace.AddNamespace($"{codeNamespace.Name}.models"); - CreateRequestBuilderClass(codeNamespace, root, root); - StopLogAndReset(stopwatch, $"{nameof(CreateRequestBuilderClass)}"); - stopwatch.Start(); - MapTypeDefinitions(codeNamespace); - StopLogAndReset(stopwatch, $"{nameof(MapTypeDefinitions)}"); + /// + /// Translate OpenApi PathItems into a tree structure that will define the classes + /// + /// OpenAPI Document of the API to be processed + /// Root node of the API URI space + public OpenApiUrlTreeNode CreateUriSpace(OpenApiDocument doc) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + var node = OpenApiUrlTreeNode.Create(doc, Constants.DefaultOpenApiLabel); + stopwatch.Stop(); + logger.LogTrace("{timestamp}ms: Created UriSpace tree", stopwatch.ElapsedMilliseconds); + return node; + } + private CodeNamespace rootNamespace; + private CodeNamespace modelsNamespace; + + /// + /// Convert UriSpace of OpenApiPathItems into conceptual SDK Code model + /// + /// Root OpenApiUriSpaceNode of API to be generated + /// + public CodeNamespace CreateSourceModel(OpenApiUrlTreeNode root) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); - logger.LogTrace("{timestamp}ms: Created source model with {count} classes", stopwatch.ElapsedMilliseconds, codeNamespace.GetChildElements(true).Count()); + rootNamespace = CodeNamespace.InitRootNamespace(); + var codeNamespace = rootNamespace.AddNamespace(config.ClientNamespaceName); + modelsNamespace = rootNamespace.AddNamespace($"{codeNamespace.Name}.models"); + CreateRequestBuilderClass(codeNamespace, root, root); + StopLogAndReset(stopwatch, $"{nameof(CreateRequestBuilderClass)}"); + stopwatch.Start(); + MapTypeDefinitions(codeNamespace); + StopLogAndReset(stopwatch, $"{nameof(MapTypeDefinitions)}"); - return rootNamespace; - } + logger.LogTrace("{timestamp}ms: Created source model with {count} classes", stopwatch.ElapsedMilliseconds, codeNamespace.GetChildElements(true).Count()); - /// - /// Manipulate CodeDOM for language specific issues - /// - /// - /// - public void ApplyLanguageRefinement(GenerationConfiguration config, CodeNamespace generatedCode) - { - var stopwatch = new Stopwatch(); - stopwatch.Start(); + return rootNamespace; + } + + /// + /// Manipulate CodeDOM for language specific issues + /// + /// + /// + public void ApplyLanguageRefinement(GenerationConfiguration config, CodeNamespace generatedCode) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); - ILanguageRefiner.Refine(config, generatedCode); + ILanguageRefiner.Refine(config, generatedCode); - stopwatch.Stop(); - logger.LogDebug("{timestamp}ms: Language refinement applied", stopwatch.ElapsedMilliseconds); - } + stopwatch.Stop(); + logger.LogDebug("{timestamp}ms: Language refinement applied", stopwatch.ElapsedMilliseconds); + } - /// - /// Iterate through Url Space and create request builder classes for each node in the tree - /// - /// Root node of URI space from the OpenAPI described API - /// A CodeNamespace object that contains request builder classes for the Uri Space + /// + /// Iterate through Url Space and create request builder classes for each node in the tree + /// + /// Root node of URI space from the OpenAPI described API + /// A CodeNamespace object that contains request builder classes for the Uri Space - public async Task CreateLanguageSourceFilesAsync(GenerationLanguage language, CodeNamespace generatedCode) - { - var languageWriter = LanguageWriter.GetLanguageWriter(language, this.config.OutputPath, this.config.ClientNamespaceName); - var stopwatch = new Stopwatch(); - stopwatch.Start(); - await new CodeRenderer(config).RenderCodeNamespaceToFilePerClassAsync(languageWriter, generatedCode); - stopwatch.Stop(); - logger.LogTrace("{timestamp}ms: Files written to {path}", stopwatch.ElapsedMilliseconds, config.OutputPath); - } - private static readonly string requestBuilderSuffix = "RequestBuilder"; - private static readonly string voidType = "void"; - private static readonly string coreInterfaceType = "IRequestAdapter"; - private static readonly string requestAdapterParameterName = "requestAdapter"; - private static readonly string constructorMethodName = "constructor"; - /// - /// Create a CodeClass instance that is a request builder class for the OpenApiUrlTreeNode - /// - private void CreateRequestBuilderClass(CodeNamespace currentNamespace, OpenApiUrlTreeNode currentNode, OpenApiUrlTreeNode rootNode) + public async Task CreateLanguageSourceFilesAsync(GenerationLanguage language, CodeNamespace generatedCode) + { + var languageWriter = LanguageWriter.GetLanguageWriter(language, this.config.OutputPath, this.config.ClientNamespaceName); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + await new CodeRenderer(config).RenderCodeNamespaceToFilePerClassAsync(languageWriter, generatedCode); + stopwatch.Stop(); + logger.LogTrace("{timestamp}ms: Files written to {path}", stopwatch.ElapsedMilliseconds, config.OutputPath); + } + private static readonly string requestBuilderSuffix = "RequestBuilder"; + private static readonly string voidType = "void"; + private static readonly string coreInterfaceType = "IRequestAdapter"; + private static readonly string requestAdapterParameterName = "requestAdapter"; + private static readonly string constructorMethodName = "constructor"; + /// + /// Create a CodeClass instance that is a request builder class for the OpenApiUrlTreeNode + /// + private void CreateRequestBuilderClass(CodeNamespace currentNamespace, OpenApiUrlTreeNode currentNode, OpenApiUrlTreeNode rootNode) + { + // Determine Class Name + CodeClass codeClass; + var isApiClientClass = currentNode == rootNode; + if (isApiClientClass) + codeClass = currentNamespace.AddClass(new CodeClass { + Name = config.ClientClassName, + ClassKind = CodeClassKind.RequestBuilder, + Description = "The main entry point of the SDK, exposes the configuration and the fluent API." + }).First(); + else { - // Determine Class Name - CodeClass codeClass; - var isApiClientClass = currentNode == rootNode; - if (isApiClientClass) - codeClass = currentNamespace.AddClass(new CodeClass { - Name = config.ClientClassName, + var targetNS = currentNode.DoesNodeBelongToItemSubnamespace() ? currentNamespace.EnsureItemNamespace() : currentNamespace; + var className = currentNode.GetClassName(requestBuilderSuffix); + codeClass = targetNS.AddClass(new CodeClass { + Name = className, ClassKind = CodeClassKind.RequestBuilder, - Description = "The main entry point of the SDK, exposes the configuration and the fluent API." + Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel, $"Builds and executes requests for operations under {currentNode.Path}"), }).First(); - else - { - var targetNS = currentNode.DoesNodeBelongToItemSubnamespace() ? currentNamespace.EnsureItemNamespace() : currentNamespace; - var className = currentNode.GetClassName(requestBuilderSuffix); - codeClass = targetNS.AddClass(new CodeClass { - Name = className, - ClassKind = CodeClassKind.RequestBuilder, - Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel, $"Builds and executes requests for operations under {currentNode.Path}"), - }).First(); - } + } - logger.LogTrace("Creating class {class}", codeClass.Name); + logger.LogTrace("Creating class {class}", codeClass.Name); - // Add properties for children - foreach (var child in currentNode.Children) + // Add properties for children + foreach (var child in currentNode.Children) + { + var propIdentifier = child.Value.GetClassName(); + var propType = propIdentifier + requestBuilderSuffix; + if (child.Value.IsPathSegmentWithSingleSimpleParameter()) { - var propIdentifier = child.Value.GetClassName(); - var propType = propIdentifier + requestBuilderSuffix; - if (child.Value.IsPathSegmentWithSingleSimpleParameter()) - { - var prop = CreateIndexer($"{propIdentifier}-indexer", propType, child.Value, currentNode); - codeClass.SetIndexer(prop); - } - else if (child.Value.IsComplexPathWithAnyNumberOfParameters()) - { - CreateMethod(propIdentifier, propType, codeClass, child.Value); - } - else - { - var prop = CreateProperty(propIdentifier, propType, kind: CodePropertyKind.RequestBuilder); // we should add the type definition here but we can't as it might not have been generated yet - codeClass.AddProperty(prop); - } + var prop = CreateIndexer($"{propIdentifier}-indexer", propType, child.Value, currentNode); + codeClass.SetIndexer(prop); } - - // Add methods for Operations - if (currentNode.HasOperations(Constants.DefaultOpenApiLabel)) + else if (child.Value.IsComplexPathWithAnyNumberOfParameters()) { - foreach(var operation in currentNode - .PathItems[Constants.DefaultOpenApiLabel] - .Operations - .Where(x => x.Value.RequestBody?.Content?.Any(y => !config.IgnoredRequestContentTypes.Contains(y.Key)) ?? true)) - CreateOperationMethods(currentNode, operation.Key, operation.Value, codeClass); + CreateMethod(propIdentifier, propType, codeClass, child.Value); } - CreateUrlManagement(codeClass, currentNode, isApiClientClass); - - Parallel.ForEach(currentNode.Children.Values, childNode => + else { - var targetNamespaceName = childNode.GetNodeNamespaceFromPath(config.ClientNamespaceName); - var targetNamespace = rootNamespace.FindNamespaceByName(targetNamespaceName) ?? rootNamespace.AddNamespace(targetNamespaceName); - CreateRequestBuilderClass(targetNamespace, childNode, rootNode); - }); - } - private static void CreateMethod(string propIdentifier, string propType, CodeClass codeClass, OpenApiUrlTreeNode currentNode) - { - var methodToAdd = new CodeMethod { - Name = propIdentifier, - MethodKind = CodeMethodKind.RequestBuilderWithParameters, - Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel, $"Builds and executes requests for operations under {currentNode.Path}"), - Access = AccessModifier.Public, - IsAsync = false, - IsStatic = false, - }; - methodToAdd.ReturnType = new CodeType { - Name = propType, - ActionOf = false, - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.None, - IsExternal = false, - IsNullable = false, - }; - AddPathParametersToMethod(currentNode, methodToAdd, false); - codeClass.AddMethod(methodToAdd); - } - private static void AddPathParametersToMethod(OpenApiUrlTreeNode currentNode, CodeMethod methodToAdd, bool asOptional) { - foreach(var parameter in currentNode.GetPathParametersForCurrentSegment()) { - var mParameter = new CodeParameter { - Name = parameter.Name, - Optional = asOptional, - Description = parameter.Description, - ParameterKind = CodeParameterKind.Path, - UrlTemplateParameterName = parameter.Name, - }; - mParameter.Type = GetPrimitiveType(parameter.Schema); - methodToAdd.AddParameter(mParameter); + var prop = CreateProperty(propIdentifier, propType, kind: CodePropertyKind.RequestBuilder); // we should add the type definition here but we can't as it might not have been generated yet + codeClass.AddProperty(prop); } } - private static readonly string PathParametersParameterName = "pathParameters"; - private void CreateUrlManagement(CodeClass currentClass, OpenApiUrlTreeNode currentNode, bool isApiClientClass) { - var pathProperty = new CodeProperty { - Access = AccessModifier.Private, - Name = "urlTemplate", - DefaultValue = $"\"{currentNode.GetUrlTemplate()}\"", - ReadOnly = true, - Description = "Url template to use to build the URL for the current request builder", - PropertyKind = CodePropertyKind.UrlTemplate, - Type = new CodeType { - Name = "string", - IsNullable = false, - IsExternal = true, - }, - }; - currentClass.AddProperty(pathProperty); - var requestAdapterProperty = new CodeProperty { - Name = requestAdapterParameterName, - Description = "The request adapter to use to execute the requests.", - PropertyKind = CodePropertyKind.RequestAdapter, - Access = AccessModifier.Private, - ReadOnly = true, + // Add methods for Operations + if (currentNode.HasOperations(Constants.DefaultOpenApiLabel)) + { + foreach(var operation in currentNode + .PathItems[Constants.DefaultOpenApiLabel] + .Operations + .Where(x => x.Value.RequestBody?.Content?.Any(y => !config.IgnoredRequestContentTypes.Contains(y.Key)) ?? true)) + CreateOperationMethods(currentNode, operation.Key, operation.Value, codeClass); + } + CreateUrlManagement(codeClass, currentNode, isApiClientClass); + + Parallel.ForEach(currentNode.Children.Values, childNode => + { + var targetNamespaceName = childNode.GetNodeNamespaceFromPath(config.ClientNamespaceName); + var targetNamespace = rootNamespace.FindNamespaceByName(targetNamespaceName) ?? rootNamespace.AddNamespace(targetNamespaceName); + CreateRequestBuilderClass(targetNamespace, childNode, rootNode); + }); + } + private static void CreateMethod(string propIdentifier, string propType, CodeClass codeClass, OpenApiUrlTreeNode currentNode) + { + var methodToAdd = new CodeMethod { + Name = propIdentifier, + MethodKind = CodeMethodKind.RequestBuilderWithParameters, + Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel, $"Builds and executes requests for operations under {currentNode.Path}"), + Access = AccessModifier.Public, + IsAsync = false, + IsStatic = false, + }; + methodToAdd.ReturnType = new CodeType { + Name = propType, + ActionOf = false, + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.None, + IsExternal = false, + IsNullable = false, + }; + AddPathParametersToMethod(currentNode, methodToAdd, false); + codeClass.AddMethod(methodToAdd); + } + private static void AddPathParametersToMethod(OpenApiUrlTreeNode currentNode, CodeMethod methodToAdd, bool asOptional) { + foreach(var parameter in currentNode.GetPathParametersForCurrentSegment()) { + var mParameter = new CodeParameter { + Name = parameter.Name, + Optional = asOptional, + Description = parameter.Description, + ParameterKind = CodeParameterKind.Path, + UrlTemplateParameterName = parameter.Name, }; - requestAdapterProperty.Type = new CodeType { - Name = coreInterfaceType, + mParameter.Type = GetPrimitiveType(parameter.Schema); + methodToAdd.AddParameter(mParameter); + } + } + private static readonly string PathParametersParameterName = "pathParameters"; + private void CreateUrlManagement(CodeClass currentClass, OpenApiUrlTreeNode currentNode, bool isApiClientClass) { + var pathProperty = new CodeProperty { + Access = AccessModifier.Private, + Name = "urlTemplate", + DefaultValue = $"\"{currentNode.GetUrlTemplate()}\"", + ReadOnly = true, + Description = "Url template to use to build the URL for the current request builder", + PropertyKind = CodePropertyKind.UrlTemplate, + Type = new CodeType { + Name = "string", + IsNullable = false, + IsExternal = true, + }, + }; + currentClass.AddProperty(pathProperty); + + var requestAdapterProperty = new CodeProperty { + Name = requestAdapterParameterName, + Description = "The request adapter to use to execute the requests.", + PropertyKind = CodePropertyKind.RequestAdapter, + Access = AccessModifier.Private, + ReadOnly = true, + }; + requestAdapterProperty.Type = new CodeType { + Name = coreInterfaceType, + IsExternal = true, + IsNullable = false, + }; + currentClass.AddProperty(requestAdapterProperty); + var constructor = currentClass.AddMethod(new CodeMethod { + Name = constructorMethodName, + MethodKind = isApiClientClass ? CodeMethodKind.ClientConstructor : CodeMethodKind.Constructor, + IsAsync = false, + IsStatic = false, + Description = $"Instantiates a new {currentClass.Name.ToFirstCharacterUpperCase()} and sets the default values.", + Access = AccessModifier.Public, + }).First(); + constructor.ReturnType = new CodeType { Name = voidType, IsExternal = true }; + var pathParametersProperty = new CodeProperty { + Name = PathParametersParameterName, + Description = "Path parameters for the request", + PropertyKind = CodePropertyKind.PathParameters, + Access = AccessModifier.Private, + ReadOnly = true, + Type = new CodeType { + Name = "Dictionary", IsExternal = true, IsNullable = false, - }; - currentClass.AddProperty(requestAdapterProperty); - var constructor = currentClass.AddMethod(new CodeMethod { - Name = constructorMethodName, - MethodKind = isApiClientClass ? CodeMethodKind.ClientConstructor : CodeMethodKind.Constructor, - IsAsync = false, - IsStatic = false, - Description = $"Instantiates a new {currentClass.Name.ToFirstCharacterUpperCase()} and sets the default values.", - Access = AccessModifier.Public, - }).First(); - constructor.ReturnType = new CodeType { Name = voidType, IsExternal = true }; - var pathParametersProperty = new CodeProperty { - Name = PathParametersParameterName, - Description = "Path parameters for the request", - PropertyKind = CodePropertyKind.PathParameters, - Access = AccessModifier.Private, - ReadOnly = true, - Type = new CodeType { - Name = "Dictionary", - IsExternal = true, - IsNullable = false, - }, - }; - currentClass.AddProperty(pathParametersProperty); - if(isApiClientClass) { - constructor.SerializerModules = config.Serializers; - constructor.DeserializerModules = config.Deserializers; - constructor.BaseUrl = config.ApiRootUrl; - pathParametersProperty.DefaultValue = $"new {pathParametersProperty.Type.Name}()"; - } else { - constructor.AddParameter(new CodeParameter { - Name = PathParametersParameterName, - Type = pathParametersProperty.Type, - Optional = false, - Description = pathParametersProperty.Description, - ParameterKind = CodeParameterKind.PathParameters, - }); - AddPathParametersToMethod(currentNode, constructor, true); - } + }, + }; + currentClass.AddProperty(pathParametersProperty); + if(isApiClientClass) { + constructor.SerializerModules = config.Serializers; + constructor.DeserializerModules = config.Deserializers; + constructor.BaseUrl = config.ApiRootUrl; + pathParametersProperty.DefaultValue = $"new {pathParametersProperty.Type.Name}()"; + } else { constructor.AddParameter(new CodeParameter { - Name = requestAdapterParameterName, - Type = requestAdapterProperty.Type, + Name = PathParametersParameterName, + Type = pathParametersProperty.Type, Optional = false, - Description = requestAdapterProperty.Description, - ParameterKind = CodeParameterKind.RequestAdapter, - }); - if(isApiClientClass && config.UsesBackingStore) { - var factoryInterfaceName = $"{BackingStoreInterface}Factory"; - var backingStoreParam = new CodeParameter { - Name = "backingStore", - Optional = true, - Description = "The backing store to use for the models.", - ParameterKind = CodeParameterKind.BackingStore, - Type = new CodeType { - Name = factoryInterfaceName, - IsNullable = true, - } - }; - constructor.AddParameter(backingStoreParam); - } - } - private static readonly Func shortestNamespaceOrder = (x) => x.GetNamespaceDepth(); - /// - /// Remaps definitions to custom types so they can be used later in generation or in refiners - /// - private void MapTypeDefinitions(CodeElement codeElement) { - var unmappedTypes = GetUnmappedTypeDefinitions(codeElement).Distinct(); - - var unmappedTypesWithNoName = unmappedTypes.Where(x => string.IsNullOrEmpty(x.Name)).ToList(); - - unmappedTypesWithNoName.ForEach(x => { - logger.LogWarning($"Type with empty name and parent {x.Parent.Name}"); + Description = pathParametersProperty.Description, + ParameterKind = CodeParameterKind.PathParameters, }); - - var unmappedTypesWithName = unmappedTypes.Except(unmappedTypesWithNoName); - - var unmappedRequestBuilderTypes = unmappedTypesWithName - .Where(x => - x.Parent is CodeProperty property && property.IsOfKind(CodePropertyKind.RequestBuilder) || - x.Parent is CodeIndexer || - x.Parent is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestBuilderWithParameters)) - .ToList(); - - Parallel.ForEach(unmappedRequestBuilderTypes, x => { - var parentNS = x.Parent.Parent.Parent as CodeNamespace; - x.TypeDefinition = parentNS.FindChildrenByName(x.Name) - .OrderBy(shortestNamespaceOrder) - .FirstOrDefault(); - // searching down first because most request builder properties on a request builder are just sub paths on the API - if(x.TypeDefinition == null) { - parentNS = parentNS.Parent as CodeNamespace; - x.TypeDefinition = parentNS - .FindNamespaceByName($"{parentNS.Name}.{x.Name.Substring(0, x.Name.Length - requestBuilderSuffix.Length).ToFirstCharacterLowerCase()}".TrimEnd(nsNameSeparator)) - ?.FindChildrenByName(x.Name) - ?.OrderBy(shortestNamespaceOrder) - ?.FirstOrDefault(); - // in case of the .item namespace, going to the parent and then down to the target by convention - // this avoid getting the wrong request builder in case we have multiple request builders with the same name in the parent branch - // in both cases we always take the uppermost item (smaller numbers of segments in the namespace name) + AddPathParametersToMethod(currentNode, constructor, true); + } + constructor.AddParameter(new CodeParameter { + Name = requestAdapterParameterName, + Type = requestAdapterProperty.Type, + Optional = false, + Description = requestAdapterProperty.Description, + ParameterKind = CodeParameterKind.RequestAdapter, + }); + if(isApiClientClass && config.UsesBackingStore) { + var factoryInterfaceName = $"{BackingStoreInterface}Factory"; + var backingStoreParam = new CodeParameter { + Name = "backingStore", + Optional = true, + Description = "The backing store to use for the models.", + ParameterKind = CodeParameterKind.BackingStore, + Type = new CodeType { + Name = factoryInterfaceName, + IsNullable = true, } - }); - - Parallel.ForEach(unmappedTypesWithName.Where(x => x.TypeDefinition == null).GroupBy(x => x.Name), x => { - if (rootNamespace.FindChildByName(x.First().Name) is CodeElement definition) - foreach (var type in x) - { - type.TypeDefinition = definition; - logger.LogWarning($"Mapped type {type.Name} for {type.Parent.Name} using the fallback approach."); - } - }); - } - private static readonly char nsNameSeparator = '.'; - private static IEnumerable filterUnmappedTypeDefitions(IEnumerable source) => - source.OfType() - .Union(source - .OfType() - .SelectMany(x => x.Types)) - .Where(x => !x.IsExternal && x.TypeDefinition == null); - private IEnumerable GetUnmappedTypeDefinitions(CodeElement codeElement) { - var childElementsUnmappedTypes = codeElement.GetChildElements(true).SelectMany(x => GetUnmappedTypeDefinitions(x)); - return codeElement switch - { - CodeMethod method => filterUnmappedTypeDefitions(method.Parameters.Select(x => x.Type).Union(new CodeTypeBase[] { method.ReturnType })).Union(childElementsUnmappedTypes), - CodeProperty property => filterUnmappedTypeDefitions(new CodeTypeBase[] { property.Type }).Union(childElementsUnmappedTypes), - CodeIndexer indexer => filterUnmappedTypeDefitions(new CodeTypeBase[] { indexer.ReturnType }).Union(childElementsUnmappedTypes), - _ => childElementsUnmappedTypes, }; + constructor.AddParameter(backingStoreParam); } - private CodeIndexer CreateIndexer(string childIdentifier, string childType, OpenApiUrlTreeNode currentNode, OpenApiUrlTreeNode parentNode) + } + private static readonly Func shortestNamespaceOrder = (x) => x.GetNamespaceDepth(); + /// + /// Remaps definitions to custom types so they can be used later in generation or in refiners + /// + private void MapTypeDefinitions(CodeElement codeElement) { + var unmappedTypes = GetUnmappedTypeDefinitions(codeElement).Distinct(); + + var unmappedTypesWithNoName = unmappedTypes.Where(x => string.IsNullOrEmpty(x.Name)).ToList(); + + unmappedTypesWithNoName.ForEach(x => { + logger.LogWarning($"Type with empty name and parent {x.Parent.Name}"); + }); + + var unmappedTypesWithName = unmappedTypes.Except(unmappedTypesWithNoName); + + var unmappedRequestBuilderTypes = unmappedTypesWithName + .Where(x => + x.Parent is CodeProperty property && property.IsOfKind(CodePropertyKind.RequestBuilder) || + x.Parent is CodeIndexer || + x.Parent is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestBuilderWithParameters)) + .ToList(); + + Parallel.ForEach(unmappedRequestBuilderTypes, x => { + var parentNS = x.Parent.Parent.Parent as CodeNamespace; + x.TypeDefinition = parentNS.FindChildrenByName(x.Name) + .OrderBy(shortestNamespaceOrder) + .FirstOrDefault(); + // searching down first because most request builder properties on a request builder are just sub paths on the API + if(x.TypeDefinition == null) { + parentNS = parentNS.Parent as CodeNamespace; + x.TypeDefinition = parentNS + .FindNamespaceByName($"{parentNS.Name}.{x.Name.Substring(0, x.Name.Length - requestBuilderSuffix.Length).ToFirstCharacterLowerCase()}".TrimEnd(nsNameSeparator)) + ?.FindChildrenByName(x.Name) + ?.OrderBy(shortestNamespaceOrder) + ?.FirstOrDefault(); + // in case of the .item namespace, going to the parent and then down to the target by convention + // this avoid getting the wrong request builder in case we have multiple request builders with the same name in the parent branch + // in both cases we always take the uppermost item (smaller numbers of segments in the namespace name) + } + }); + + Parallel.ForEach(unmappedTypesWithName.Where(x => x.TypeDefinition == null).GroupBy(x => x.Name), x => { + if (rootNamespace.FindChildByName(x.First().Name) is CodeElement definition) + foreach (var type in x) + { + type.TypeDefinition = definition; + logger.LogWarning($"Mapped type {type.Name} for {type.Parent.Name} using the fallback approach."); + } + }); + } + private static readonly char nsNameSeparator = '.'; + private static IEnumerable filterUnmappedTypeDefitions(IEnumerable source) => + source.OfType() + .Union(source + .OfType() + .SelectMany(x => x.Types)) + .Where(x => !x.IsExternal && x.TypeDefinition == null); + private IEnumerable GetUnmappedTypeDefinitions(CodeElement codeElement) { + var childElementsUnmappedTypes = codeElement.GetChildElements(true).SelectMany(x => GetUnmappedTypeDefinitions(x)); + return codeElement switch { - logger.LogTrace("Creating indexer {name}", childIdentifier); - return new CodeIndexer - { - Name = childIdentifier, - Description = $"Gets an item from the {currentNode.GetNodeNamespaceFromPath(config.ClientNamespaceName)} collection", - IndexType = new CodeType { Name = "string", IsExternal = true, }, - ReturnType = new CodeType { Name = childType }, - ParameterName = currentNode.Segment.SanitizeUrlTemplateParameterName().TrimStart('{').TrimEnd('}'), - PathSegment = parentNode.GetNodeNamespaceFromPath(string.Empty).Split('.').Last(), - }; - } + CodeMethod method => filterUnmappedTypeDefitions(method.Parameters.Select(x => x.Type).Union(new CodeTypeBase[] { method.ReturnType })).Union(childElementsUnmappedTypes), + CodeProperty property => filterUnmappedTypeDefitions(new CodeTypeBase[] { property.Type }).Union(childElementsUnmappedTypes), + CodeIndexer indexer => filterUnmappedTypeDefitions(new CodeTypeBase[] { indexer.ReturnType }).Union(childElementsUnmappedTypes), + _ => childElementsUnmappedTypes, + }; + } + private CodeIndexer CreateIndexer(string childIdentifier, string childType, OpenApiUrlTreeNode currentNode, OpenApiUrlTreeNode parentNode) + { + logger.LogTrace("Creating indexer {name}", childIdentifier); + return new CodeIndexer + { + Name = childIdentifier, + Description = $"Gets an item from the {currentNode.GetNodeNamespaceFromPath(config.ClientNamespaceName)} collection", + IndexType = new CodeType { Name = "string", IsExternal = true, }, + ReturnType = new CodeType { Name = childType }, + ParameterName = currentNode.Segment.SanitizeUrlTemplateParameterName().TrimStart('{').TrimEnd('}'), + PathSegment = parentNode.GetNodeNamespaceFromPath(string.Empty).Split('.').Last(), + }; + } - private CodeProperty CreateProperty(string childIdentifier, string childType, string defaultValue = null, OpenApiSchema typeSchema = null, CodeElement typeDefinition = null, CodePropertyKind kind = CodePropertyKind.Custom) + private CodeProperty CreateProperty(string childIdentifier, string childType, string defaultValue = null, OpenApiSchema typeSchema = null, CodeElement typeDefinition = null, CodePropertyKind kind = CodePropertyKind.Custom) + { + var propertyName = childIdentifier; + config.PropertiesPrefixToStrip.ForEach(x => propertyName = propertyName.Replace(x, string.Empty)); + var prop = new CodeProperty { - var propertyName = childIdentifier; - config.PropertiesPrefixToStrip.ForEach(x => propertyName = propertyName.Replace(x, string.Empty)); - var prop = new CodeProperty - { - Name = propertyName, - DefaultValue = defaultValue, - PropertyKind = kind, - Description = typeSchema?.Description, - }; - if(propertyName != childIdentifier) - prop.SerializationName = childIdentifier; - - var propType = GetPrimitiveType(typeSchema, childType); - propType.TypeDefinition = typeDefinition; - propType.CollectionKind = typeSchema.IsArray() ? CodeType.CodeTypeCollectionKind.Complex : default; - prop.Type = propType; - logger.LogTrace("Creating property {name} of {type}", prop.Name, prop.Type.Name); - return prop; - } - private static readonly HashSet typeNamesToSkip = new() {"object", "array"}; - private static CodeType GetPrimitiveType(OpenApiSchema typeSchema, string childType = default) { - var typeNames = new List{typeSchema?.Items?.Type, childType, typeSchema?.Type}; - if(typeSchema?.AnyOf?.Any() ?? false) - typeNames.AddRange(typeSchema.AnyOf.Select(x => x.Type)); // double is sometimes an anyof string, number and enum - // first value that's not null, and not "object" for primitive collections, the items type matters - var typeName = typeNames.FirstOrDefault(x => !string.IsNullOrEmpty(x) && !typeNamesToSkip.Contains(x)); - - if(string.IsNullOrEmpty(typeName)) - return null; - var format = typeSchema?.Format ?? typeSchema?.Items?.Format; - var isExternal = false; - if (typeSchema?.Items?.Enum?.Any() ?? false) - typeName = childType; - else if("string".Equals(typeName, StringComparison.OrdinalIgnoreCase)) { - isExternal = true; - if("date-time".Equals(format, StringComparison.OrdinalIgnoreCase)) - typeName = "DateTimeOffset"; - else if("duration".Equals(format, StringComparison.OrdinalIgnoreCase)) - typeName = "TimeSpan"; - else if("date".Equals(format, StringComparison.OrdinalIgnoreCase)) - typeName = "DateOnly"; - else if("time".Equals(format, StringComparison.OrdinalIgnoreCase)) - typeName = "TimeOnly"; - else if ("base64url".Equals(format, StringComparison.OrdinalIgnoreCase)) - typeName = "binary"; - } else if ("double".Equals(format, StringComparison.OrdinalIgnoreCase) || - "float".Equals(format, StringComparison.OrdinalIgnoreCase) || - "int64".Equals(format, StringComparison.OrdinalIgnoreCase) || - "decimal".Equals(format, StringComparison.OrdinalIgnoreCase)) { - isExternal = true; - typeName = format.ToLowerInvariant(); - } else if ("boolean".Equals(typeName, StringComparison.OrdinalIgnoreCase) || - "integer".Equals(typeName, StringComparison.OrdinalIgnoreCase)) + Name = propertyName, + DefaultValue = defaultValue, + PropertyKind = kind, + Description = typeSchema?.Description, + }; + if(propertyName != childIdentifier) + prop.SerializationName = childIdentifier; + + var propType = GetPrimitiveType(typeSchema, childType); + propType.TypeDefinition = typeDefinition; + propType.CollectionKind = typeSchema.IsArray() ? CodeType.CodeTypeCollectionKind.Complex : default; + prop.Type = propType; + logger.LogTrace("Creating property {name} of {type}", prop.Name, prop.Type.Name); + return prop; + } + private static readonly HashSet typeNamesToSkip = new() {"object", "array"}; + private static CodeType GetPrimitiveType(OpenApiSchema typeSchema, string childType = default) { + var typeNames = new List{typeSchema?.Items?.Type, childType, typeSchema?.Type}; + if(typeSchema?.AnyOf?.Any() ?? false) + typeNames.AddRange(typeSchema.AnyOf.Select(x => x.Type)); // double is sometimes an anyof string, number and enum + // first value that's not null, and not "object" for primitive collections, the items type matters + var typeName = typeNames.FirstOrDefault(x => !string.IsNullOrEmpty(x) && !typeNamesToSkip.Contains(x)); + + if(string.IsNullOrEmpty(typeName)) + return null; + var format = typeSchema?.Format ?? typeSchema?.Items?.Format; + var isExternal = false; + if (typeSchema?.Items?.Enum?.Any() ?? false) + typeName = childType; + else if("string".Equals(typeName, StringComparison.OrdinalIgnoreCase)) { isExternal = true; - return new CodeType { - Name = typeName, - IsExternal = isExternal, - }; - } - private const string RequestBodyBinaryContentType = "application/octet-stream"; - private static readonly HashSet noContentStatusCodes = new() { "201", "202", "204" }; - private void CreateOperationMethods(OpenApiUrlTreeNode currentNode, OperationType operationType, OpenApiOperation operation, CodeClass parentClass) - { - var parameterClass = CreateOperationParameter(currentNode, operationType, operation); - - var schema = operation.GetResponseSchema(); - var method = (HttpMethod)Enum.Parse(typeof(HttpMethod), operationType.ToString()); - var executorMethod = new CodeMethod { - Name = operationType.ToString(), - MethodKind = CodeMethodKind.RequestExecutor, - HttpMethod = method, - Description = operation.Description ?? operation.Summary, - }; - parentClass.AddMethod(executorMethod); - if (schema != null) + if("date-time".Equals(format, StringComparison.OrdinalIgnoreCase)) + typeName = "DateTimeOffset"; + else if("duration".Equals(format, StringComparison.OrdinalIgnoreCase)) + typeName = "TimeSpan"; + else if("date".Equals(format, StringComparison.OrdinalIgnoreCase)) + typeName = "DateOnly"; + else if("time".Equals(format, StringComparison.OrdinalIgnoreCase)) + typeName = "TimeOnly"; + else if ("base64url".Equals(format, StringComparison.OrdinalIgnoreCase)) + typeName = "binary"; + } else if ("double".Equals(format, StringComparison.OrdinalIgnoreCase) || + "float".Equals(format, StringComparison.OrdinalIgnoreCase) || + "int64".Equals(format, StringComparison.OrdinalIgnoreCase) || + "decimal".Equals(format, StringComparison.OrdinalIgnoreCase)) { + isExternal = true; + typeName = format.ToLowerInvariant(); + } else if ("boolean".Equals(typeName, StringComparison.OrdinalIgnoreCase) || + "integer".Equals(typeName, StringComparison.OrdinalIgnoreCase)) + isExternal = true; + return new CodeType { + Name = typeName, + IsExternal = isExternal, + }; + } + private const string RequestBodyBinaryContentType = "application/octet-stream"; + private static readonly HashSet noContentStatusCodes = new() { "201", "202", "204" }; + private static readonly HashSet errorStatusCodes = new(Enumerable.Range(400, 599).Select(x => x.ToString()) + .Concat(new[] { "4XX", "5XX" }), StringComparer.OrdinalIgnoreCase); + + private void AddErrorMappingsForExecutorMethod(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeMethod executorMethod) { + foreach(var response in operation.Responses.Where(x => errorStatusCodes.Contains(x.Key))) { + var errorCode = response.Key.ToUpperInvariant(); + var errorSchema = response.Value.GetResponseSchema(); + var parentElement = string.IsNullOrEmpty(response.Value.Reference?.Id) && string.IsNullOrEmpty(errorSchema?.Reference?.Id) + ? executorMethod as CodeElement + : modelsNamespace; + var errorType = CreateModelDeclarations(currentNode, errorSchema, operation, parentElement, $"{errorCode}Error", response: response.Value); + if (errorType is CodeType codeType && + codeType.TypeDefinition is CodeClass codeClass && + !codeClass.IsErrorDefinition) { - var returnType = CreateModelDeclarations(currentNode, schema, operation, executorMethod, "Response"); - executorMethod.ReturnType = returnType ?? throw new InvalidOperationException("Could not resolve return type for operation"); - } else { - var returnType = voidType; - if(operation.Responses.Any(x => x.Value.Content.ContainsKey(RequestBodyBinaryContentType))) - returnType = "binary"; - else if(!operation.Responses.Any(x => noContentStatusCodes.Contains(x.Key))) - logger.LogWarning($"could not find operation return type {operationType} {currentNode.Path}"); - executorMethod.ReturnType = new CodeType { Name = returnType, IsExternal = true, }; + codeClass.IsErrorDefinition = true; } - - - AddRequestBuilderMethodParameters(currentNode, operation, parameterClass, executorMethod); - - var handlerParam = new CodeParameter { - Name = "responseHandler", - Optional = true, - ParameterKind = CodeParameterKind.ResponseHandler, - Description = "Response handler to use in place of the default response handling provided by the core service", - Type = new CodeType { Name = "IResponseHandler", IsExternal = true }, - }; - executorMethod.AddParameter(handlerParam);// Add response handler parameter - - var cancellationParam = new CodeParameter{ - Name = "cancellationToken", - Optional = true, - ParameterKind = CodeParameterKind.Cancellation, - Description = "Cancellation token to use when cancelling requests", - Type = new CodeType { Name = "CancellationToken", IsExternal = true }, - }; - executorMethod.AddParameter(cancellationParam);// Add cancellation token parameter - logger.LogTrace("Creating method {name} of {type}", executorMethod.Name, executorMethod.ReturnType); - - var generatorMethod = new CodeMethod { - Name = $"Create{operationType.ToString().ToFirstCharacterUpperCase()}RequestInformation", - MethodKind = CodeMethodKind.RequestGenerator, - IsAsync = false, - HttpMethod = method, - Description = operation.Description ?? operation.Summary, - ReturnType = new CodeType { Name = "RequestInformation", IsNullable = false, IsExternal = true}, + executorMethod.ErrorMappings.Add(errorCode, errorType); + } + } + private void CreateOperationMethods(OpenApiUrlTreeNode currentNode, OperationType operationType, OpenApiOperation operation, CodeClass parentClass) + { + var parameterClass = CreateOperationParameter(currentNode, operationType, operation); + + var schema = operation.GetResponseSchema(); + var method = (HttpMethod)Enum.Parse(typeof(HttpMethod), operationType.ToString()); + var executorMethod = new CodeMethod { + Name = operationType.ToString(), + MethodKind = CodeMethodKind.RequestExecutor, + HttpMethod = method, + Description = operation.Description ?? operation.Summary, + }; + parentClass.AddMethod(executorMethod); + AddErrorMappingsForExecutorMethod(currentNode, operation, executorMethod); + if (schema != null) + { + var returnType = CreateModelDeclarations(currentNode, schema, operation, executorMethod, "Response"); + executorMethod.ReturnType = returnType ?? throw new InvalidOperationException("Could not resolve return type for operation"); + } else { + var returnType = voidType; + if(operation.Responses.Any(x => x.Value.Content.ContainsKey(RequestBodyBinaryContentType))) + returnType = "binary"; + else if(!operation.Responses.Any(x => noContentStatusCodes.Contains(x.Key))) + logger.LogWarning($"could not find operation return type {operationType} {currentNode.Path}"); + executorMethod.ReturnType = new CodeType { Name = returnType, IsExternal = true, }; + } + + + AddRequestBuilderMethodParameters(currentNode, operation, parameterClass, executorMethod); + + var handlerParam = new CodeParameter { + Name = "responseHandler", + Optional = true, + ParameterKind = CodeParameterKind.ResponseHandler, + Description = "Response handler to use in place of the default response handling provided by the core service", + Type = new CodeType { Name = "IResponseHandler", IsExternal = true }, + }; + executorMethod.AddParameter(handlerParam);// Add response handler parameter + + var cancellationParam = new CodeParameter{ + Name = "cancellationToken", + Optional = true, + ParameterKind = CodeParameterKind.Cancellation, + Description = "Cancellation token to use when cancelling requests", + Type = new CodeType { Name = "CancellationToken", IsExternal = true }, + }; + executorMethod.AddParameter(cancellationParam);// Add cancellation token parameter + logger.LogTrace("Creating method {name} of {type}", executorMethod.Name, executorMethod.ReturnType); + + var generatorMethod = new CodeMethod { + Name = $"Create{operationType.ToString().ToFirstCharacterUpperCase()}RequestInformation", + MethodKind = CodeMethodKind.RequestGenerator, + IsAsync = false, + HttpMethod = method, + Description = operation.Description ?? operation.Summary, + ReturnType = new CodeType { Name = "RequestInformation", IsNullable = false, IsExternal = true}, + }; + parentClass.AddMethod(generatorMethod); + AddRequestBuilderMethodParameters(currentNode, operation, parameterClass, generatorMethod); + logger.LogTrace("Creating method {name} of {type}", generatorMethod.Name, generatorMethod.ReturnType); + } + private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeClass parameterClass, CodeMethod method) { + var nonBinaryRequestBody = operation.RequestBody?.Content?.FirstOrDefault(x => !RequestBodyBinaryContentType.Equals(x.Key, StringComparison.OrdinalIgnoreCase)); + if (nonBinaryRequestBody.HasValue && nonBinaryRequestBody.Value.Value != null) + { + var requestBodySchema = nonBinaryRequestBody.Value.Value.Schema; + var requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, "RequestBody"); + method.AddParameter(new CodeParameter { + Name = "body", + Type = requestBodyType, + Optional = false, + ParameterKind = CodeParameterKind.RequestBody, + Description = requestBodySchema.Description + }); + method.ContentType = nonBinaryRequestBody.Value.Key; + } else if (operation.RequestBody?.Content?.ContainsKey(RequestBodyBinaryContentType) ?? false) { + var nParam = new CodeParameter { + Name = "body", + Optional = false, + ParameterKind = CodeParameterKind.RequestBody, + Description = $"Binary request body", + Type = new CodeType { + Name = "binary", + IsExternal = true, + IsNullable = false, + }, }; - parentClass.AddMethod(generatorMethod); - AddRequestBuilderMethodParameters(currentNode, operation, parameterClass, generatorMethod); - logger.LogTrace("Creating method {name} of {type}", generatorMethod.Name, generatorMethod.ReturnType); + method.AddParameter(nParam); } - private void AddRequestBuilderMethodParameters(OpenApiUrlTreeNode currentNode, OpenApiOperation operation, CodeClass parameterClass, CodeMethod method) { - var nonBinaryRequestBody = operation.RequestBody?.Content?.FirstOrDefault(x => !RequestBodyBinaryContentType.Equals(x.Key, StringComparison.OrdinalIgnoreCase)); - if (nonBinaryRequestBody.HasValue && nonBinaryRequestBody.Value.Value != null) + if(parameterClass != null) { + var qsParam = new CodeParameter { - var requestBodySchema = nonBinaryRequestBody.Value.Value.Schema; - var requestBodyType = CreateModelDeclarations(currentNode, requestBodySchema, operation, method, "RequestBody"); - method.AddParameter(new CodeParameter { - Name = "body", - Type = requestBodyType, - Optional = false, - ParameterKind = CodeParameterKind.RequestBody, - Description = requestBodySchema.Description - }); - method.ContentType = nonBinaryRequestBody.Value.Key; - } else if (operation.RequestBody?.Content?.ContainsKey(RequestBodyBinaryContentType) ?? false) { - var nParam = new CodeParameter { - Name = "body", - Optional = false, - ParameterKind = CodeParameterKind.RequestBody, - Description = $"Binary request body", - Type = new CodeType { - Name = "binary", - IsExternal = true, - IsNullable = false, - }, - }; - method.AddParameter(nParam); - } - if(parameterClass != null) { - var qsParam = new CodeParameter - { - Name = "q", - Optional = true, - ParameterKind = CodeParameterKind.QueryParameter, - Description = "Request query parameters", - Type = new CodeType { Name = parameterClass.Name, ActionOf = true, TypeDefinition = parameterClass }, - }; - method.AddParameter(qsParam); - } - var headersParam = new CodeParameter { - Name = "h", - Optional = true, - ParameterKind = CodeParameterKind.Headers, - Description = "Request headers", - Type = new CodeType { Name = "IDictionary", ActionOf = true, IsExternal = true }, - }; - method.AddParameter(headersParam); - var optionsParam = new CodeParameter { - Name = "o", + Name = "q", Optional = true, - ParameterKind = CodeParameterKind.Options, - Description = "Request options", - Type = new CodeType { Name = "IEnumerable", ActionOf = false, IsExternal = true }, - }; - method.AddParameter(optionsParam); - } - private string GetModelsNamespaceNameFromReferenceId(string referenceId) { - if (string.IsNullOrEmpty(referenceId)) return referenceId; - if(referenceId.StartsWith(config.ClientClassName, StringComparison.OrdinalIgnoreCase)) - referenceId = referenceId[config.ClientClassName.Length..]; - referenceId = referenceId.Trim(nsNameSeparator); - var lastDotIndex = referenceId.LastIndexOf(nsNameSeparator); - var namespaceSuffix = lastDotIndex != -1 ? referenceId[..lastDotIndex] : referenceId; - return $"{modelsNamespace.Name}.{namespaceSuffix}"; - } - private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, CodeNamespace codeNamespace, string classNameSuffix = "") { - var className = currentNode.GetClassName(operation: operation, suffix: classNameSuffix); - var codeDeclaration = AddModelDeclarationIfDoesntExit(currentNode, schema, className, codeNamespace); - return new CodeType { - TypeDefinition = codeDeclaration, - Name = className, + ParameterKind = CodeParameterKind.QueryParameter, + Description = "Request query parameters", + Type = new CodeType { Name = parameterClass.Name, ActionOf = true, TypeDefinition = parameterClass }, }; - } - private CodeTypeBase CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation) { - var allOfs = schema.AllOf.FlattenEmptyEntries(x => x.AllOf); - CodeElement codeDeclaration = null; - var className = string.Empty; - foreach(var currentSchema in allOfs) { - var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); - var shortestNamespaceName = string.IsNullOrEmpty(referenceId) ? currentNode.GetNodeNamespaceFromPath(config.ClientNamespaceName) : GetModelsNamespaceNameFromReferenceId(referenceId); - var shortestNamespace = rootNamespace.FindNamespaceByName(shortestNamespaceName); - if(shortestNamespace == null) - shortestNamespace = rootNamespace.AddNamespace(shortestNamespaceName); - className = currentSchema.GetSchemaTitle() ?? currentNode.GetClassName(operation: operation); - codeDeclaration = AddModelDeclarationIfDoesntExit(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass, true); - } - - return new CodeType { + method.AddParameter(qsParam); + } + var headersParam = new CodeParameter { + Name = "h", + Optional = true, + ParameterKind = CodeParameterKind.Headers, + Description = "Request headers", + Type = new CodeType { Name = "IDictionary", ActionOf = true, IsExternal = true }, + }; + method.AddParameter(headersParam); + var optionsParam = new CodeParameter { + Name = "o", + Optional = true, + ParameterKind = CodeParameterKind.Options, + Description = "Request options", + Type = new CodeType { Name = "IEnumerable", ActionOf = false, IsExternal = true }, + }; + method.AddParameter(optionsParam); + } + private string GetModelsNamespaceNameFromReferenceId(string referenceId) { + if (string.IsNullOrEmpty(referenceId)) return referenceId; + if(referenceId.StartsWith(config.ClientClassName, StringComparison.OrdinalIgnoreCase)) + referenceId = referenceId[config.ClientClassName.Length..]; + referenceId = referenceId.Trim(nsNameSeparator); + var lastDotIndex = referenceId.LastIndexOf(nsNameSeparator); + var namespaceSuffix = lastDotIndex != -1 ? referenceId[..lastDotIndex] : referenceId; + return $"{modelsNamespace.Name}.{namespaceSuffix}"; + } + private CodeType CreateModelDeclarationAndType(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, CodeNamespace codeNamespace, string classNameSuffix = "", OpenApiResponse response = default) { + var className = currentNode.GetClassName(operation: operation, suffix: classNameSuffix, response: response, schema: schema); + var codeDeclaration = AddModelDeclarationIfDoesntExit(currentNode, schema, className, codeNamespace); + return new CodeType { + TypeDefinition = codeDeclaration, + Name = className, + }; + } + private CodeTypeBase CreateInheritedModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation) { + var allOfs = schema.AllOf.FlattenEmptyEntries(x => x.AllOf); + CodeElement codeDeclaration = null; + var className = string.Empty; + foreach(var currentSchema in allOfs) { + var referenceId = GetReferenceIdFromOriginalSchema(currentSchema, schema); + var shortestNamespaceName = string.IsNullOrEmpty(referenceId) ? currentNode.GetNodeNamespaceFromPath(config.ClientNamespaceName) : GetModelsNamespaceNameFromReferenceId(referenceId); + var shortestNamespace = rootNamespace.FindNamespaceByName(shortestNamespaceName); + if(shortestNamespace == null) + shortestNamespace = rootNamespace.AddNamespace(shortestNamespaceName); + className = currentSchema.GetSchemaTitle() ?? currentNode.GetClassName(operation: operation, schema: schema); + codeDeclaration = AddModelDeclarationIfDoesntExit(currentNode, currentSchema, className, shortestNamespace, codeDeclaration as CodeClass, true); + } + + return new CodeType { + TypeDefinition = codeDeclaration, + Name = className, + }; + } + private static string GetReferenceIdFromOriginalSchema(OpenApiSchema schema, OpenApiSchema parentSchema) { + var title = schema.Title; + if(!string.IsNullOrEmpty(schema.Reference?.Id)) return schema.Reference.Id; + if(parentSchema.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Reference.Id; + if(parentSchema.Items?.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Items.Reference.Id; + return (parentSchema. + AllOf + .FirstOrDefault(x => x.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) ?? + parentSchema. + AnyOf + .FirstOrDefault(x => x.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) ?? + parentSchema. + OneOf + .FirstOrDefault(x => x.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false)) + ?.Reference?.Id; + } + private CodeTypeBase CreateUnionModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, string suffixForInlineSchema) { + var schemas = schema.AnyOf.Union(schema.OneOf); + var unionType = new CodeUnionType { + Name = currentNode.GetClassName(operation: operation, suffix: suffixForInlineSchema, schema: schema), + }; + foreach(var currentSchema in schemas) { + var shortestNamespaceName = currentSchema.Reference == null ? currentNode.GetNodeNamespaceFromPath(config.ClientNamespaceName) : GetModelsNamespaceNameFromReferenceId(currentSchema.Reference.Id); + var shortestNamespace = rootNamespace.FindNamespaceByName(shortestNamespaceName); + if(shortestNamespace == null) + shortestNamespace = rootNamespace.AddNamespace(shortestNamespaceName); + var className = currentSchema.GetSchemaTitle(); + var codeDeclaration = AddModelDeclarationIfDoesntExit(currentNode, currentSchema, className, shortestNamespace); + unionType.AddType(new CodeType { TypeDefinition = codeDeclaration, Name = className, - }; + }); } - private static string GetReferenceIdFromOriginalSchema(OpenApiSchema schema, OpenApiSchema parentSchema) { - var title = schema.Title; - if(!string.IsNullOrEmpty(schema.Reference?.Id)) return schema.Reference.Id; - if(parentSchema.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Reference.Id; - if(parentSchema.Items?.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) return parentSchema.Items.Reference.Id; - return (parentSchema. - AllOf - .FirstOrDefault(x => x.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) ?? - parentSchema. - AnyOf - .FirstOrDefault(x => x.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false) ?? - parentSchema. - OneOf - .FirstOrDefault(x => x.Reference?.Id?.EndsWith(title, StringComparison.OrdinalIgnoreCase) ?? false)) - ?.Reference?.Id; + return unionType; + } + private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, CodeElement parentElement, string suffixForInlineSchema, OpenApiResponse response = default) + { + var codeNamespace = parentElement.GetImmediateParentOfType(); + + if (!schema.IsReferencedSchema() && schema.Properties.Any()) { // Inline schema, i.e. specific to the Operation + return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffixForInlineSchema); + } else if(schema.IsAllOf()) { + return CreateInheritedModelDeclaration(currentNode, schema, operation); + } else if(schema.IsAnyOf() || schema.IsOneOf()) { + return CreateUnionModelDeclaration(currentNode, schema, operation, suffixForInlineSchema); + } else if(schema.IsObject()) { + // referenced schema, no inheritance or union type + var targetNamespace = GetShortestNamespace(codeNamespace, schema); + return CreateModelDeclarationAndType(currentNode, schema, operation, targetNamespace, response: response); + } else if (schema.IsArray()) { + // collections at root + var type = GetPrimitiveType(schema?.Items, string.Empty); + if(type == null) + type = CreateModelDeclarationAndType(currentNode, schema?.Items, operation, codeNamespace); + type.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; + return type; + } else if(!string.IsNullOrEmpty(schema.Type)) + return GetPrimitiveType(schema, string.Empty); + else throw new InvalidOperationException("un handled case, might be object type or array type"); + } + private CodeElement GetExistingDeclaration(bool checkInAllNamespaces, CodeNamespace currentNamespace, OpenApiUrlTreeNode currentNode, string declarationName) { + var searchNameSpace = GetSearchNamespace(checkInAllNamespaces, currentNode, currentNamespace); + return searchNameSpace.FindChildByName(declarationName, checkInAllNamespaces) as CodeElement; + } + private CodeNamespace GetSearchNamespace(bool checkInAllNamespaces, OpenApiUrlTreeNode currentNode, CodeNamespace currentNamespace) { + if(checkInAllNamespaces) return rootNamespace; + else if (currentNode.DoesNodeBelongToItemSubnamespace()) return currentNamespace.EnsureItemNamespace(); + else return currentNamespace; + } + private CodeElement AddModelDeclarationIfDoesntExit(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass inheritsFrom = null, bool checkInAllNamespaces = false) { + var existingDeclaration = GetExistingDeclaration(checkInAllNamespaces, currentNamespace, currentNode, declarationName); + if(existingDeclaration == null) // we can find it in the components + { + if(schema.Enum.Any()) { + var newEnum = new CodeEnum { + Name = declarationName, + Options = schema.Enum.OfType().Select(x => x.Value).Where(x => !"null".Equals(x)).ToList(),//TODO set the flag property + Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel), + }; + return currentNamespace.AddEnum(newEnum).First(); + } else + return AddModelClass(currentNode, schema, declarationName, currentNamespace, inheritsFrom); + } else + return existingDeclaration; + } + private CodeNamespace GetShortestNamespace(CodeNamespace currentNamespace, OpenApiSchema currentSchema) { + if(!string.IsNullOrEmpty(currentSchema.Reference?.Id)) { + var parentClassNamespaceName = GetModelsNamespaceNameFromReferenceId(currentSchema.Reference.Id); + return rootNamespace.AddNamespace(parentClassNamespaceName); } - private CodeTypeBase CreateUnionModelDeclaration(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, string suffixForInlineSchema) { - var schemas = schema.AnyOf.Union(schema.OneOf); - var unionType = new CodeUnionType { - Name = currentNode.GetClassName(operation: operation, suffix: suffixForInlineSchema), - }; - foreach(var currentSchema in schemas) { - var shortestNamespaceName = currentSchema.Reference == null ? currentNode.GetNodeNamespaceFromPath(config.ClientNamespaceName) : GetModelsNamespaceNameFromReferenceId(currentSchema.Reference.Id); - var shortestNamespace = rootNamespace.FindNamespaceByName(shortestNamespaceName); - if(shortestNamespace == null) - shortestNamespace = rootNamespace.AddNamespace(shortestNamespaceName); - var className = currentSchema.GetSchemaTitle(); - var codeDeclaration = AddModelDeclarationIfDoesntExit(currentNode, currentSchema, className, shortestNamespace); - unionType.AddType(new CodeType { - TypeDefinition = codeDeclaration, - Name = className, - }); + return currentNamespace; + } + private CodeClass AddModelClass(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass inheritsFrom = null) { + var referencedAllOfs = schema.AllOf.Where(x => x.Reference != null); + if(inheritsFrom == null && referencedAllOfs.Any()) {// any non-reference would be the current class in some description styles + var parentSchema = referencedAllOfs.FirstOrDefault(); + if(parentSchema != null) { + var parentClassNamespace = GetShortestNamespace(currentNamespace, parentSchema); + inheritsFrom = AddModelDeclarationIfDoesntExit(currentNode, parentSchema, parentSchema.GetSchemaTitle(), parentClassNamespace, null, true) as CodeClass; } - return unionType; } - private CodeTypeBase CreateModelDeclarations(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, OpenApiOperation operation, CodeElement parentElement, string suffixForInlineSchema) + var newClass = currentNamespace.AddClass(new CodeClass { + Name = declarationName, + ClassKind = CodeClassKind.Model, + Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel) + }).First(); + if(inheritsFrom != null) { + var declaration = newClass.StartBlock as CodeClass.Declaration; + declaration.Inherits = new CodeType { TypeDefinition = inheritsFrom, Name = inheritsFrom.Name }; + } + CreatePropertiesForModelClass(currentNode, schema, currentNamespace, newClass); + return newClass; + } + private void CreatePropertiesForModelClass(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, CodeNamespace ns, CodeClass model) { + AddSerializationMembers(model, schema?.AdditionalPropertiesAllowed ?? false, config.UsesBackingStore); + if(schema?.Properties?.Any() ?? false) { - var codeNamespace = parentElement.GetImmediateParentOfType(); - - if (!schema.IsReferencedSchema() && schema.Properties.Any()) { // Inline schema, i.e. specific to the Operation - return CreateModelDeclarationAndType(currentNode, schema, operation, codeNamespace, suffixForInlineSchema); - } else if(schema.IsAllOf()) { - return CreateInheritedModelDeclaration(currentNode, schema, operation); - } else if(schema.IsAnyOf() || schema.IsOneOf()) { - return CreateUnionModelDeclaration(currentNode, schema, operation, suffixForInlineSchema); - } else if(schema.IsObject()) { - // referenced schema, no inheritance or union type - var targetNamespace = GetShortestNamespace(codeNamespace, schema); - return CreateModelDeclarationAndType(currentNode, schema, operation, targetNamespace); - } else if (schema.IsArray()) { - // collections at root - var type = GetPrimitiveType(schema?.Items, string.Empty); - if(type == null) - type = CreateModelDeclarationAndType(currentNode, schema?.Items, operation, codeNamespace); - type.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; - return type; - } else if(!string.IsNullOrEmpty(schema.Type)) - return GetPrimitiveType(schema, string.Empty); - else throw new InvalidOperationException("un handled case, might be object type or array type"); + model.AddProperty(schema + .Properties + .Select(x => { + var propertyDefinitionSchema = x.Value.GetNonEmptySchemas().FirstOrDefault(); + var className = propertyDefinitionSchema.GetSchemaTitle(); + CodeElement definition = default; + if(propertyDefinitionSchema != null) { + if(string.IsNullOrEmpty(className)) + className = $"{model.Name}_{x.Key}"; + var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(propertyDefinitionSchema.Reference?.Id); + var targetNamespace = string.IsNullOrEmpty(shortestNamespaceName) ? ns : + (rootNamespace.FindNamespaceByName(shortestNamespaceName) ?? rootNamespace.AddNamespace(shortestNamespaceName)); + definition = AddModelDeclarationIfDoesntExit(currentNode, propertyDefinitionSchema, className, targetNamespace, null, true); + } + return CreateProperty(x.Key, className ?? x.Value.Type, typeSchema: x.Value, typeDefinition: definition); + }) + .ToArray()); + } + else if(schema?.AllOf?.Any(x => x.IsObject()) ?? false) + CreatePropertiesForModelClass(currentNode, schema.AllOf.Last(x => x.IsObject()), ns, model); + } + private const string FieldDeserializersMethodName = "GetFieldDeserializers"; + private const string SerializeMethodName = "Serialize"; + private const string AdditionalDataPropName = "AdditionalData"; + private const string BackingStorePropertyName = "BackingStore"; + private const string BackingStoreInterface = "IBackingStore"; + private const string BackedModelInterface = "IBackedModel"; + internal static void AddSerializationMembers(CodeClass model, bool includeAdditionalProperties, bool usesBackingStore) { + var serializationPropsType = $"IDictionary>"; + if(!model.ContainsMember(FieldDeserializersMethodName)) { + var deserializeProp = new CodeMethod { + Name = FieldDeserializersMethodName, + MethodKind = CodeMethodKind.Deserializer, + Access = AccessModifier.Public, + Description = "The deserialization information for the current model", + IsAsync = false, + ReturnType = new CodeType { + Name = serializationPropsType, + IsNullable = false, + IsExternal = true, + }, + }; + model.AddMethod(deserializeProp); } - private CodeElement GetExistingDeclaration(bool checkInAllNamespaces, CodeNamespace currentNamespace, OpenApiUrlTreeNode currentNode, string declarationName) { - var searchNameSpace = GetSearchNamespace(checkInAllNamespaces, currentNode, currentNamespace); - return searchNameSpace.FindChildByName(declarationName, checkInAllNamespaces) as CodeElement; + if(!model.ContainsMember(SerializeMethodName)) { + var serializeMethod = new CodeMethod { + Name = SerializeMethodName, + MethodKind = CodeMethodKind.Serializer, + IsAsync = false, + Description = $"Serializes information the current object", + ReturnType = new CodeType { Name = voidType, IsNullable = false, IsExternal = true }, + }; + var parameter = new CodeParameter { + Name = "writer", + Description = "Serialization writer to use to serialize this model", + ParameterKind = CodeParameterKind.Serializer, + Type = new CodeType { Name = "ISerializationWriter", IsExternal = true, IsNullable = false }, + }; + serializeMethod.AddParameter(parameter); + + model.AddMethod(serializeMethod); + } + if(!model.ContainsMember(AdditionalDataPropName) && + includeAdditionalProperties && + !(model.GetGreatestGrandparent(model)?.ContainsMember(AdditionalDataPropName) ?? false)) { + // we don't want to add the property if the parent already has it + var additionalDataProp = new CodeProperty { + Name = AdditionalDataPropName, + Access = AccessModifier.Public, + DefaultValue = "new Dictionary()", + PropertyKind = CodePropertyKind.AdditionalData, + Description = "Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.", + Type = new CodeType { + Name = "IDictionary", + IsNullable = false, + IsExternal = true, + }, + }; + model.AddProperty(additionalDataProp); } - private CodeNamespace GetSearchNamespace(bool checkInAllNamespaces, OpenApiUrlTreeNode currentNode, CodeNamespace currentNamespace) { - if(checkInAllNamespaces) return rootNamespace; - else if (currentNode.DoesNodeBelongToItemSubnamespace()) return currentNamespace.EnsureItemNamespace(); - else return currentNamespace; + if(!model.ContainsMember(BackingStorePropertyName) && + usesBackingStore && + !(model.GetGreatestGrandparent(model)?.ContainsMember(BackingStorePropertyName) ?? false)) { + var backingStoreProperty = new CodeProperty { + Name = BackingStorePropertyName, + Access = AccessModifier.Public, + DefaultValue = $"BackingStoreFactorySingleton.Instance.CreateBackingStore()", + PropertyKind = CodePropertyKind.BackingStore, + Description = "Stores model information.", + ReadOnly = true, + Type = new CodeType { + Name = BackingStoreInterface, + IsNullable = false, + IsExternal = true, + }, + }; + model.AddProperty(backingStoreProperty); + (model.StartBlock as CodeClass.Declaration).AddImplements(new CodeType { + Name = BackedModelInterface, + IsExternal = true, + }); } - private CodeElement AddModelDeclarationIfDoesntExit(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass inheritsFrom = null, bool checkInAllNamespaces = false) { - var existingDeclaration = GetExistingDeclaration(checkInAllNamespaces, currentNamespace, currentNode, declarationName); - if(existingDeclaration == null) // we can find it in the components + } + private CodeClass CreateOperationParameter(OpenApiUrlTreeNode node, OperationType operationType, OpenApiOperation operation) + { + var parameters = node.PathItems[Constants.DefaultOpenApiLabel].Parameters.Union(operation.Parameters).Where(p => p.In == ParameterLocation.Query); + if(parameters.Any()) { + var parameterClass = new CodeClass { - if(schema.Enum.Any()) { - var newEnum = new CodeEnum { - Name = declarationName, - Options = schema.Enum.OfType().Select(x => x.Value).Where(x => !"null".Equals(x)).ToList(),//TODO set the flag property - Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel), - }; - return currentNamespace.AddEnum(newEnum).First(); - } else - return AddModelClass(currentNode, schema, declarationName, currentNamespace, inheritsFrom); - } else - return existingDeclaration; - } - private CodeNamespace GetShortestNamespace(CodeNamespace currentNamespace, OpenApiSchema currentSchema) { - if(!string.IsNullOrEmpty(currentSchema.Reference?.Id)) { - var parentClassNamespaceName = GetModelsNamespaceNameFromReferenceId(currentSchema.Reference.Id); - return rootNamespace.AddNamespace(parentClassNamespaceName); - } - return currentNamespace; - } - private CodeClass AddModelClass(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, string declarationName, CodeNamespace currentNamespace, CodeClass inheritsFrom = null) { - var referencedAllOfs = schema.AllOf.Where(x => x.Reference != null); - if(inheritsFrom == null && referencedAllOfs.Any()) {// any non-reference would be the current class in some description styles - var parentSchema = referencedAllOfs.FirstOrDefault(); - if(parentSchema != null) { - var parentClassNamespace = GetShortestNamespace(currentNamespace, parentSchema); - inheritsFrom = AddModelDeclarationIfDoesntExit(currentNode, parentSchema, parentSchema.GetSchemaTitle(), parentClassNamespace, null, true) as CodeClass; - } - } - var newClass = currentNamespace.AddClass(new CodeClass { - Name = declarationName, - ClassKind = CodeClassKind.Model, - Description = currentNode.GetPathItemDescription(Constants.DefaultOpenApiLabel) - }).First(); - if(inheritsFrom != null) { - var declaration = newClass.StartBlock as CodeClass.Declaration; - declaration.Inherits = new CodeType { TypeDefinition = inheritsFrom, Name = inheritsFrom.Name }; - } - CreatePropertiesForModelClass(currentNode, schema, currentNamespace, newClass); - return newClass; - } - private void CreatePropertiesForModelClass(OpenApiUrlTreeNode currentNode, OpenApiSchema schema, CodeNamespace ns, CodeClass model) { - AddSerializationMembers(model, schema?.AdditionalPropertiesAllowed ?? false, config.UsesBackingStore); - if(schema?.Properties?.Any() ?? false) + Name = operationType.ToString() + "QueryParameters", + ClassKind = CodeClassKind.QueryParameters, + Description = operation.Description ?? operation.Summary + }; + foreach (var parameter in parameters) { - model.AddProperty(schema - .Properties - .Select(x => { - var propertyDefinitionSchema = x.Value.GetNonEmptySchemas().FirstOrDefault(); - var className = propertyDefinitionSchema.GetSchemaTitle(); - CodeElement definition = default; - if(propertyDefinitionSchema != null) { - if(string.IsNullOrEmpty(className)) - className = $"{model.Name}_{x.Key}"; - var shortestNamespaceName = GetModelsNamespaceNameFromReferenceId(propertyDefinitionSchema.Reference?.Id); - var targetNamespace = string.IsNullOrEmpty(shortestNamespaceName) ? ns : - (rootNamespace.FindNamespaceByName(shortestNamespaceName) ?? rootNamespace.AddNamespace(shortestNamespaceName)); - definition = AddModelDeclarationIfDoesntExit(currentNode, propertyDefinitionSchema, className, targetNamespace, null, true); - } - return CreateProperty(x.Key, className ?? x.Value.Type, typeSchema: x.Value, typeDefinition: definition); - }) - .ToArray()); - } - else if(schema?.AllOf?.Any(x => x.IsObject()) ?? false) - CreatePropertiesForModelClass(currentNode, schema.AllOf.Last(x => x.IsObject()), ns, model); - } - private const string FieldDeserializersMethodName = "GetFieldDeserializers"; - private const string SerializeMethodName = "Serialize"; - private const string AdditionalDataPropName = "AdditionalData"; - private const string BackingStorePropertyName = "BackingStore"; - private const string BackingStoreInterface = "IBackingStore"; - private const string BackedModelInterface = "IBackedModel"; - internal static void AddSerializationMembers(CodeClass model, bool includeAdditionalProperties, bool usesBackingStore) { - var serializationPropsType = $"IDictionary>"; - if(!model.ContainsMember(FieldDeserializersMethodName)) { - var deserializeProp = new CodeMethod { - Name = FieldDeserializersMethodName, - MethodKind = CodeMethodKind.Deserializer, - Access = AccessModifier.Public, - Description = "The deserialization information for the current model", - IsAsync = false, - ReturnType = new CodeType { - Name = serializationPropsType, - IsNullable = false, - IsExternal = true, - }, - }; - model.AddMethod(deserializeProp); - } - if(!model.ContainsMember(SerializeMethodName)) { - var serializeMethod = new CodeMethod { - Name = SerializeMethodName, - MethodKind = CodeMethodKind.Serializer, - IsAsync = false, - Description = $"Serializes information the current object", - ReturnType = new CodeType { Name = voidType, IsNullable = false, IsExternal = true }, - }; - var parameter = new CodeParameter { - Name = "writer", - Description = "Serialization writer to use to serialize this model", - ParameterKind = CodeParameterKind.Serializer, - Type = new CodeType { Name = "ISerializationWriter", IsExternal = true, IsNullable = false }, - }; - serializeMethod.AddParameter(parameter); - - model.AddMethod(serializeMethod); - } - if(!model.ContainsMember(AdditionalDataPropName) && - includeAdditionalProperties && - !(model.GetGreatestGrandparent(model)?.ContainsMember(AdditionalDataPropName) ?? false)) { - // we don't want to add the property if the parent already has it - var additionalDataProp = new CodeProperty { - Name = AdditionalDataPropName, - Access = AccessModifier.Public, - DefaultValue = "new Dictionary()", - PropertyKind = CodePropertyKind.AdditionalData, - Description = "Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.", - Type = new CodeType { - Name = "IDictionary", - IsNullable = false, - IsExternal = true, - }, - }; - model.AddProperty(additionalDataProp); - } - if(!model.ContainsMember(BackingStorePropertyName) && - usesBackingStore && - !(model.GetGreatestGrandparent(model)?.ContainsMember(BackingStorePropertyName) ?? false)) { - var backingStoreProperty = new CodeProperty { - Name = BackingStorePropertyName, - Access = AccessModifier.Public, - DefaultValue = $"BackingStoreFactorySingleton.Instance.CreateBackingStore()", - PropertyKind = CodePropertyKind.BackingStore, - Description = "Stores model information.", - ReadOnly = true, - Type = new CodeType { - Name = BackingStoreInterface, - IsNullable = false, + var prop = new CodeProperty + { + Name = FixQueryParameterIdentifier(parameter), + Description = parameter.Description, + Type = new CodeType + { IsExternal = true, + Name = parameter.Schema.Items?.Type ?? parameter.Schema.Type, + CollectionKind = parameter.Schema.IsArray() ? CodeType.CodeTypeCollectionKind.Array : default, }, }; - model.AddProperty(backingStoreProperty); - (model.StartBlock as CodeClass.Declaration).AddImplements(new CodeType { - Name = BackedModelInterface, - IsExternal = true, - }); - } - } - private CodeClass CreateOperationParameter(OpenApiUrlTreeNode node, OperationType operationType, OpenApiOperation operation) - { - var parameters = node.PathItems[Constants.DefaultOpenApiLabel].Parameters.Union(operation.Parameters).Where(p => p.In == ParameterLocation.Query); - if(parameters.Any()) { - var parameterClass = new CodeClass + + if (!parameterClass.ContainsMember(parameter.Name)) { - Name = operationType.ToString() + "QueryParameters", - ClassKind = CodeClassKind.QueryParameters, - Description = operation.Description ?? operation.Summary - }; - foreach (var parameter in parameters) + parameterClass.AddProperty(prop); + } + else { - var prop = new CodeProperty - { - Name = FixQueryParameterIdentifier(parameter), - Description = parameter.Description, - Type = new CodeType - { - IsExternal = true, - Name = parameter.Schema.Items?.Type ?? parameter.Schema.Type, - CollectionKind = parameter.Schema.IsArray() ? CodeType.CodeTypeCollectionKind.Array : default, - }, - }; - - if (!parameterClass.ContainsMember(parameter.Name)) - { - parameterClass.AddProperty(prop); - } - else - { - logger.LogWarning("Ignoring duplicate parameter {name}", parameter.Name); - } + logger.LogWarning("Ignoring duplicate parameter {name}", parameter.Name); } + } - return parameterClass; - } else return null; - } + return parameterClass; + } else return null; + } - private static string FixQueryParameterIdentifier(OpenApiParameter parameter) - { - // Replace with regexes pulled from settings that are API specific + private static string FixQueryParameterIdentifier(OpenApiParameter parameter) + { + // Replace with regexes pulled from settings that are API specific - return parameter.Name.Replace("$","").ToCamelCase(); - } + return parameter.Name.Replace("$","").ToCamelCase(); } } diff --git a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs index 16c3c23d09..c9ad5d1052 100644 --- a/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs +++ b/tests/Kiota.Builder.Tests/KiotaBuilderTests.cs @@ -508,4 +508,219 @@ public void MapsDuration(){ var progressProp = codeModel.FindChildByName("progress", true); Assert.Equal("TimeSpan", progressProp.Type.Name); } + [Fact] + public void AddsErrorMapping(){ + var node = OpenApiUrlTreeNode.Create(); + node.Attach("tasks", new OpenApiPathItem() { + Operations = { + [OperationType.Get] = new OpenApiOperation() { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse() + { + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "progress", new OpenApiSchema{ + Type = "string", + } + } + } + } + } + } + }, + ["4XX"] = new OpenApiResponse() + { + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "errorId", new OpenApiSchema{ + Type = "string", + } + } + } + } + } + } + }, + ["5XX"] = new OpenApiResponse() + { + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "serviceErrorId", new OpenApiSchema{ + Type = "string", + } + } + } + } + } + } + }, + ["401"] = new OpenApiResponse() + { + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "authenticationRealm", new OpenApiSchema{ + Type = "string", + } + } + } + } + } + } + } + } + } + } + }, "default"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration() { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }); + var codeModel = builder.CreateSourceModel(node); + var executorMethod = codeModel.FindChildByName("get", true); + Assert.NotNull(executorMethod); + Assert.NotEmpty(executorMethod.ErrorMappings); + Assert.Contains("4XX", executorMethod.ErrorMappings.Keys); + Assert.Contains("401", executorMethod.ErrorMappings.Keys); + Assert.Contains("5XX", executorMethod.ErrorMappings.Keys); + var errorType401 = codeModel.FindChildByName("tasks401Error", true); + Assert.NotNull(errorType401); + Assert.True(errorType401.IsErrorDefinition); + Assert.NotNull(errorType401.FindChildByName("authenticationRealm", true)); + var errorType4XX = codeModel.FindChildByName("tasks4XXError", true); + Assert.NotNull(errorType4XX); + Assert.True(errorType4XX.IsErrorDefinition); + Assert.NotNull(errorType4XX.FindChildByName("errorId", true)); + var errorType5XX = codeModel.FindChildByName("tasks5XXError", true); + Assert.NotNull(errorType5XX); + Assert.True(errorType5XX.IsErrorDefinition); + Assert.NotNull(errorType5XX.FindChildByName("serviceErrorId", true)); + + } + [Fact] + public void DoesntAddSuffixesToErrorTypesWhenComponents(){ + var errorSchema = new OpenApiSchema { + Type = "object", + Properties = new Dictionary { + { + "errorId", new OpenApiSchema { + Type = "string" + } + } + }, + Reference = new OpenApiReference { + Id = "microsoft.graph.error", + Type = ReferenceType.Schema + }, + UnresolvedReference = false + }; + var errorResponse = new OpenApiResponse() + { + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = errorSchema + } + }, + Reference = new OpenApiReference { + Id = "microsoft.graph.error", + Type = ReferenceType.Response + }, + UnresolvedReference = false + }; + var document = new OpenApiDocument() { + Paths = new OpenApiPaths() { + ["tasks"] = new OpenApiPathItem() { + Operations = { + [OperationType.Get] = new OpenApiOperation() { + Responses = new OpenApiResponses + { + ["200"] = new OpenApiResponse() + { + Content = + { + ["application/json"] = new OpenApiMediaType() + { + Schema = new OpenApiSchema + { + Type = "object", + Properties = new Dictionary { + { + "progress", new OpenApiSchema{ + Type = "string", + } + } + } + } + } + } + }, + ["4XX"] = errorResponse, + ["5XX"] = errorResponse, + ["401"] = errorResponse + } + } + } + } + }, + Components = new OpenApiComponents() { + Schemas = new Dictionary { + { + "microsoft.graph.error", errorSchema + } + }, + Responses = new Dictionary { + { + "microsoft.graph.error", errorResponse + } + } + }, + }; + var node = OpenApiUrlTreeNode.Create(document, "default"); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration() { ClientClassName = "Graph", ApiRootUrl = "https://localhost" }); + var codeModel = builder.CreateSourceModel(node); + var executorMethod = codeModel.FindChildByName("get", true); + Assert.NotNull(executorMethod); + Assert.NotEmpty(executorMethod.ErrorMappings); + Assert.Contains("4XX", executorMethod.ErrorMappings.Keys); + Assert.Contains("401", executorMethod.ErrorMappings.Keys); + Assert.Contains("5XX", executorMethod.ErrorMappings.Keys); + var errorType = codeModel.FindChildByName("Error", true); + Assert.NotNull(errorType); + Assert.True(errorType.IsErrorDefinition); + Assert.NotNull(errorType.FindChildByName("errorId", true)); + + Assert.Null(codeModel.FindChildByName("tasks401Error", true)); + Assert.Null(codeModel.FindChildByName("tasks4XXError", true)); + Assert.Null(codeModel.FindChildByName("tasks5XXError", true)); + } + } From d45a713c9438dea41ac0448c936e5d304c561dc4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 2 Feb 2022 14:35:10 -0500 Subject: [PATCH 02/36] - adds support for creating the error mapping in executor method in CSharp Signed-off-by: Vincent Biret --- .../Writers/CSharp/CodeMethodWriter.cs | 552 ++++++----- .../Writers/CSharp/CodeMethodWriterTests.cs | 929 +++++++++--------- 2 files changed, 759 insertions(+), 722 deletions(-) diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index f0dad7133f..2a8e050102 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -2,299 +2,307 @@ using System.Collections.Generic; using System.Linq; using Kiota.Builder.Extensions; -using Kiota.Builder.Writers.Extensions; -namespace Kiota.Builder.Writers.CSharp { - public class CodeMethodWriter : BaseElementWriter +namespace Kiota.Builder.Writers.CSharp; +public class CodeMethodWriter : BaseElementWriter +{ + public CodeMethodWriter(CSharpConventionService conventionService): base(conventionService) { } + public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter writer) { - public CodeMethodWriter(CSharpConventionService conventionService): base(conventionService) { } - public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter writer) - { - if(codeElement == null) throw new ArgumentNullException(nameof(codeElement)); - if(codeElement.ReturnType == null) throw new InvalidOperationException($"{nameof(codeElement.ReturnType)} should not be null"); - if(writer == null) throw new ArgumentNullException(nameof(writer)); - if(!(codeElement.Parent is CodeClass)) throw new InvalidOperationException("the parent of a method should be a class"); + if(codeElement == null) throw new ArgumentNullException(nameof(codeElement)); + if(codeElement.ReturnType == null) throw new InvalidOperationException($"{nameof(codeElement.ReturnType)} should not be null"); + if(writer == null) throw new ArgumentNullException(nameof(writer)); + if(!(codeElement.Parent is CodeClass)) throw new InvalidOperationException("the parent of a method should be a class"); - var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); - var parentClass = codeElement.Parent as CodeClass; - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; - var isVoid = conventions.VoidTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); - WriteMethodDocumentation(codeElement, writer); - WriteMethodPrototype(codeElement, writer, returnType, inherits, isVoid); - writer.IncreaseIndent(); - var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); - var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); - var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); - var optionsParam = codeElement.Parameters.OfKind(CodeParameterKind.Options); - var requestParams = new RequestParams(requestBodyParam, queryStringParam, headersParam, optionsParam); - foreach(var parameter in codeElement.Parameters.Where(x => !x.Optional).OrderBy(x => x.Name)) { - var parameterName = parameter.Name.ToFirstCharacterLowerCase(); - if(nameof(String).Equals(parameter.Type.Name, StringComparison.OrdinalIgnoreCase)) - writer.WriteLine($"if(string.IsNullOrEmpty({parameterName})) throw new ArgumentNullException(nameof({parameterName}));"); - else - writer.WriteLine($"_ = {parameterName} ?? throw new ArgumentNullException(nameof({parameterName}));"); - } - switch(codeElement.MethodKind) { - case CodeMethodKind.Serializer: - WriteSerializerBody(inherits, codeElement, parentClass, writer); + var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); + var parentClass = codeElement.Parent as CodeClass; + var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + var isVoid = conventions.VoidTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); + WriteMethodDocumentation(codeElement, writer); + WriteMethodPrototype(codeElement, writer, returnType, inherits, isVoid); + writer.IncreaseIndent(); + var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); + var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); + var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); + var optionsParam = codeElement.Parameters.OfKind(CodeParameterKind.Options); + var requestParams = new RequestParams(requestBodyParam, queryStringParam, headersParam, optionsParam); + foreach(var parameter in codeElement.Parameters.Where(x => !x.Optional).OrderBy(x => x.Name)) { + var parameterName = parameter.Name.ToFirstCharacterLowerCase(); + if(nameof(String).Equals(parameter.Type.Name, StringComparison.OrdinalIgnoreCase)) + writer.WriteLine($"if(string.IsNullOrEmpty({parameterName})) throw new ArgumentNullException(nameof({parameterName}));"); + else + writer.WriteLine($"_ = {parameterName} ?? throw new ArgumentNullException(nameof({parameterName}));"); + } + switch(codeElement.MethodKind) { + case CodeMethodKind.Serializer: + WriteSerializerBody(inherits, codeElement, parentClass, writer); + break; + case CodeMethodKind.RequestGenerator: + WriteRequestGeneratorBody(codeElement, requestParams, parentClass, writer); break; - case CodeMethodKind.RequestGenerator: - WriteRequestGeneratorBody(codeElement, requestParams, parentClass, writer); - break; - case CodeMethodKind.RequestExecutor: - WriteRequestExecutorBody(codeElement, requestParams, isVoid, returnType, writer); - break; - case CodeMethodKind.Deserializer: - WriteDeserializerBody(codeElement, parentClass, writer); - break; - case CodeMethodKind.ClientConstructor: - WriteConstructorBody(parentClass, codeElement, writer); - WriteApiConstructorBody(parentClass, codeElement, writer); - break; - case CodeMethodKind.Constructor: - case CodeMethodKind.RawUrlConstructor: - WriteConstructorBody(parentClass, codeElement, writer); - break; - case CodeMethodKind.RequestBuilderWithParameters: - WriteRequestBuilderBody(parentClass, codeElement, writer); - break; - case CodeMethodKind.Getter: - case CodeMethodKind.Setter: - throw new InvalidOperationException("getters and setters are automatically added on fields in dotnet"); - case CodeMethodKind.RequestBuilderBackwardCompatibility: - throw new InvalidOperationException("RequestBuilderBackwardCompatibility is not supported as the request builders are implemented by properties."); - default: - writer.WriteLine("return null;"); + case CodeMethodKind.RequestExecutor: + WriteRequestExecutorBody(codeElement, requestParams, isVoid, returnType, writer); break; - } - writer.DecreaseIndent(); - writer.WriteLine("}"); - } - private void WriteRequestBuilderBody(CodeClass parentClass, CodeMethod codeElement, LanguageWriter writer) - { - var importSymbol = conventions.GetTypeString(codeElement.ReturnType, parentClass); - conventions.AddRequestBuilderBody(parentClass, importSymbol, writer, prefix: "return ", pathParameters: codeElement.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Path))); - } - private static void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) { - var requestAdapterProperty = parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter); - var backingStoreParameter = method.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.BackingStore)); - var requestAdapterPropertyName = requestAdapterProperty.Name.ToFirstCharacterUpperCase(); - WriteSerializationRegistration(method.SerializerModules, writer, "RegisterDefaultSerializer"); - WriteSerializationRegistration(method.DeserializerModules, writer, "RegisterDefaultDeserializer"); - writer.WriteLine($"{requestAdapterPropertyName}.BaseUrl = \"{method.BaseUrl}\";"); - if(backingStoreParameter != null) - writer.WriteLine($"{requestAdapterPropertyName}.EnableBackingStore({backingStoreParameter.Name});"); + case CodeMethodKind.Deserializer: + WriteDeserializerBody(codeElement, parentClass, writer); + break; + case CodeMethodKind.ClientConstructor: + WriteConstructorBody(parentClass, codeElement, writer); + WriteApiConstructorBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.Constructor: + case CodeMethodKind.RawUrlConstructor: + WriteConstructorBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.RequestBuilderWithParameters: + WriteRequestBuilderBody(parentClass, codeElement, writer); + break; + case CodeMethodKind.Getter: + case CodeMethodKind.Setter: + throw new InvalidOperationException("getters and setters are automatically added on fields in dotnet"); + case CodeMethodKind.RequestBuilderBackwardCompatibility: + throw new InvalidOperationException("RequestBuilderBackwardCompatibility is not supported as the request builders are implemented by properties."); + default: + writer.WriteLine("return null;"); + break; } - private static void WriteSerializationRegistration(List serializationClassNames, LanguageWriter writer, string methodName) { - if(serializationClassNames != null) - foreach(var serializationClassName in serializationClassNames) - writer.WriteLine($"ApiClientBuilder.{methodName}<{serializationClassName}>();"); + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + private void WriteRequestBuilderBody(CodeClass parentClass, CodeMethod codeElement, LanguageWriter writer) + { + var importSymbol = conventions.GetTypeString(codeElement.ReturnType, parentClass); + conventions.AddRequestBuilderBody(parentClass, importSymbol, writer, prefix: "return ", pathParameters: codeElement.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Path))); + } + private static void WriteApiConstructorBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) { + var requestAdapterProperty = parentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter); + var backingStoreParameter = method.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.BackingStore)); + var requestAdapterPropertyName = requestAdapterProperty.Name.ToFirstCharacterUpperCase(); + WriteSerializationRegistration(method.SerializerModules, writer, "RegisterDefaultSerializer"); + WriteSerializationRegistration(method.DeserializerModules, writer, "RegisterDefaultDeserializer"); + writer.WriteLine($"{requestAdapterPropertyName}.BaseUrl = \"{method.BaseUrl}\";"); + if(backingStoreParameter != null) + writer.WriteLine($"{requestAdapterPropertyName}.EnableBackingStore({backingStoreParameter.Name});"); + } + private static void WriteSerializationRegistration(List serializationClassNames, LanguageWriter writer, string methodName) { + if(serializationClassNames != null) + foreach(var serializationClassName in serializationClassNames) + writer.WriteLine($"ApiClientBuilder.{methodName}<{serializationClassName}>();"); + } + private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer) { + foreach(var propWithDefault in parentClass + .Properties + .Where(x => !string.IsNullOrEmpty(x.DefaultValue)) + .OrderByDescending(x => x.PropertyKind) + .ThenBy(x => x.Name)) { + writer.WriteLine($"{propWithDefault.Name.ToFirstCharacterUpperCase()} = {propWithDefault.DefaultValue};"); } - private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer) { - foreach(var propWithDefault in parentClass - .Properties - .Where(x => !string.IsNullOrEmpty(x.DefaultValue)) - .OrderByDescending(x => x.PropertyKind) - .ThenBy(x => x.Name)) { - writer.WriteLine($"{propWithDefault.Name.ToFirstCharacterUpperCase()} = {propWithDefault.DefaultValue};"); + if(parentClass.IsOfKind(CodeClassKind.RequestBuilder)) { + if(currentMethod.IsOfKind(CodeMethodKind.Constructor)) { + var pathParametersParam = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.PathParameters)); + conventions.AddParametersAssignment(writer, + pathParametersParam.Type, + pathParametersParam.Name.ToFirstCharacterLowerCase(), + currentMethod.Parameters + .Where(x => x.IsOfKind(CodeParameterKind.Path)) + .Select(x => (x.Type, x.UrlTemplateParameterName, x.Name.ToFirstCharacterLowerCase())) + .ToArray()); + AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.PathParameters, CodePropertyKind.PathParameters, writer, conventions.TempDictionaryVarName); } - if(parentClass.IsOfKind(CodeClassKind.RequestBuilder)) { - if(currentMethod.IsOfKind(CodeMethodKind.Constructor)) { - var pathParametersParam = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.PathParameters)); - conventions.AddParametersAssignment(writer, - pathParametersParam.Type, - pathParametersParam.Name.ToFirstCharacterLowerCase(), - currentMethod.Parameters - .Where(x => x.IsOfKind(CodeParameterKind.Path)) - .Select(x => (x.Type, x.UrlTemplateParameterName, x.Name.ToFirstCharacterLowerCase())) - .ToArray()); - AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.PathParameters, CodePropertyKind.PathParameters, writer, conventions.TempDictionaryVarName); - } - else if(currentMethod.IsOfKind(CodeMethodKind.RawUrlConstructor)) { - var pathParametersProp = parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); - var rawUrlParam = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.RawUrl)); - conventions.AddParametersAssignment(writer, - pathParametersProp.Type, - string.Empty, - (rawUrlParam.Type, Constants.RawUrlParameterName, rawUrlParam.Name.ToFirstCharacterLowerCase())); - AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.PathParameters, CodePropertyKind.PathParameters, writer, conventions.TempDictionaryVarName); - } - AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.RequestAdapter, CodePropertyKind.RequestAdapter, writer); + else if(currentMethod.IsOfKind(CodeMethodKind.RawUrlConstructor)) { + var pathParametersProp = parentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); + var rawUrlParam = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.RawUrl)); + conventions.AddParametersAssignment(writer, + pathParametersProp.Type, + string.Empty, + (rawUrlParam.Type, Constants.RawUrlParameterName, rawUrlParam.Name.ToFirstCharacterLowerCase())); + AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.PathParameters, CodePropertyKind.PathParameters, writer, conventions.TempDictionaryVarName); } + AssignPropertyFromParameter(parentClass, currentMethod, CodeParameterKind.RequestAdapter, CodePropertyKind.RequestAdapter, writer); } - private static void AssignPropertyFromParameter(CodeClass parentClass, CodeMethod currentMethod, CodeParameterKind parameterKind, CodePropertyKind propertyKind, LanguageWriter writer, string variableName = default) { - var property = parentClass.GetPropertyOfKind(propertyKind); - if(property != null) { - var parameter = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(parameterKind)); - if(!string.IsNullOrEmpty(variableName)) - writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {variableName};"); - else if(parameter != null) - writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {parameter.Name};"); - } + } + private static void AssignPropertyFromParameter(CodeClass parentClass, CodeMethod currentMethod, CodeParameterKind parameterKind, CodePropertyKind propertyKind, LanguageWriter writer, string variableName = default) { + var property = parentClass.GetPropertyOfKind(propertyKind); + if(property != null) { + var parameter = currentMethod.Parameters.FirstOrDefault(x => x.IsOfKind(parameterKind)); + if(!string.IsNullOrEmpty(variableName)) + writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {variableName};"); + else if(parameter != null) + writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {parameter.Name};"); } - private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { - var hideParentMember = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; - var parentSerializationInfo = hideParentMember ? $"(base.{codeElement.Name.ToFirstCharacterUpperCase()}())" : string.Empty; - writer.WriteLine($"return new Dictionary>{parentSerializationInfo} {{"); - writer.IncreaseIndent(); - foreach(var otherProp in parentClass - .Properties - .Where(x => x.IsOfKind(CodePropertyKind.Custom)) - .OrderBy(x => x.Name)) { - writer.WriteLine($"{{\"{otherProp.SerializationName ?? otherProp.Name.ToFirstCharacterLowerCase()}\", (o,n) => {{ (o as {parentClass.Name.ToFirstCharacterUpperCase()}).{otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}(); }} }},"); - } - writer.DecreaseIndent(); - writer.WriteLine("};"); + } + private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { + var hideParentMember = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + var parentSerializationInfo = hideParentMember ? $"(base.{codeElement.Name.ToFirstCharacterUpperCase()}())" : string.Empty; + writer.WriteLine($"return new Dictionary>{parentSerializationInfo} {{"); + writer.IncreaseIndent(); + foreach(var otherProp in parentClass + .Properties + .Where(x => x.IsOfKind(CodePropertyKind.Custom)) + .OrderBy(x => x.Name)) { + writer.WriteLine($"{{\"{otherProp.SerializationName ?? otherProp.Name.ToFirstCharacterLowerCase()}\", (o,n) => {{ (o as {parentClass.Name.ToFirstCharacterUpperCase()}).{otherProp.Name.ToFirstCharacterUpperCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeElement)}(); }} }},"); } - private string GetDeserializationMethodName(CodeTypeBase propType, CodeMethod method) { - var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; - var propertyType = conventions.GetTypeString(propType, method, false); - if(propType is CodeType currentType) { - if(isCollection) - if(currentType.TypeDefinition == null) - return $"GetCollectionOfPrimitiveValues<{propertyType}>().ToList"; - else if (currentType.TypeDefinition is CodeEnum enumType) - return $"GetCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>().ToList"; - else - return $"GetCollectionOfObjectValues<{propertyType}>().ToList"; + writer.DecreaseIndent(); + writer.WriteLine("};"); + } + private string GetDeserializationMethodName(CodeTypeBase propType, CodeMethod method) { + var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; + var propertyType = conventions.GetTypeString(propType, method, false); + if(propType is CodeType currentType) { + if(isCollection) + if(currentType.TypeDefinition == null) + return $"GetCollectionOfPrimitiveValues<{propertyType}>().ToList"; else if (currentType.TypeDefinition is CodeEnum enumType) - return $"GetEnumValue<{enumType.Name.ToFirstCharacterUpperCase()}>"; - } - return propertyType switch - { - "byte[]" => "GetByteArrayValue", - _ when conventions.IsPrimitiveType(propertyType) => $"Get{propertyType.TrimEnd(CSharpConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value", - _ => $"GetObjectValue<{propertyType.ToFirstCharacterUpperCase()}>", - }; - } - private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) { - if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); - - var isStream = conventions.StreamTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); - var generatorMethodName = (codeElement.Parent as CodeClass) - .Methods - .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod) - ?.Name; - var parametersList = new CodeParameter[] { requestParams.requestBody, requestParams.queryString, requestParams.headers, requestParams.options } - .Select(x => x?.Name).Where(x => x != null).Aggregate((x,y) => $"{x}, {y}"); - writer.WriteLine($"var requestInfo = {generatorMethodName}({parametersList});"); - writer.WriteLine($"{(isVoid ? string.Empty : "return ")}await RequestAdapter.{GetSendRequestMethodName(isVoid, isStream, codeElement.ReturnType.IsCollection, returnType)}(requestInfo, responseHandler, cancellationToken);"); + return $"GetCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>().ToList"; + else + return $"GetCollectionOfObjectValues<{propertyType}>().ToList"; + else if (currentType.TypeDefinition is CodeEnum enumType) + return $"GetEnumValue<{enumType.Name.ToFirstCharacterUpperCase()}>"; } - private const string RequestInfoVarName = "requestInfo"; - private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer) { - if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); - - var operationName = codeElement.HttpMethod.ToString(); - var urlTemplateParamsProperty = currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); - var urlTemplateProperty = currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate); - var requestAdapterProperty = currentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter); - writer.WriteLine($"var {RequestInfoVarName} = new RequestInformation {{"); + return propertyType switch + { + "byte[]" => "GetByteArrayValue", + _ when conventions.IsPrimitiveType(propertyType) => $"Get{propertyType.TrimEnd(CSharpConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value", + _ => $"GetObjectValue<{propertyType.ToFirstCharacterUpperCase()}>", + }; + } + private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requestParams, bool isVoid, string returnType, LanguageWriter writer) { + if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); + + var isStream = conventions.StreamTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); + var generatorMethodName = (codeElement.Parent as CodeClass) + .Methods + .FirstOrDefault(x => x.IsOfKind(CodeMethodKind.RequestGenerator) && x.HttpMethod == codeElement.HttpMethod) + ?.Name; + var parametersList = new CodeParameter[] { requestParams.requestBody, requestParams.queryString, requestParams.headers, requestParams.options } + .Select(x => x?.Name).Where(x => x != null).Aggregate((x,y) => $"{x}, {y}"); + writer.WriteLine($"var requestInfo = {generatorMethodName}({parametersList});"); + var errorMappingVarName = "default"; + if(codeElement.ErrorMappings.Any()) { + errorMappingVarName = "errorMapping"; + writer.WriteLine($"var {errorMappingVarName} = new Dictionary> {{"); writer.IncreaseIndent(); - writer.WriteLines($"HttpMethod = Method.{operationName?.ToUpperInvariant()},", - $"UrlTemplate = {GetPropertyCall(urlTemplateProperty, "string.Empty")},", - $"PathParameters = {GetPropertyCall(urlTemplateParamsProperty, "string.Empty")},"); - writer.DecreaseIndent(); - writer.WriteLine("};"); - if(requestParams.requestBody != null) { - if(requestParams.requestBody.Type.Name.Equals(conventions.StreamTypeName, StringComparison.OrdinalIgnoreCase)) - writer.WriteLine($"{RequestInfoVarName}.SetStreamContent({requestParams.requestBody.Name});"); - else - writer.WriteLine($"{RequestInfoVarName}.SetContentFromParsable({requestAdapterProperty.Name.ToFirstCharacterUpperCase()}, \"{codeElement.ContentType}\", {requestParams.requestBody.Name});"); + foreach(var errorMapping in codeElement.ErrorMappings) { + writer.WriteLine($"{{\"{errorMapping.Key.ToUpperInvariant()}\", () => new {errorMapping.Value.Name.ToFirstCharacterUpperCase()}()}},"); } - if(requestParams.queryString != null) { - writer.WriteLine($"if ({requestParams.queryString.Name} != null) {{"); - writer.IncreaseIndent(); - writer.WriteLines($"var qParams = new {operationName?.ToFirstCharacterUpperCase()}QueryParameters();", - $"{requestParams.queryString.Name}.Invoke(qParams);", - $"qParams.AddQueryParameters({RequestInfoVarName}.QueryParameters);"); - writer.DecreaseIndent(); - writer.WriteLine("}"); - } - if(requestParams.headers != null) - writer.WriteLine($"{requestParams.headers.Name}?.Invoke({RequestInfoVarName}.Headers);"); - if(requestParams.options != null) - writer.WriteLine($"{RequestInfoVarName}.AddRequestOptions({requestParams.options.Name}?.ToArray());"); - writer.WriteLine($"return {RequestInfoVarName};"); + writer.CloseBlock("};"); } - private static string GetPropertyCall(CodeProperty property, string defaultValue) => property == null ? defaultValue : $"{property.Name.ToFirstCharacterUpperCase()}"; - private void WriteSerializerBody(bool shouldHide, CodeMethod method, CodeClass parentClass, LanguageWriter writer) { - var additionalDataProperty = parentClass.GetPropertyOfKind(CodePropertyKind.AdditionalData); - if(shouldHide) - writer.WriteLine("base.Serialize(writer);"); - foreach(var otherProp in parentClass - .Properties - .Where(x => x.IsOfKind(CodePropertyKind.Custom)) - .OrderBy(x => x.Name)) { - writer.WriteLine($"writer.{GetSerializationMethodName(otherProp.Type, method)}(\"{otherProp.SerializationName ?? otherProp.Name.ToFirstCharacterLowerCase()}\", {otherProp.Name.ToFirstCharacterUpperCase()});"); - } - if(additionalDataProperty != null) - writer.WriteLine($"writer.WriteAdditionalData({additionalDataProperty.Name});"); + writer.WriteLine($"{(isVoid ? string.Empty : "return ")}await RequestAdapter.{GetSendRequestMethodName(isVoid, isStream, codeElement.ReturnType.IsCollection, returnType)}(requestInfo, responseHandler, {errorMappingVarName}, cancellationToken);"); + } + private const string RequestInfoVarName = "requestInfo"; + private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams requestParams, CodeClass currentClass, LanguageWriter writer) { + if(codeElement.HttpMethod == null) throw new InvalidOperationException("http method cannot be null"); + + var operationName = codeElement.HttpMethod.ToString(); + var urlTemplateParamsProperty = currentClass.GetPropertyOfKind(CodePropertyKind.PathParameters); + var urlTemplateProperty = currentClass.GetPropertyOfKind(CodePropertyKind.UrlTemplate); + var requestAdapterProperty = currentClass.GetPropertyOfKind(CodePropertyKind.RequestAdapter); + writer.WriteLine($"var {RequestInfoVarName} = new RequestInformation {{"); + writer.IncreaseIndent(); + writer.WriteLines($"HttpMethod = Method.{operationName?.ToUpperInvariant()},", + $"UrlTemplate = {GetPropertyCall(urlTemplateProperty, "string.Empty")},", + $"PathParameters = {GetPropertyCall(urlTemplateParamsProperty, "string.Empty")},"); + writer.DecreaseIndent(); + writer.WriteLine("};"); + if(requestParams.requestBody != null) { + if(requestParams.requestBody.Type.Name.Equals(conventions.StreamTypeName, StringComparison.OrdinalIgnoreCase)) + writer.WriteLine($"{RequestInfoVarName}.SetStreamContent({requestParams.requestBody.Name});"); + else + writer.WriteLine($"{RequestInfoVarName}.SetContentFromParsable({requestAdapterProperty.Name.ToFirstCharacterUpperCase()}, \"{codeElement.ContentType}\", {requestParams.requestBody.Name});"); } - private string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCollection, string returnType) { - if(isVoid) return "SendNoContentAsync"; - else if(isStream || conventions.IsPrimitiveType(returnType)) - if(isCollection) - return $"SendPrimitiveCollectionAsync<{returnType.StripArraySuffix()}>"; - else - return $"SendPrimitiveAsync<{returnType}>"; - else if (isCollection) return $"SendCollectionAsync<{returnType.StripArraySuffix()}>"; - else return $"SendAsync<{returnType}>"; + if(requestParams.queryString != null) { + writer.WriteLine($"if ({requestParams.queryString.Name} != null) {{"); + writer.IncreaseIndent(); + writer.WriteLines($"var qParams = new {operationName?.ToFirstCharacterUpperCase()}QueryParameters();", + $"{requestParams.queryString.Name}.Invoke(qParams);", + $"qParams.AddQueryParameters({RequestInfoVarName}.QueryParameters);"); + writer.DecreaseIndent(); + writer.WriteLine("}"); } - private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) { - var isDescriptionPresent = !string.IsNullOrEmpty(code.Description); - var parametersWithDescription = code.Parameters.Where(x => !string.IsNullOrEmpty(code.Description)); - if (isDescriptionPresent || parametersWithDescription.Any()) { - writer.WriteLine($"{conventions.DocCommentPrefix}"); - if(isDescriptionPresent) - writer.WriteLine($"{conventions.DocCommentPrefix}{code.Description}"); - foreach(var paramWithDescription in parametersWithDescription.OrderBy(x => x.Name)) - writer.WriteLine($"{conventions.DocCommentPrefix}{paramWithDescription.Description}"); - writer.WriteLine($"{conventions.DocCommentPrefix}"); - } + if(requestParams.headers != null) + writer.WriteLine($"{requestParams.headers.Name}?.Invoke({RequestInfoVarName}.Headers);"); + if(requestParams.options != null) + writer.WriteLine($"{RequestInfoVarName}.AddRequestOptions({requestParams.options.Name}?.ToArray());"); + writer.WriteLine($"return {RequestInfoVarName};"); + } + private static string GetPropertyCall(CodeProperty property, string defaultValue) => property == null ? defaultValue : $"{property.Name.ToFirstCharacterUpperCase()}"; + private void WriteSerializerBody(bool shouldHide, CodeMethod method, CodeClass parentClass, LanguageWriter writer) { + var additionalDataProperty = parentClass.GetPropertyOfKind(CodePropertyKind.AdditionalData); + if(shouldHide) + writer.WriteLine("base.Serialize(writer);"); + foreach(var otherProp in parentClass + .Properties + .Where(x => x.IsOfKind(CodePropertyKind.Custom)) + .OrderBy(x => x.Name)) { + writer.WriteLine($"writer.{GetSerializationMethodName(otherProp.Type, method)}(\"{otherProp.SerializationName ?? otherProp.Name.ToFirstCharacterLowerCase()}\", {otherProp.Name.ToFirstCharacterUpperCase()});"); } - private static readonly CodeParameterOrderComparer parameterOrderComparer = new(); - private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string returnType, bool inherits, bool isVoid) { - var staticModifier = code.IsStatic ? "static " : string.Empty; - var hideModifier = inherits && code.IsSerializationMethod ? "new " : string.Empty; - var genericTypePrefix = isVoid ? string.Empty : "<"; - var genericTypeSuffix = code.IsAsync && !isVoid ? ">": string.Empty; - var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); - var asyncPrefix = code.IsAsync ? "async Task" + genericTypePrefix : string.Empty; - var voidCorrectedTaskReturnType = code.IsAsync && isVoid ? string.Empty : returnType; - if(code.ReturnType.IsArray && code.IsOfKind(CodeMethodKind.RequestExecutor)) - voidCorrectedTaskReturnType = $"IEnumerable<{voidCorrectedTaskReturnType.StripArraySuffix()}>"; - // TODO: Task type should be moved into the refiner - var completeReturnType = isConstructor ? - string.Empty : - $"{asyncPrefix}{voidCorrectedTaskReturnType}{genericTypeSuffix} "; - var baseSuffix = string.Empty; - if(isConstructor && inherits) - baseSuffix = " : base()"; - var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p=> conventions.GetParameterSignature(p, code)).ToList()); - var methodName = isConstructor ? code.Parent.Name.ToFirstCharacterUpperCase() : code.Name.ToFirstCharacterUpperCase(); - writer.WriteLine($"{conventions.GetAccessModifier(code.Access)} {staticModifier}{hideModifier}{completeReturnType}{methodName}({parameters}){baseSuffix} {{"); + if(additionalDataProperty != null) + writer.WriteLine($"writer.WriteAdditionalData({additionalDataProperty.Name});"); + } + private string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCollection, string returnType) { + if(isVoid) return "SendNoContentAsync"; + else if(isStream || conventions.IsPrimitiveType(returnType)) + if(isCollection) + return $"SendPrimitiveCollectionAsync<{returnType.StripArraySuffix()}>"; + else + return $"SendPrimitiveAsync<{returnType}>"; + else if (isCollection) return $"SendCollectionAsync<{returnType.StripArraySuffix()}>"; + else return $"SendAsync<{returnType}>"; + } + private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer) { + var isDescriptionPresent = !string.IsNullOrEmpty(code.Description); + var parametersWithDescription = code.Parameters.Where(x => !string.IsNullOrEmpty(code.Description)); + if (isDescriptionPresent || parametersWithDescription.Any()) { + writer.WriteLine($"{conventions.DocCommentPrefix}"); + if(isDescriptionPresent) + writer.WriteLine($"{conventions.DocCommentPrefix}{code.Description}"); + foreach(var paramWithDescription in parametersWithDescription.OrderBy(x => x.Name)) + writer.WriteLine($"{conventions.DocCommentPrefix}{paramWithDescription.Description}"); + writer.WriteLine($"{conventions.DocCommentPrefix}"); } - private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method) { - var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; - var propertyType = conventions.GetTypeString(propType, method, false); - if(propType is CodeType currentType) { - if(isCollection) - if(currentType.TypeDefinition == null) - return $"WriteCollectionOfPrimitiveValues<{propertyType}>"; - else if(currentType.TypeDefinition is CodeEnum enumType) - return $"WriteCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>"; - else - return $"WriteCollectionOfObjectValues<{propertyType}>"; - else if (currentType.TypeDefinition is CodeEnum enumType) - return $"WriteEnumValue<{enumType.Name.ToFirstCharacterUpperCase()}>"; - - } - return propertyType switch - { - "byte[]" => "WriteByteArrayValue", - _ when conventions.IsPrimitiveType(propertyType) => $"Write{propertyType.TrimEnd(CSharpConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value", - _ => $"WriteObjectValue<{propertyType.ToFirstCharacterUpperCase()}>", - }; + } + private static readonly CodeParameterOrderComparer parameterOrderComparer = new(); + private void WriteMethodPrototype(CodeMethod code, LanguageWriter writer, string returnType, bool inherits, bool isVoid) { + var staticModifier = code.IsStatic ? "static " : string.Empty; + var hideModifier = inherits && code.IsSerializationMethod ? "new " : string.Empty; + var genericTypePrefix = isVoid ? string.Empty : "<"; + var genericTypeSuffix = code.IsAsync && !isVoid ? ">": string.Empty; + var isConstructor = code.IsOfKind(CodeMethodKind.Constructor, CodeMethodKind.ClientConstructor, CodeMethodKind.RawUrlConstructor); + var asyncPrefix = code.IsAsync ? "async Task" + genericTypePrefix : string.Empty; + var voidCorrectedTaskReturnType = code.IsAsync && isVoid ? string.Empty : returnType; + if(code.ReturnType.IsArray && code.IsOfKind(CodeMethodKind.RequestExecutor)) + voidCorrectedTaskReturnType = $"IEnumerable<{voidCorrectedTaskReturnType.StripArraySuffix()}>"; + // TODO: Task type should be moved into the refiner + var completeReturnType = isConstructor ? + string.Empty : + $"{asyncPrefix}{voidCorrectedTaskReturnType}{genericTypeSuffix} "; + var baseSuffix = string.Empty; + if(isConstructor && inherits) + baseSuffix = " : base()"; + var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p=> conventions.GetParameterSignature(p, code)).ToList()); + var methodName = isConstructor ? code.Parent.Name.ToFirstCharacterUpperCase() : code.Name.ToFirstCharacterUpperCase(); + writer.WriteLine($"{conventions.GetAccessModifier(code.Access)} {staticModifier}{hideModifier}{completeReturnType}{methodName}({parameters}){baseSuffix} {{"); + } + private string GetSerializationMethodName(CodeTypeBase propType, CodeMethod method) { + var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; + var propertyType = conventions.GetTypeString(propType, method, false); + if(propType is CodeType currentType) { + if(isCollection) + if(currentType.TypeDefinition == null) + return $"WriteCollectionOfPrimitiveValues<{propertyType}>"; + else if(currentType.TypeDefinition is CodeEnum enumType) + return $"WriteCollectionOfEnumValues<{enumType.Name.ToFirstCharacterUpperCase()}>"; + else + return $"WriteCollectionOfObjectValues<{propertyType}>"; + else if (currentType.TypeDefinition is CodeEnum enumType) + return $"WriteEnumValue<{enumType.Name.ToFirstCharacterUpperCase()}>"; + } + return propertyType switch + { + "byte[]" => "WriteByteArrayValue", + _ when conventions.IsPrimitiveType(propertyType) => $"Write{propertyType.TrimEnd(CSharpConventionService.NullableMarker).ToFirstCharacterUpperCase()}Value", + _ => $"WriteObjectValue<{propertyType.ToFirstCharacterUpperCase()}>", + }; } } diff --git a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs index a57e2c4f00..a9a9cb660d 100644 --- a/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/CSharp/CodeMethodWriterTests.cs @@ -5,455 +5,484 @@ using Kiota.Builder.Tests; using Xunit; -namespace Kiota.Builder.Writers.CSharp.Tests { - public class CodeMethodWriterTests : IDisposable { - private const string DefaultPath = "./"; - private const string DefaultName = "name"; - private readonly StringWriter tw; - private readonly LanguageWriter writer; - private readonly CodeMethod method; - private readonly CodeClass parentClass; - private const string MethodName = "methodName"; - private const string ReturnTypeName = "Somecustomtype"; - private const string MethodDescription = "some description"; - private const string ParamDescription = "some parameter description"; - private const string ParamName = "paramName"; - public CodeMethodWriterTests() +namespace Kiota.Builder.Writers.CSharp.Tests; +public class CodeMethodWriterTests : IDisposable { + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeMethod method; + private readonly CodeClass parentClass; + private readonly CodeNamespace root; + private const string MethodName = "methodName"; + private const string ReturnTypeName = "Somecustomtype"; + private const string MethodDescription = "some description"; + private const string ParamDescription = "some parameter description"; + private const string ParamName = "paramName"; + public CodeMethodWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.CSharp, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + parentClass = new CodeClass { + Name = "parentClass" + }; + root.AddClass(parentClass); + method = new CodeMethod { + Name = MethodName, + }; + method.ReturnType = new CodeType { + Name = ReturnTypeName + }; + parentClass.AddMethod(method); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + private void AddRequestProperties() { + parentClass.AddProperty(new CodeProperty { + Name = "RequestAdapter", + PropertyKind = CodePropertyKind.RequestAdapter, + }); + parentClass.AddProperty(new CodeProperty { + Name = "pathParameters", + PropertyKind = CodePropertyKind.PathParameters, + }); + parentClass.AddProperty(new CodeProperty { + Name = "urlTemplate", + PropertyKind = CodePropertyKind.UrlTemplate, + }); + } + private void AddSerializationProperties() { + var addData = parentClass.AddProperty(new CodeProperty { + Name = "additionalData", + PropertyKind = CodePropertyKind.AdditionalData, + }).First(); + addData.Type = new CodeType { + Name = "string" + }; + var dummyProp = parentClass.AddProperty(new CodeProperty { + Name = "dummyProp", + }).First(); + dummyProp.Type = new CodeType { + Name = "string" + }; + var dummyCollectionProp = parentClass.AddProperty(new CodeProperty { + Name = "dummyColl", + }).First(); + dummyCollectionProp.Type = new CodeType { + Name = "string", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }; + var dummyComplexCollection = parentClass.AddProperty(new CodeProperty { + Name = "dummyComplexColl" + }).First(); + dummyComplexCollection.Type = new CodeType { + Name = "Complex", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + TypeDefinition = new CodeClass { + Name = "SomeComplexType" + } + }; + var dummyEnumProp = parentClass.AddProperty(new CodeProperty{ + Name = "dummyEnumCollection", + }).First(); + dummyEnumProp.Type = new CodeType { + Name = "SomeEnum", + TypeDefinition = new CodeEnum { + Name = "EnumType" + } + }; + } + private void AddInheritanceClass() { + (parentClass.StartBlock as CodeClass.Declaration).Inherits = new CodeType { + Name = "someParentClass" + }; + } + private void AddRequestBodyParameters() { + var stringType = new CodeType { + Name = "string", + }; + method.AddParameter(new CodeParameter { + Name = "h", + ParameterKind = CodeParameterKind.Headers, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "q", + ParameterKind = CodeParameterKind.QueryParameter, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "b", + ParameterKind = CodeParameterKind.RequestBody, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "r", + ParameterKind = CodeParameterKind.ResponseHandler, + Type = stringType, + }); + method.AddParameter(new CodeParameter { + Name = "o", + ParameterKind = CodeParameterKind.Options, + Type = stringType, + }); + method.AddParameter(new CodeParameter { - writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.CSharp, DefaultPath, DefaultName); - tw = new StringWriter(); - writer.SetTextWriter(tw); - var root = CodeNamespace.InitRootNamespace(); - parentClass = new CodeClass { - Name = "parentClass" - }; - root.AddClass(parentClass); - method = new CodeMethod { - Name = MethodName, - }; - method.ReturnType = new CodeType { - Name = ReturnTypeName - }; - parentClass.AddMethod(method); - } - public void Dispose() - { - tw?.Dispose(); - GC.SuppressFinalize(this); - } - private void AddRequestProperties() { - parentClass.AddProperty(new CodeProperty { - Name = "RequestAdapter", - PropertyKind = CodePropertyKind.RequestAdapter, - }); - parentClass.AddProperty(new CodeProperty { - Name = "pathParameters", - PropertyKind = CodePropertyKind.PathParameters, - }); - parentClass.AddProperty(new CodeProperty { - Name = "urlTemplate", - PropertyKind = CodePropertyKind.UrlTemplate, - }); - } - private void AddSerializationProperties() { - var addData = parentClass.AddProperty(new CodeProperty { - Name = "additionalData", - PropertyKind = CodePropertyKind.AdditionalData, - }).First(); - addData.Type = new CodeType { - Name = "string" - }; - var dummyProp = parentClass.AddProperty(new CodeProperty { - Name = "dummyProp", - }).First(); - dummyProp.Type = new CodeType { - Name = "string" - }; - var dummyCollectionProp = parentClass.AddProperty(new CodeProperty { - Name = "dummyColl", - }).First(); - dummyCollectionProp.Type = new CodeType { - Name = "string", - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, - }; - var dummyComplexCollection = parentClass.AddProperty(new CodeProperty { - Name = "dummyComplexColl" - }).First(); - dummyComplexCollection.Type = new CodeType { - Name = "Complex", - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, - TypeDefinition = new CodeClass { - Name = "SomeComplexType" - } - }; - var dummyEnumProp = parentClass.AddProperty(new CodeProperty{ - Name = "dummyEnumCollection", - }).First(); - dummyEnumProp.Type = new CodeType { - Name = "SomeEnum", - TypeDefinition = new CodeEnum { - Name = "EnumType" - } - }; - } - private void AddInheritanceClass() { - (parentClass.StartBlock as CodeClass.Declaration).Inherits = new CodeType { - Name = "someParentClass" - }; - } - private void AddRequestBodyParameters() { - var stringType = new CodeType { - Name = "string", - }; - method.AddParameter(new CodeParameter { - Name = "h", - ParameterKind = CodeParameterKind.Headers, - Type = stringType, - }); - method.AddParameter(new CodeParameter{ - Name = "q", - ParameterKind = CodeParameterKind.QueryParameter, - Type = stringType, - }); - method.AddParameter(new CodeParameter{ - Name = "b", - ParameterKind = CodeParameterKind.RequestBody, - Type = stringType, - }); - method.AddParameter(new CodeParameter{ - Name = "r", - ParameterKind = CodeParameterKind.ResponseHandler, - Type = stringType, - }); - method.AddParameter(new CodeParameter { - Name = "o", - ParameterKind = CodeParameterKind.Options, - Type = stringType, - }); - method.AddParameter(new CodeParameter - { - Name = "c", - ParameterKind = CodeParameterKind.Cancellation, - Type = stringType, - }); - } - [Fact] - public void WritesRequestBuilder() { - method.MethodKind = CodeMethodKind.RequestBuilderBackwardCompatibility; - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void WritesRequestBodiesThrowOnNullHttpMethod() { - method.MethodKind = CodeMethodKind.RequestExecutor; - Assert.Throws(() => writer.Write(method)); - method.MethodKind = CodeMethodKind.RequestGenerator; - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void WritesRequestExecutorBody() { - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("var requestInfo", result); - Assert.Contains("SendAsync", result); - Assert.Contains(AsyncKeyword, result); - Assert.Contains("await", result); - Assert.Contains("cancellationToken", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestExecutorBodyForCollections() { - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - method.ReturnType.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("SendCollectionAsync", result); - Assert.Contains("cancellationToken", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestGeneratorBody() { - method.MethodKind = CodeMethodKind.RequestGenerator; - method.HttpMethod = HttpMethod.Get; - AddRequestProperties(); - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("var requestInfo = new RequestInformation", result); - Assert.Contains("HttpMethod = Method.GET", result); - Assert.Contains("UrlTemplate = ", result); - Assert.Contains("PathParameters = ", result); - Assert.Contains("h?.Invoke", result); - Assert.Contains("AddQueryParameters", result); - Assert.Contains("SetContentFromParsable", result); - Assert.Contains("AddRequestOptions", result); - Assert.Contains("return requestInfo;", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesInheritedDeSerializerBody() { - method.MethodKind = CodeMethodKind.Deserializer; - AddSerializationProperties(); - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("base.", result); - Assert.Contains("new", result); - } - [Fact] - public void WritesDeSerializerBody() { - method.MethodKind = CodeMethodKind.Deserializer; - AddSerializationProperties(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("GetStringValue", result); - Assert.Contains("GetCollectionOfPrimitiveValues", result); - Assert.Contains("GetCollectionOfObjectValues", result); - Assert.Contains("GetEnumValue", result); - } - [Fact] - public void WritesInheritedSerializerBody() { - method.MethodKind = CodeMethodKind.Serializer; - method.IsAsync = false; - AddSerializationProperties(); - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("base.Serialize(writer);", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesSerializerBody() { - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { - Name = "string" - }; - method.MethodKind = CodeMethodKind.Serializer; - method.IsAsync = false; - AddSerializationProperties(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("WriteStringValue", result); - Assert.Contains("WriteCollectionOfPrimitiveValues", result); - Assert.Contains("WriteCollectionOfObjectValues", result); - Assert.Contains("WriteEnumValue", result); - Assert.Contains("WriteAdditionalData(additionalData);", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesMethodAsyncDescription() { - - method.Description = MethodDescription; - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { - Name = "string" - }; - method.AddParameter(parameter); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("/// ", result); - Assert.Contains(MethodDescription, result); - Assert.Contains("", result); - Assert.Contains(ParamName, result); - Assert.Contains(ParamDescription, result); - Assert.Contains("", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesMethodSyncDescription() { - - method.Description = MethodDescription; - method.IsAsync = false; - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { + Name = "c", + ParameterKind = CodeParameterKind.Cancellation, + Type = stringType, + }); + } + [Fact] + public void WritesRequestBuilder() { + method.MethodKind = CodeMethodKind.RequestBuilderBackwardCompatibility; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestBodiesThrowOnNullHttpMethod() { + method.MethodKind = CodeMethodKind.RequestExecutor; + Assert.Throws(() => writer.Write(method)); + method.MethodKind = CodeMethodKind.RequestGenerator; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestExecutorBody() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass{ + Name = "Error4XX", + }).First(); + var error5XX = root.AddClass(new CodeClass{ + Name = "Error5XX", + }).First(); + var error401 = root.AddClass(new CodeClass{ + Name = "Error401", + }).First(); + method.ErrorMappings = new () { + {"4XX", new CodeType {Name = "Error4XX", TypeDefinition = error4XX}}, + {"5XX", new CodeType {Name = "Error5XX", TypeDefinition = error5XX}}, + {"403", new CodeType {Name = "Error403", TypeDefinition = error401}}, + }; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo", result); + Assert.Contains("var errorMapping = new Dictionary>", result); + Assert.Contains("{\"4XX\", () => new Error4XX()},", result); + Assert.Contains("{\"5XX\", () => new Error5XX()},", result); + Assert.Contains("{\"403\", () => new Error403()},", result); + Assert.Contains("SendAsync", result); + Assert.Contains(AsyncKeyword, result); + Assert.Contains("await", result); + Assert.Contains("cancellationToken", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesntCreateDictionaryOnEmptyErrorMapping() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("var errorMapping = new Dictionary>", result); + Assert.Contains("default", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestExecutorBodyForCollections() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + method.ReturnType.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("SendCollectionAsync", result); + Assert.Contains("cancellationToken", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorBody() { + method.MethodKind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("var requestInfo = new RequestInformation", result); + Assert.Contains("HttpMethod = Method.GET", result); + Assert.Contains("UrlTemplate = ", result); + Assert.Contains("PathParameters = ", result); + Assert.Contains("h?.Invoke", result); + Assert.Contains("AddQueryParameters", result); + Assert.Contains("SetContentFromParsable", result); + Assert.Contains("AddRequestOptions", result); + Assert.Contains("return requestInfo;", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedDeSerializerBody() { + method.MethodKind = CodeMethodKind.Deserializer; + AddSerializationProperties(); + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("base.", result); + Assert.Contains("new", result); + } + [Fact] + public void WritesDeSerializerBody() { + method.MethodKind = CodeMethodKind.Deserializer; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("GetStringValue", result); + Assert.Contains("GetCollectionOfPrimitiveValues", result); + Assert.Contains("GetCollectionOfObjectValues", result); + Assert.Contains("GetEnumValue", result); + } + [Fact] + public void WritesInheritedSerializerBody() { + method.MethodKind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("base.Serialize(writer);", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesSerializerBody() { + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.MethodKind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("WriteStringValue", result); + Assert.Contains("WriteCollectionOfPrimitiveValues", result); + Assert.Contains("WriteCollectionOfObjectValues", result); + Assert.Contains("WriteEnumValue", result); + Assert.Contains("WriteAdditionalData(additionalData);", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesMethodAsyncDescription() { + + method.Description = MethodDescription; + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.AddParameter(parameter); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("/// ", result); + Assert.Contains(MethodDescription, result); + Assert.Contains("", result); + Assert.Contains(ParamName, result); + Assert.Contains(ParamDescription, result); + Assert.Contains("", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesMethodSyncDescription() { + + method.Description = MethodDescription; + method.IsAsync = false; + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.AddParameter(parameter); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("@returns a Promise of", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void Defensive() { + var codeMethodWriter = new CodeMethodWriter(new CSharpConventionService()); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); + var originalParent = method.Parent; + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + method.Parent = originalParent; + method.ReturnType = null; + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + } + [Fact] + public void ThrowsIfParentIsNotClass() { + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void ThrowsIfReturnTypeIsMissing() { + method.ReturnType = null; + Assert.Throws(() => writer.Write(method)); + } + private const string TaskPrefix = "Task<"; + private const string AsyncKeyword = "async"; + [Fact] + public void WritesReturnType() { + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"{AsyncKeyword} {TaskPrefix}{ReturnTypeName}> {MethodName.ToFirstCharacterUpperCase()}", result); // async default + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesNotAddUndefinedOnNonNullableReturnType(){ + method.ReturnType.IsNullable = false; + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("?", result); + } + [Fact] + public void DoesNotAddAsyncInformationOnSyncMethods() { + method.IsAsync = false; + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain(TaskPrefix, result); + Assert.DoesNotContain(AsyncKeyword, result); + } + [Fact] + public void WritesPublicMethodByDefault() { + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("public ", result);// public default + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPrivateMethod() { + method.Access = AccessModifier.Private; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("private ", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesProtectedMethod() { + method.Access = AccessModifier.Protected; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("protected ", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPathParameterRequestBuilder() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; + method.AddParameter(new CodeParameter { + Name = "pathParam", + ParameterKind = CodeParameterKind.Path, + Type = new CodeType { Name = "string" - }; - method.AddParameter(parameter); - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain("@returns a Promise of", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void Defensive() { - var codeMethodWriter = new CodeMethodWriter(new CSharpConventionService()); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); - var originalParent = method.Parent; - method.Parent = CodeNamespace.InitRootNamespace(); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); - method.Parent = originalParent; - method.ReturnType = null; - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); - } - [Fact] - public void ThrowsIfParentIsNotClass() { - method.Parent = CodeNamespace.InitRootNamespace(); - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void ThrowsIfReturnTypeIsMissing() { - method.ReturnType = null; - Assert.Throws(() => writer.Write(method)); - } - private const string TaskPrefix = "Task<"; - private const string AsyncKeyword = "async"; - [Fact] - public void WritesReturnType() { - writer.Write(method); - var result = tw.ToString(); - Assert.Contains($"{AsyncKeyword} {TaskPrefix}{ReturnTypeName}> {MethodName.ToFirstCharacterUpperCase()}", result); // async default - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void DoesNotAddUndefinedOnNonNullableReturnType(){ - method.ReturnType.IsNullable = false; - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain("?", result); - } - [Fact] - public void DoesNotAddAsyncInformationOnSyncMethods() { - method.IsAsync = false; - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain(TaskPrefix, result); - Assert.DoesNotContain(AsyncKeyword, result); - } - [Fact] - public void WritesPublicMethodByDefault() { - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("public ", result);// public default - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesPrivateMethod() { - method.Access = AccessModifier.Private; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("private ", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesProtectedMethod() { - method.Access = AccessModifier.Protected; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("protected ", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesPathParameterRequestBuilder() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; - method.AddParameter(new CodeParameter { - Name = "pathParam", - ParameterKind = CodeParameterKind.Path, - Type = new CodeType { - Name = "string" - } - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("RequestAdapter", result); - Assert.Contains("PathParameters", result); - Assert.Contains("pathParam", result); - Assert.Contains("return new", result); - } - [Fact] - public void WritesConstructor() { - method.MethodKind = CodeMethodKind.Constructor; - var defaultValue = "someVal"; - var propName = "propWithDefaultValue"; - parentClass.AddProperty(new CodeProperty { - Name = propName, - DefaultValue = defaultValue, - PropertyKind = CodePropertyKind.UrlTemplate, - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains($"{propName.ToFirstCharacterUpperCase()} = {defaultValue}", result); - } - [Fact] - public void WritesApiConstructor() { - method.MethodKind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty { - Name = "core", - PropertyKind = CodePropertyKind.RequestAdapter, - }).First(); - coreProp.Type = new CodeType { - Name = "RequestAdapter", - IsExternal = true, - }; - method.AddParameter(new CodeParameter { - Name = "core", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, - }); - method.DeserializerModules = new() {"com.microsoft.kiota.serialization.Deserializer"}; - method.SerializerModules = new() {"com.microsoft.kiota.serialization.Serializer"}; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains("RegisterDefaultSerializer", result); - Assert.Contains("RegisterDefaultDeserializer", result); - } - [Fact] - public void WritesApiConstructorWithBackingStore() { - method.MethodKind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty { - Name = "core", - PropertyKind = CodePropertyKind.RequestAdapter, - }).First(); - coreProp.Type = new CodeType { - Name = "RequestAdapter", - IsExternal = true, - }; - method.AddParameter(new CodeParameter { - Name = "core", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, - }); - var backingStoreParam = new CodeParameter { - Name = "backingStore", - ParameterKind = CodeParameterKind.BackingStore, - }; - backingStoreParam.Type = new CodeType { - Name = "IBackingStore", - IsExternal = true, - }; - method.AddParameter(backingStoreParam); - var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.CSharp, DefaultPath, DefaultName); - tempWriter.SetTextWriter(tw); - tempWriter.Write(method); - var result = tw.ToString(); - Assert.Contains("EnableBackingStore", result); - } - [Fact] - public void ThrowsOnGetter() { - method.MethodKind = CodeMethodKind.Getter; - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void ThrowsOnSetter() { - method.MethodKind = CodeMethodKind.Setter; - Assert.Throws(() => writer.Write(method)); - } - } -} + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("RequestAdapter", result); + Assert.Contains("PathParameters", result); + Assert.Contains("pathParam", result); + Assert.Contains("return new", result); + } + [Fact] + public void WritesConstructor() { + method.MethodKind = CodeMethodKind.Constructor; + var defaultValue = "someVal"; + var propName = "propWithDefaultValue"; + parentClass.AddProperty(new CodeProperty { + Name = propName, + DefaultValue = defaultValue, + PropertyKind = CodePropertyKind.UrlTemplate, + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains($"{propName.ToFirstCharacterUpperCase()} = {defaultValue}", result); + } + [Fact] + public void WritesApiConstructor() { + method.MethodKind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty { + Name = "core", + PropertyKind = CodePropertyKind.RequestAdapter, + }).First(); + coreProp.Type = new CodeType { + Name = "RequestAdapter", + IsExternal = true, + }; + method.AddParameter(new CodeParameter { + Name = "core", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + method.DeserializerModules = new() {"com.microsoft.kiota.serialization.Deserializer"}; + method.SerializerModules = new() {"com.microsoft.kiota.serialization.Serializer"}; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains("RegisterDefaultSerializer", result); + Assert.Contains("RegisterDefaultDeserializer", result); + } + [Fact] + public void WritesApiConstructorWithBackingStore() { + method.MethodKind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty { + Name = "core", + PropertyKind = CodePropertyKind.RequestAdapter, + }).First(); + coreProp.Type = new CodeType { + Name = "RequestAdapter", + IsExternal = true, + }; + method.AddParameter(new CodeParameter { + Name = "core", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + var backingStoreParam = new CodeParameter { + Name = "backingStore", + ParameterKind = CodeParameterKind.BackingStore, + }; + backingStoreParam.Type = new CodeType { + Name = "IBackingStore", + IsExternal = true, + }; + method.AddParameter(backingStoreParam); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.CSharp, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(method); + var result = tw.ToString(); + Assert.Contains("EnableBackingStore", result); + } + [Fact] + public void ThrowsOnGetter() { + method.MethodKind = CodeMethodKind.Getter; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void ThrowsOnSetter() { + method.MethodKind = CodeMethodKind.Setter; + Assert.Throws(() => writer.Write(method)); + } +} From b20bc6626d03064b4c91f729648ef18755afdccb Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 2 Feb 2022 15:22:42 -0500 Subject: [PATCH 03/36] - adds error mapping as parameter to http request adapter dotnet - adds errors deserialization and throw in dotnet Signed-off-by: Vincent Biret --- abstractions/dotnet/src/IRequestAdapter.cs | 16 +++++--- .../dotnet/src/serialization/IParseNode.cs | 6 +++ .../src/HttpClientRequestAdapter.cs | 38 ++++++++++++++++--- .../dotnet/json/src/JsonParseNode.cs | 18 +++++++++ 4 files changed, 68 insertions(+), 10 deletions(-) diff --git a/abstractions/dotnet/src/IRequestAdapter.cs b/abstractions/dotnet/src/IRequestAdapter.cs index 61da556f49..b7980f6b10 100644 --- a/abstractions/dotnet/src/IRequestAdapter.cs +++ b/abstractions/dotnet/src/IRequestAdapter.cs @@ -2,6 +2,7 @@ // Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. // ------------------------------------------------------------------------------ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -29,41 +30,46 @@ public interface IRequestAdapter /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized response model. - Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable; + Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable; /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized response model collection. - Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable; + Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable; /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized primitive response model. - Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default); + Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default); /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// The deserialized primitive response model collection. - Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default); + Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default); /// /// Executes the HTTP request specified by the given RequestInformation with no return content. /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the requests. /// A Task to await completion. - Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default); + Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default); /// /// The base url for every request. /// diff --git a/abstractions/dotnet/src/serialization/IParseNode.cs b/abstractions/dotnet/src/serialization/IParseNode.cs index 314c49f8ae..e96a2db892 100644 --- a/abstractions/dotnet/src/serialization/IParseNode.cs +++ b/abstractions/dotnet/src/serialization/IParseNode.cs @@ -99,6 +99,12 @@ public interface IParseNode /// The model object value of the node. T GetObjectValue() where T : IParsable; /// + /// Gets the resulting error from the node. + /// + /// The error object value of the node. + /// The error factory. + IParsable GetErrorValue(Func factory); + /// /// Callback called before the node is deserialized. /// Action OnBeforeAssignFieldValues { get; set; } diff --git a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs index 95a82600cb..4d72652755 100644 --- a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs +++ b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs @@ -58,13 +58,15 @@ public ISerializationWriterFactory SerializationWriterFactory /// /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. - public async Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) where ModelType : IParsable + public async Task> SendCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); if(responseHandler == null) { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); var result = rootNode.GetCollectionOfObjectValues(); return result; @@ -77,13 +79,15 @@ public async Task> SendCollectionAsync(Request /// /// The RequestInformation object to use for the HTTP request. /// The response handler to use for the HTTP request instead of the default handler. + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// The deserialized primitive response model collection. - public async Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) { + public async Task> SendPrimitiveCollectionAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); if(responseHandler == null) { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); var result = rootNode.GetCollectionOfPrimitiveValues(); return result; @@ -96,14 +100,16 @@ public async Task> SendPrimitiveCollectionAsync /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// The deserialized response model. - public async Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, CancellationToken cancellationToken = default) where ModelType : IParsable + public async Task SendAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) where ModelType : IParsable { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); if(responseHandler == null) { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); var result = rootNode.GetObjectValue(); return result; @@ -116,9 +122,10 @@ public async Task SendAsync(RequestInformation requestInfo /// /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// The deserialized primitive response model. - public async Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, CancellationToken cancellationToken = default) + public async Task SendPrimitiveAsync(RequestInformation requestInfo, IResponseHandler responseHandler = default, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); @@ -131,6 +138,7 @@ public async Task SendPrimitiveAsync(RequestInformation re } else { + await ThrowFailedResponse(response, errorMapping); var rootNode = await GetRootParseNode(response); object result; if(modelType == typeof(bool)) @@ -173,9 +181,10 @@ public async Task SendPrimitiveAsync(RequestInformation re /// /// The instance to send /// The to use with the response + /// The error factories mapping to use in case of a failed request. /// The to use for cancelling the request. /// - public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, CancellationToken cancellationToken = default) + public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); requestInfo.Content?.Dispose(); @@ -184,6 +193,25 @@ public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHa else await responseHandler.HandleResponseAsync(response); } + private async Task ThrowFailedResponse(HttpResponseMessage response, Dictionary> errorMapping) + { + if(response.IsSuccessStatusCode) return; + + var statusCodeAsInt = (int)response.StatusCode; + var statusCodeAsString = statusCodeAsInt.ToString(); + Func errorFactory; + if(errorMapping == null || + !errorMapping.TryGetValue(statusCodeAsString, out errorFactory) && + !(statusCodeAsInt >= 400 && statusCodeAsInt < 500 && errorMapping.TryGetValue("4XX", out errorFactory)) && + !(statusCodeAsInt >= 500 && statusCodeAsInt < 600 && errorMapping.TryGetValue("5XX", out errorFactory))) + throw new HttpRequestException($"The server returned an unexpected status code and no error factory is registered for this code: {statusCodeAsString}"); + + var rootNode = await GetRootParseNode(response); + var result = rootNode.GetErrorValue(errorFactory); + if(result is not Exception ex) + throw new HttpRequestException($"The server returned an unexpected status code and the error registered for this code failed to deserialize: {statusCodeAsString}"); + else throw ex; + } private async Task GetRootParseNode(HttpResponseMessage response) { var responseContentType = response.Content.Headers?.ContentType?.MediaType?.ToLowerInvariant(); diff --git a/serialization/dotnet/json/src/JsonParseNode.cs b/serialization/dotnet/json/src/JsonParseNode.cs index ec44a7228c..378196da94 100644 --- a/serialization/dotnet/json/src/JsonParseNode.cs +++ b/serialization/dotnet/json/src/JsonParseNode.cs @@ -256,12 +256,30 @@ public IEnumerable GetCollectionOfPrimitiveValues() public T GetObjectValue() where T : IParsable { var item = (T)(typeof(T).GetConstructor(new Type[] { }).Invoke(new object[] { })); + return GetObjectValueInternal(item); + } + private T GetObjectValueInternal(T item) where T : IParsable + { var fieldDeserializers = item.GetFieldDeserializers(); OnBeforeAssignFieldValues?.Invoke(item); AssignFieldValues(item, fieldDeserializers); OnAfterAssignFieldValues?.Invoke(item); return item; } + /// + /// Gets the resulting error from the node. + /// + /// The error object value of the node. + /// The error factory. + public IParsable GetErrorValue(Func factory) + { + if (factory == null) throw new ArgumentNullException(nameof(factory)); + + var instance = factory.Invoke(); + if (instance == null) throw new InvalidOperationException("factory returned null"); + + return GetObjectValueInternal(instance); + } private void AssignFieldValues(T item, IDictionary> fieldDeserializers) where T : IParsable { if(_jsonNode.ValueKind != JsonValueKind.Object) return; From a04b2de9e0c118a522a4fa533f1976fb4e53df74 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 3 Feb 2022 13:13:51 -0500 Subject: [PATCH 04/36] - adds missing throw error in http request adapter Signed-off-by: Vincent Biret --- http/dotnet/httpclient/src/HttpClientRequestAdapter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs index 4d72652755..c51a022a94 100644 --- a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs +++ b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs @@ -187,6 +187,7 @@ public async Task SendPrimitiveAsync(RequestInformation re public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHandler responseHandler = null, Dictionary> errorMapping = default, CancellationToken cancellationToken = default) { var response = await GetHttpResponseMessage(requestInfo, cancellationToken); + await ThrowFailedResponse(response, errorMapping); requestInfo.Content?.Dispose(); if(responseHandler == null) response.Dispose(); From 51421db88fdd1fd2b82739e4eb44e822e3547bfa Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 3 Feb 2022 13:49:52 -0500 Subject: [PATCH 05/36] - adds a refiner to add the usings for exception and the inheritance Signed-off-by: Vincent Biret --- src/Kiota.Builder/Refiners/CSharpRefiner.cs | 5 + .../Refiners/CommonLanguageRefiner.cs | 18 + .../Refiners/CSharpLanguageRefinerTests.cs | 371 +++++++++--------- 3 files changed, 215 insertions(+), 179 deletions(-) diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index a23d7b5ffa..6e736a31a7 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -33,6 +33,11 @@ public override void Refine(CodeNamespace generatedCode) DisambiguatePropertiesWithClassNames(generatedCode); AddConstructorsForDefaultValues(generatedCode, false); AddSerializationModulesImport(generatedCode); + AddParentClassToErrorClasses( + generatedCode, + "Exception", + "System" + ); } private static void DisambiguatePropertiesWithClassNames(CodeElement currentElement) { if(currentElement is CodeClass currentClass) { diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index 744713d95e..bc0f77cd72 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -541,4 +541,22 @@ protected static void CorrectDateTypes(CodeClass parentClass, Dictionary AddParentClassToErrorClasses(x, parentClassName, parentClassNamespace)); + } } diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 4b6566d7aa..98c54aaef0 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -1,190 +1,203 @@ using System.Linq; using Xunit; -namespace Kiota.Builder.Refiners.Tests { - public class CSharpLanguageRefinerTests { - private readonly CodeNamespace root = CodeNamespace.InitRootNamespace(); - #region CommonLanguageRefinerTests - [Fact] - public void DoesNotEscapesReservedKeywordsForClassOrPropertyKind() { - // Arrange - var model = root.AddClass(new CodeClass { - Name = "break", // this a keyword - ClassKind = CodeClassKind.Model, - }).First(); - var property = model.AddProperty(new CodeProperty - { - Name = "alias",// this a keyword - Type = new CodeType - { - Name = "string" - } - }).First(); - // Act - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); - // Assert - Assert.Equal("break", model.Name); - Assert.DoesNotContain("@", model.Name); // classname will be capitalized - Assert.Equal("alias", property.Name); - Assert.DoesNotContain("@", property.Name); // classname will be capitalized - } - [Fact] - public void ConvertsUnionTypesToWrapper() { - var model = root.AddClass(new CodeClass { - Name = "model", - ClassKind = CodeClassKind.Model - }).First(); - var union = new CodeUnionType { - Name = "union", - }; - union.AddType(new () { - Name = "type1", - }, new() { - Name = "type2" - }); - var property = model.AddProperty(new CodeProperty { - Name = "deserialize", - PropertyKind = CodePropertyKind.Custom, - Type = union.Clone() as CodeTypeBase, - }).First(); - var method = model.AddMethod(new CodeMethod { - Name = "method", - ReturnType = union.Clone() as CodeTypeBase - }).First(); - var parameter = new CodeParameter { - Name = "param1", - Type = union.Clone() as CodeTypeBase - }; - var indexer = new CodeIndexer { - Name = "idx", - ReturnType = union.Clone() as CodeTypeBase, - }; - model.SetIndexer(indexer); - method.AddParameter(parameter); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); //using CSharp so the indexer doesn't get removed - Assert.True(property.Type is CodeType); - Assert.True(parameter.Type is CodeType); - Assert.True(method.ReturnType is CodeType); - Assert.True(indexer.ReturnType is CodeType); - } - [Fact] - public void MovesClassesWithNamespaceNamesUnderNamespace() { - var graphNS = root.AddNamespace("graph"); - var modelNS = graphNS.AddNamespace("graph.model"); - var model = graphNS.AddClass(new CodeClass { - Name = "model", - ClassKind = CodeClassKind.Model - }).First(); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); - Assert.Single(root.GetChildElements(true)); - Assert.Single(graphNS.GetChildElements(true)); - Assert.Single(modelNS.GetChildElements(true)); - Assert.Equal(modelNS, model.Parent); - } - [Fact] - public void KeepsCancellationParametersInRequestExecutors() +namespace Kiota.Builder.Refiners.Tests; +public class CSharpLanguageRefinerTests { + private readonly CodeNamespace root = CodeNamespace.InitRootNamespace(); + #region CommonLanguageRefinerTests + [Fact] + public void AddsExceptionInheritanceOnErrorClasses() { + var model = root.AddClass(new CodeClass { + Name = "break", // this a keyword + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + + var declaration = model.StartBlock as CodeClass.Declaration; + + Assert.Contains("Exception", declaration.Usings.Select(x => x.Name)); + Assert.Equal("Exception", declaration.Inherits.Name); + } + [Fact] + public void DoesNotEscapesReservedKeywordsForClassOrPropertyKind() { + // Arrange + var model = root.AddClass(new CodeClass { + Name = "break", // this a keyword + ClassKind = CodeClassKind.Model, + }).First(); + var property = model.AddProperty(new CodeProperty { - var model = root.AddClass(new CodeClass - { - Name = "model", - ClassKind = CodeClassKind.RequestBuilder - }).First(); - var method = model.AddMethod(new CodeMethod - { - Name = "getMethod", - MethodKind = CodeMethodKind.RequestExecutor, - ReturnType = new CodeType { - Name = "string" - } - }).First(); - var cancellationParam = new CodeParameter + Name = "alias",// this a keyword + Type = new CodeType { - Name = "cancelletionToken", - Optional = true, - ParameterKind = CodeParameterKind.Cancellation, - Description = "Cancellation token to use when cancelling requests", - Type = new CodeType { Name = "CancelletionToken", IsExternal = true }, - }; - method.AddParameter(cancellationParam); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); //using CSharp so the cancelletionToken doesn't get removed - Assert.True(method.Parameters.Any()); - Assert.Contains(cancellationParam, method.Parameters); - } - #endregion - #region CSharp - [Fact] - public void DisambiguatePropertiesWithClassNames() { - var model = root.AddClass(new CodeClass { - Name = "Model", - ClassKind = CodeClassKind.Model - }).First(); - var propToAdd = model.AddProperty(new CodeProperty{ - Name = "model", - Type = new CodeType { - Name = "string" - } - }).First(); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); - Assert.Equal("model_prop", propToAdd.Name); - Assert.Equal("model", propToAdd.SerializationName); - } - [Fact] - public void DisambiguatePropertiesWithClassNames_DoesntReplaceSerializationName() { - var serializationName = "serializationName"; - var model = root.AddClass(new CodeClass { - Name = "Model", - ClassKind = CodeClassKind.Model - }).First(); - var propToAdd = model.AddProperty(new CodeProperty{ - Name = "model", - Type = new CodeType { - Name = "string" - }, - SerializationName = serializationName, - }).First(); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); - Assert.Equal(serializationName, propToAdd.SerializationName); - } - [Fact] - public void ReplacesDateOnlyByNativeType() + Name = "string" + } + }).First(); + // Act + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + // Assert + Assert.Equal("break", model.Name); + Assert.DoesNotContain("@", model.Name); // classname will be capitalized + Assert.Equal("alias", property.Name); + Assert.DoesNotContain("@", property.Name); // classname will be capitalized + } + [Fact] + public void ConvertsUnionTypesToWrapper() { + var model = root.AddClass(new CodeClass { + Name = "model", + ClassKind = CodeClassKind.Model + }).First(); + var union = new CodeUnionType { + Name = "union", + }; + union.AddType(new () { + Name = "type1", + }, new() { + Name = "type2" + }); + var property = model.AddProperty(new CodeProperty { + Name = "deserialize", + PropertyKind = CodePropertyKind.Custom, + Type = union.Clone() as CodeTypeBase, + }).First(); + var method = model.AddMethod(new CodeMethod { + Name = "method", + ReturnType = union.Clone() as CodeTypeBase + }).First(); + var parameter = new CodeParameter { + Name = "param1", + Type = union.Clone() as CodeTypeBase + }; + var indexer = new CodeIndexer { + Name = "idx", + ReturnType = union.Clone() as CodeTypeBase, + }; + model.SetIndexer(indexer); + method.AddParameter(parameter); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); //using CSharp so the indexer doesn't get removed + Assert.True(property.Type is CodeType); + Assert.True(parameter.Type is CodeType); + Assert.True(method.ReturnType is CodeType); + Assert.True(indexer.ReturnType is CodeType); + } + [Fact] + public void MovesClassesWithNamespaceNamesUnderNamespace() { + var graphNS = root.AddNamespace("graph"); + var modelNS = graphNS.AddNamespace("graph.model"); + var model = graphNS.AddClass(new CodeClass { + Name = "model", + ClassKind = CodeClassKind.Model + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + Assert.Single(root.GetChildElements(true)); + Assert.Single(graphNS.GetChildElements(true)); + Assert.Single(modelNS.GetChildElements(true)); + Assert.Equal(modelNS, model.Parent); + } + [Fact] + public void KeepsCancellationParametersInRequestExecutors() + { + var model = root.AddClass(new CodeClass { - var model = root.AddClass(new CodeClass - { - Name = "model", - ClassKind = CodeClassKind.Model - }).First(); - var method = model.AddMethod(new CodeMethod - { - Name = "method", - ReturnType = new CodeType - { - Name = "DateOnly" - }, - }).First(); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); - Assert.NotEmpty(model.StartBlock.Usings); - Assert.Equal("Date", method.ReturnType.Name); - } - [Fact] - public void ReplacesTimeOnlyByNativeType() + Name = "model", + ClassKind = CodeClassKind.RequestBuilder + }).First(); + var method = model.AddMethod(new CodeMethod { - var model = root.AddClass(new CodeClass + Name = "getMethod", + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string" + } + }).First(); + var cancellationParam = new CodeParameter + { + Name = "cancelletionToken", + Optional = true, + ParameterKind = CodeParameterKind.Cancellation, + Description = "Cancellation token to use when cancelling requests", + Type = new CodeType { Name = "CancelletionToken", IsExternal = true }, + }; + method.AddParameter(cancellationParam); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); //using CSharp so the cancelletionToken doesn't get removed + Assert.True(method.Parameters.Any()); + Assert.Contains(cancellationParam, method.Parameters); + } + #endregion + #region CSharp + [Fact] + public void DisambiguatePropertiesWithClassNames() { + var model = root.AddClass(new CodeClass { + Name = "Model", + ClassKind = CodeClassKind.Model + }).First(); + var propToAdd = model.AddProperty(new CodeProperty{ + Name = "model", + Type = new CodeType { + Name = "string" + } + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + Assert.Equal("model_prop", propToAdd.Name); + Assert.Equal("model", propToAdd.SerializationName); + } + [Fact] + public void DisambiguatePropertiesWithClassNames_DoesntReplaceSerializationName() { + var serializationName = "serializationName"; + var model = root.AddClass(new CodeClass { + Name = "Model", + ClassKind = CodeClassKind.Model + }).First(); + var propToAdd = model.AddProperty(new CodeProperty{ + Name = "model", + Type = new CodeType { + Name = "string" + }, + SerializationName = serializationName, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + Assert.Equal(serializationName, propToAdd.SerializationName); + } + [Fact] + public void ReplacesDateOnlyByNativeType() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + ClassKind = CodeClassKind.Model + }).First(); + var method = model.AddMethod(new CodeMethod + { + Name = "method", + ReturnType = new CodeType { - Name = "model", - ClassKind = CodeClassKind.Model - }).First(); - var method = model.AddMethod(new CodeMethod + Name = "DateOnly" + }, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + Assert.NotEmpty(model.StartBlock.Usings); + Assert.Equal("Date", method.ReturnType.Name); + } + [Fact] + public void ReplacesTimeOnlyByNativeType() + { + var model = root.AddClass(new CodeClass + { + Name = "model", + ClassKind = CodeClassKind.Model + }).First(); + var method = model.AddMethod(new CodeMethod + { + Name = "method", + ReturnType = new CodeType { - Name = "method", - ReturnType = new CodeType - { - Name = "TimeOnly" - }, - }).First(); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); - Assert.NotEmpty(model.StartBlock.Usings); - Assert.Equal("Time", method.ReturnType.Name); - } - #endregion + Name = "TimeOnly" + }, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + Assert.NotEmpty(model.StartBlock.Usings); + Assert.Equal("Time", method.ReturnType.Name); } + #endregion } From 34d330b4e593bb9a0635df9bf951e8005160f2aa Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 3 Feb 2022 14:19:04 -0500 Subject: [PATCH 06/36] - adds error types as usings for error mappings Signed-off-by: Vincent Biret --- .../Refiners/CommonLanguageRefiner.cs | 7 +++ .../Refiners/CSharpLanguageRefinerTests.cs | 50 ++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index bc0f77cd72..0427cd004d 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -440,11 +440,17 @@ protected static void AddPropertiesAndMethodTypesImports(CodeElement current, bo .OfType() .Select(x => x.ReturnType) .Distinct(); + var errorTypes = currentClassChildren + .OfType() + .Where(x => x.IsOfKind(CodeMethodKind.RequestExecutor)) + .SelectMany(x => x.ErrorMappings.Values) + .Distinct(); var usingsToAdd = propertiesTypes .Union(methodsParametersTypes) .Union(methodsReturnTypes) .Union(indexerTypes) .Union(inheritTypes) + .Union(errorTypes) .Where(x => x != null) .SelectMany(x => x?.AllTypes?.Select(y => new Tuple(y, y?.TypeDefinition?.GetImmediateParentOfType()))) .Where(x => x.Item2 != null && (includeCurrentNamespace || x.Item2 != currentClassNamespace)) @@ -554,6 +560,7 @@ protected static void AddParentClassToErrorClasses(CodeElement currentElement, s Name = parentClassName, Declaration = new CodeType { Name = parentClassNamespace, + IsExternal = true, } }); } diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 98c54aaef0..743afcc5c0 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Xunit; namespace Kiota.Builder.Refiners.Tests; @@ -8,7 +9,7 @@ public class CSharpLanguageRefinerTests { [Fact] public void AddsExceptionInheritanceOnErrorClasses() { var model = root.AddClass(new CodeClass { - Name = "break", // this a keyword + Name = "somemodel", ClassKind = CodeClassKind.Model, IsErrorDefinition = true, }).First(); @@ -20,6 +21,51 @@ public void AddsExceptionInheritanceOnErrorClasses() { Assert.Equal("Exception", declaration.Inherits.Name); } [Fact] + public void FailsExceptionInheritanceOnErrorClassesWhichAlreadyInherit() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var declaration = model.StartBlock as CodeClass.Declaration; + declaration.Inherits = new CodeType { + Name = "SomeOtherModel" + }; + Assert.Throws(() => ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root)); + } + [Fact] + public void AddsUsingsForErrorTypesForRequestExecutor() { + var requestBuilder = root.AddClass(new CodeClass { + Name = "somerequestbuilder", + ClassKind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = root.AddNamespace($"{root.Name}.subns"); // otherwise the import gets trimmed + var errorClass = subNS.AddClass(new CodeClass { + Name = "Error4XX", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var requestExecutor = requestBuilder.AddMethod(new CodeMethod { + Name = "get", + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string" + }, + ErrorMappings = new () { + { "4XX", new CodeType { + Name = "Error4XX", + TypeDefinition = errorClass, + } + }, + }, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.CSharp }, root); + + var declaration = requestBuilder.StartBlock as CodeClass.Declaration; + + Assert.Contains("Error4XX", declaration.Usings.Select(x => x.Declaration?.Name)); + } + [Fact] public void DoesNotEscapesReservedKeywordsForClassOrPropertyKind() { // Arrange var model = root.AddClass(new CodeClass { From 42b92b82257ba0538106758a2922969f102b6178 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 3 Feb 2022 14:31:06 -0500 Subject: [PATCH 07/36] - updates native response handler to pass the error mapping --- abstractions/dotnet/src/IResponseHandler.cs | 6 +++++- abstractions/dotnet/src/NativeResponseHandler.cs | 13 +++++++++---- .../httpclient/src/HttpClientRequestAdapter.cs | 10 +++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/abstractions/dotnet/src/IResponseHandler.cs b/abstractions/dotnet/src/IResponseHandler.cs index c2012208b2..e46c3d8c0c 100644 --- a/abstractions/dotnet/src/IResponseHandler.cs +++ b/abstractions/dotnet/src/IResponseHandler.cs @@ -3,6 +3,9 @@ // ------------------------------------------------------------------------------ using System.Threading.Tasks; +using System.Collections.Generic; +using System; +using Microsoft.Kiota.Abstractions.Serialization; namespace Microsoft.Kiota.Abstractions { @@ -15,9 +18,10 @@ public interface IResponseHandler /// Callback method that is invoked when a response is received. /// /// The native response object. + /// The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. /// The type of the native response object. /// The type of the response model object. /// A task that represents the asynchronous operation and contains the deserialized response. - Task HandleResponseAsync(NativeResponseType response); + Task HandleResponseAsync(NativeResponseType response, Dictionary> errorMappings); } } diff --git a/abstractions/dotnet/src/NativeResponseHandler.cs b/abstractions/dotnet/src/NativeResponseHandler.cs index e15cf42191..0cead8e434 100644 --- a/abstractions/dotnet/src/NativeResponseHandler.cs +++ b/abstractions/dotnet/src/NativeResponseHandler.cs @@ -3,6 +3,9 @@ // ------------------------------------------------------------------------------ using System.Threading.Tasks; +using System.Collections.Generic; +using System; +using Microsoft.Kiota.Abstractions.Serialization; namespace Microsoft.Kiota.Abstractions { @@ -17,13 +20,15 @@ public class NativeResponseHandler : IResponseHandler public object Value; /// - /// Handles the response of type and return an instance of + /// The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. /// - /// The response to be handled - /// - public Task HandleResponseAsync(NativeResponseType response) + public Dictionary> ErrorsMappings { get; set; } + + /// + public Task HandleResponseAsync(NativeResponseType response, Dictionary> errorMappings) { Value = response; + ErrorsMappings = errorMappings; return Task.FromResult(default(ModelType)); } } diff --git a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs index c51a022a94..f0d49fab1d 100644 --- a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs +++ b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs @@ -72,7 +72,7 @@ public async Task> SendCollectionAsync(Request return result; } else - return await responseHandler.HandleResponseAsync>(response); + return await responseHandler.HandleResponseAsync>(response, errorMapping); } /// /// Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. @@ -93,7 +93,7 @@ public async Task> SendPrimitiveCollectionAsync>(response); + return await responseHandler.HandleResponseAsync>(response, errorMapping); } /// /// Send a instance with an instance of @@ -115,7 +115,7 @@ public async Task SendAsync(RequestInformation requestInfo return result; } else - return await responseHandler.HandleResponseAsync(response); + return await responseHandler.HandleResponseAsync(response, errorMapping); } /// /// Send a instance with a primitive instance of @@ -174,7 +174,7 @@ public async Task SendPrimitiveAsync(RequestInformation re } } else - return await responseHandler.HandleResponseAsync(response); + return await responseHandler.HandleResponseAsync(response, errorMapping); } /// /// Send a instance with an empty request body @@ -192,7 +192,7 @@ public async Task SendNoContentAsync(RequestInformation requestInfo, IResponseHa if(responseHandler == null) response.Dispose(); else - await responseHandler.HandleResponseAsync(response); + await responseHandler.HandleResponseAsync(response, errorMapping); } private async Task ThrowFailedResponse(HttpResponseMessage response, Dictionary> errorMapping) { From 70ae1f04a07e5173c32ce40124db87312e93adec Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 3 Feb 2022 15:05:18 -0500 Subject: [PATCH 08/36] - fixes an issue where CSharp error inheritance would try to hide inexisting IParsable members Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs index 2a8e050102..668c0862dd 100644 --- a/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/CSharp/CodeMethodWriter.cs @@ -16,7 +16,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); var parentClass = codeElement.Parent as CodeClass; - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + var inherits = parentClass.StartBlock is CodeClass.Declaration declaration && declaration.Inherits != null && !parentClass.IsErrorDefinition; var isVoid = conventions.VoidTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); WriteMethodDocumentation(codeElement, writer); WriteMethodPrototype(codeElement, writer, returnType, inherits, isVoid); @@ -44,7 +44,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri WriteRequestExecutorBody(codeElement, requestParams, isVoid, returnType, writer); break; case CodeMethodKind.Deserializer: - WriteDeserializerBody(codeElement, parentClass, writer); + WriteDeserializerBody(inherits, codeElement, parentClass, writer); break; case CodeMethodKind.ClientConstructor: WriteConstructorBody(parentClass, codeElement, writer); @@ -131,9 +131,8 @@ private static void AssignPropertyFromParameter(CodeClass parentClass, CodeMetho writer.WriteLine($"{property.Name.ToFirstCharacterUpperCase()} = {parameter.Name};"); } } - private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { - var hideParentMember = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; - var parentSerializationInfo = hideParentMember ? $"(base.{codeElement.Name.ToFirstCharacterUpperCase()}())" : string.Empty; + private void WriteDeserializerBody(bool shouldHide, CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { + var parentSerializationInfo = shouldHide ? $"(base.{codeElement.Name.ToFirstCharacterUpperCase()}())" : string.Empty; writer.WriteLine($"return new Dictionary>{parentSerializationInfo} {{"); writer.IncreaseIndent(); foreach(var otherProp in parentClass From c1fc3634b4dd806daf9bdc38f021e414f23401fc Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 05:07:44 -0800 Subject: [PATCH 09/36] Apply suggestions from code review Co-authored-by: Eastman --- abstractions/dotnet/src/NativeResponseHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/abstractions/dotnet/src/NativeResponseHandler.cs b/abstractions/dotnet/src/NativeResponseHandler.cs index 0cead8e434..111e4c2196 100644 --- a/abstractions/dotnet/src/NativeResponseHandler.cs +++ b/abstractions/dotnet/src/NativeResponseHandler.cs @@ -22,13 +22,13 @@ public class NativeResponseHandler : IResponseHandler /// /// The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. /// - public Dictionary> ErrorsMappings { get; set; } + public Dictionary> ErrorMappings { get; set; } /// public Task HandleResponseAsync(NativeResponseType response, Dictionary> errorMappings) { Value = response; - ErrorsMappings = errorMappings; + ErrorMappings = errorMappings; return Task.FromResult(default(ModelType)); } } From b40d59c18f989cf0ada7121288c91627c6d8960b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 10:35:41 -0500 Subject: [PATCH 10/36] - adds error mapping parameter for java request adapter interface --- .../kiota/NativeResponseHandler.java | 12 ++++++++++- .../com/microsoft/kiota/RequestAdapter.java | 21 ++++++++++++------- .../com/microsoft/kiota/ResponseHandler.java | 7 ++++++- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java index 366f19b2c1..82ff8a3677 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java @@ -1,21 +1,31 @@ package com.microsoft.kiota; +import java.util.HashMap; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import com.microsoft.kiota.serialization.Parsable; + /** Default response handler to access the native response object. */ public class NativeResponseHandler implements ResponseHandler { /** Native response object as returned by the core service */ @Nullable public Object value; + /** The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. */ + @Nullable + public HashMap> errorMappings; + + /** {@inheritdoc} */ @Nonnull @Override public CompletableFuture handleResponseAsync( - NativeResponseType response) { + NativeResponseType response, + HashMap> errorMappings) { this.value = response; + this.errorMappings = errorMappings; return CompletableFuture.completedFuture(null); } diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java index 62121bb6d2..44d062f2c6 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java @@ -1,6 +1,7 @@ package com.microsoft.kiota; import java.util.concurrent.CompletableFuture; +import java.util.HashMap; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -23,41 +24,45 @@ public interface RequestAdapter { @Nonnull SerializationWriterFactory getSerializationWriterFactory(); /** - * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. + * Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. * @param targetClass the class of the response model to deserialize the response into. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized response model. */ - CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler); + CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** - * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. + * Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. * @param targetClass the class of the response model to deserialize the response into. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized response model collection. */ - CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler); + CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** - * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. + * Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. * @param targetClass the class of the response model to deserialize the response into. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized primitive response model. */ - CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler); + CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** - * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive collection response model. + * Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive collection response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. * @param targetClass the class of the response model to deserialize the response into. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized primitive collection response model. */ - CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler); + CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** * Sets The base url for every request. * @param baseUrl The base url for every request. diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java index 9505518093..d24c28f18f 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java @@ -1,18 +1,23 @@ package com.microsoft.kiota; +import java.util.HashMap; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.microsoft.kiota.serialization.Parsable; /** Defines the contract for a response handler. */ public interface ResponseHandler { /** * Callback method that is invoked when a response is received. * @param response The native response object. + * @param errorMappings the error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. * @param The type of the native response object. * @param The type of the response model object. * @return A CompletableFuture that represents the asynchronous operation and contains the deserialized response. */ @Nonnull - CompletableFuture handleResponseAsync(@Nonnull final NativeResponseType response); + CompletableFuture handleResponseAsync(@Nonnull final NativeResponseType response, @Nullable final HashMap> errorMappings); } \ No newline at end of file From 0cac5dedbb06c5db4ad6fde55ec74b0720c4b6a0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 10:49:28 -0500 Subject: [PATCH 11/36] - implements errors deserialization for java --- .../kiota/http/OkHttpRequestAdapter.java | 104 ++++++++++++------ 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index 0444635ba1..a5f163942e 100644 --- a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -5,6 +5,7 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.time.OffsetDateTime; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.StringJoiner; @@ -91,48 +92,46 @@ public void enableBackingStore(@Nullable final BackingStoreFactory backingStoreF } } @Nonnull - public CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler) { + public CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); - return this.getHttpResponseMessage(requestInfo).thenCompose(response -> { + return this.getHttpResponseMessage(requestInfo) + .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) + .thenCompose(response -> { if(responseHandler == null) { - final ResponseBody body = response.body(); try { - try (final InputStream rawInputStream = body.byteStream()) { - final ParseNode rootNode = pNodeFactory.getParseNode(getMediaTypeAndSubType(body.contentType()), rawInputStream); - final Iterable result = rootNode.getCollectionOfObjectValues(targetClass); - return CompletableFuture.completedStage(result); - } + final ParseNode rootNode = getRootParseNode(response); + final Iterable result = rootNode.getCollectionOfObjectValues(targetClass); + return CompletableFuture.completedStage(result); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { response.close(); } } else { - return responseHandler.handleResponseAsync(response); + return responseHandler.handleResponseAsync(response, errorMappings); } }); } @Nonnull - public CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler) { + public CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); - return this.getHttpResponseMessage(requestInfo).thenCompose(response -> { + return this.getHttpResponseMessage(requestInfo) + .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) + .thenCompose(response -> { if(responseHandler == null) { - final ResponseBody body = response.body(); try { - try (final InputStream rawInputStream = body.byteStream()) { - final ParseNode rootNode = pNodeFactory.getParseNode(getMediaTypeAndSubType(body.contentType()), rawInputStream); - final ModelType result = rootNode.getObjectValue(targetClass); - return CompletableFuture.completedStage(result); - } + final ParseNode rootNode = getRootParseNode(response); + final ModelType result = rootNode.getObjectValue(targetClass); + return CompletableFuture.completedStage(result); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { response.close(); } } else { - return responseHandler.handleResponseAsync(response); + return responseHandler.handleResponseAsync(response, errorMappings); } }); } @@ -140,20 +139,21 @@ private String getMediaTypeAndSubType(final MediaType mediaType) { return mediaType.type() + "/" + mediaType.subtype(); } @Nonnull - public CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler) { - return this.getHttpResponseMessage(requestInfo).thenCompose(response -> { + public CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { + return this.getHttpResponseMessage(requestInfo) + .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) + .thenCompose(response -> { if(responseHandler == null) { - final ResponseBody body = response.body(); try { if(targetClass == Void.class) { return CompletableFuture.completedStage(null); } else { - final InputStream rawInputStream = body.byteStream(); if(targetClass == InputStream.class) { + final ResponseBody body = response.body(); + final InputStream rawInputStream = body.byteStream(); return CompletableFuture.completedStage((ModelType)rawInputStream); } - final ParseNode rootNode = pNodeFactory.getParseNode(getMediaTypeAndSubType(body.contentType()), rawInputStream); - rawInputStream.close(); + final ParseNode rootNode = getRootParseNode(response); Object result; if(targetClass == Boolean.class) { result = rootNode.getBooleanValue(); @@ -180,32 +180,68 @@ public CompletableFuture sendPrimitiveAsync(@Nonnull fina response.close(); } } else { - return responseHandler.handleResponseAsync(response); + return responseHandler.handleResponseAsync(response, errorMappings); } }); } - public CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler) { + public CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); - return this.getHttpResponseMessage(requestInfo).thenCompose(response -> { + return this.getHttpResponseMessage(requestInfo) + .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) + .thenCompose(response -> { if(responseHandler == null) { - final ResponseBody body = response.body(); try { - try (final InputStream rawInputStream = body.byteStream()) { - final ParseNode rootNode = pNodeFactory.getParseNode(getMediaTypeAndSubType(body.contentType()), rawInputStream); - final Iterable result = rootNode.getCollectionOfPrimitiveValues(targetClass); - return CompletableFuture.completedStage(result); - } + final ParseNode rootNode = getRootParseNode(response); + final Iterable result = rootNode.getCollectionOfPrimitiveValues(targetClass); + return CompletableFuture.completedStage(result); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { response.close(); } } else { - return responseHandler.handleResponseAsync(response); + return responseHandler.handleResponseAsync(response, errorMappings); } }); } + private ParseNode getRootParseNode(final Response response) throws IOException { + final ResponseBody body = response.body(); + try (final InputStream rawInputStream = body.byteStream()) { + final ParseNode rootNode = pNodeFactory.getParseNode(getMediaTypeAndSubType(body.contentType()), rawInputStream); + return rootNode; + } + } + private CompletableFuture throwFailedResponse(final Response response, final HashMap> errorMappings) { + if (response.isSuccessful()) return CompletableFuture.completedFuture(response); + + final String statusCodeAsString = Integer.toString(response.code()); + final Integer statusCode = response.code(); + if (errorMappings == null || + !errorMappings.containsKey(statusCodeAsString) && + !(statusCode >= 400 && statusCode < 500 && errorMappings.containsKey("4XX")) && + !(statusCode >= 500 && statusCode < 600 && errorMappings.containsKey("5XX"))) { + return CompletableFuture.failedFuture(new RuntimeException("the server returned an unexpected status code and no error class is registered for this code " + statusCode)); + } + final Class errorClass = errorMappings.containsKey(statusCodeAsString) ? + errorMappings.get(statusCodeAsString) : + (statusCode >= 400 && statusCode < 500 ? + errorMappings.get("4XX") : + errorMappings.get("5XX")); + try { + final ParseNode rootNode = getRootParseNode(response); + final Parsable error = rootNode.getObjectValue(errorClass); + if (error instanceof Exception) { + return CompletableFuture.failedFuture((Exception)error); + } else { + return CompletableFuture.failedFuture(new RuntimeException("unexpected error type " + error.getClass().getName())); + } + } catch (IOException ex) { + return CompletableFuture.failedFuture(ex); + } finally { + response.close(); + } + } private CompletableFuture getHttpResponseMessage(@Nonnull final RequestInformation requestInfo) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); this.setBaseUrlForRequestInformation(requestInfo); From fab35cb58067896ad971b29663fe14d79584c631 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 10:50:15 -0500 Subject: [PATCH 12/36] - udpates class path Signed-off-by: Vincent Biret --- serialization/java/json/lib/.classpath | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/serialization/java/json/lib/.classpath b/serialization/java/json/lib/.classpath index 5933c3f21a..ae30f860ac 100644 --- a/serialization/java/json/lib/.classpath +++ b/serialization/java/json/lib/.classpath @@ -13,7 +13,7 @@ - + From a49bea76bfa53457dc00a85600cdb509a46ba964 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 10:50:40 -0500 Subject: [PATCH 13/36] - updates project settings Signed-off-by: Vincent Biret --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 601a42d3c4..aa309383b2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,5 +18,6 @@ "cSpell.words": [ "npmrc", "serializers" - ] + ], + "java.configuration.updateBuildConfiguration": "automatic" } From 21d62127083bc0610d099d85a880210adfec5975 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 10:54:58 -0500 Subject: [PATCH 14/36] - adds exception inheritance in java refiner Signed-off-by: Vincent Biret --- src/Kiota.Builder/Refiners/JavaRefiner.cs | 5 ++ .../Refiners/JavaLanguageRefinerTests.cs | 62 ++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs index a4c3cf0e65..86b24188d0 100644 --- a/src/Kiota.Builder/Refiners/JavaRefiner.cs +++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs @@ -37,6 +37,11 @@ public override void Refine(CodeNamespace generatedCode) new [] { "com.microsoft.kiota.ApiClientBuilder", "com.microsoft.kiota.serialization.SerializationWriterFactoryRegistry" }, new [] { "com.microsoft.kiota.serialization.ParseNodeFactoryRegistry" }); + AddParentClassToErrorClasses( + generatedCode, + "Exception", + "java.lang" + ); } private static void SetSetterParametersToNullable(CodeElement currentElement, params Tuple[] accessorPairs) { if(currentElement is CodeMethod method && diff --git a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs index b1d5aed24a..9649894691 100644 --- a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Xunit; namespace Kiota.Builder.Refiners.Tests; @@ -6,6 +7,65 @@ public class JavaLanguageRefinerTests { private readonly CodeNamespace root = CodeNamespace.InitRootNamespace(); #region CommonLanguageRefinerTests [Fact] + public void AddsExceptionInheritanceOnErrorClasses() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + + var declaration = model.StartBlock as CodeClass.Declaration; + + Assert.Contains("Exception", declaration.Usings.Select(x => x.Name)); + Assert.Equal("Exception", declaration.Inherits.Name); + } + [Fact] + public void FailsExceptionInheritanceOnErrorClassesWhichAlreadyInherit() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var declaration = model.StartBlock as CodeClass.Declaration; + declaration.Inherits = new CodeType { + Name = "SomeOtherModel" + }; + Assert.Throws(() => ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Java }, root)); + } + [Fact] + public void AddsUsingsForErrorTypesForRequestExecutor() { + var requestBuilder = root.AddClass(new CodeClass { + Name = "somerequestbuilder", + ClassKind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = root.AddNamespace($"{root.Name}.subns"); // otherwise the import gets trimmed + var errorClass = subNS.AddClass(new CodeClass { + Name = "Error4XX", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var requestExecutor = requestBuilder.AddMethod(new CodeMethod { + Name = "get", + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string" + }, + ErrorMappings = new () { + { "4XX", new CodeType { + Name = "Error4XX", + TypeDefinition = errorClass, + } + }, + }, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + + var declaration = requestBuilder.StartBlock as CodeClass.Declaration; + + Assert.Contains("Error4XX", declaration.Usings.Select(x => x.Declaration?.Name)); + } + [Fact] public void EscapesReservedKeywordsInInternalDeclaration() { var model = root.AddClass(new CodeClass { Name = "break", From f534a36e3fa997a3934c0788149ce083bf02d123 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 11:20:31 -0500 Subject: [PATCH 15/36] - adds error mapping to java method code generation Signed-off-by: Vincent Biret --- .../Writers/Java/CodeMethodWriter.cs | 27 +- .../Writers/Java/CodeMethodWriterTests.cs | 1222 +++++++++-------- 2 files changed, 643 insertions(+), 606 deletions(-) diff --git a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs index c0caedcd72..2700ec89a1 100644 --- a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs @@ -26,7 +26,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri WriteMethodPrototype(codeElement, writer, returnType); writer.IncreaseIndent(); var parentClass = codeElement.Parent as CodeClass; - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + var inherits = parentClass.StartBlock is CodeClass.Declaration declaration && declaration.Inherits != null && !parentClass.IsErrorDefinition; var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); @@ -35,10 +35,10 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri AddNullChecks(codeElement, writer); switch(codeElement.MethodKind) { case CodeMethodKind.Serializer: - WriteSerializerBody(parentClass, codeElement, writer); + WriteSerializerBody(parentClass, codeElement, writer, inherits); break; case CodeMethodKind.Deserializer: - WriteDeserializerBody(codeElement, codeElement, parentClass, writer); + WriteDeserializerBody(codeElement, codeElement, parentClass, writer, inherits); break; case CodeMethodKind.IndexerBackwardCompatibility: WriteIndexerBody(codeElement, parentClass, writer, returnType); @@ -196,8 +196,7 @@ private void WriteIndexerBody(CodeMethod codeElement, CodeClass parentClass, Lan (codeElement.OriginalIndexer.IndexType, codeElement.OriginalIndexer.ParameterName, "id")); conventions.AddRequestBuilderBody(parentClass, returnType, writer, conventions.TempDictionaryVarName); } - private void WriteDeserializerBody(CodeMethod codeElement, CodeMethod method, CodeClass parentClass, LanguageWriter writer) { - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + private void WriteDeserializerBody(CodeMethod codeElement, CodeMethod method, CodeClass parentClass, LanguageWriter writer, bool inherits) { var fieldToSerialize = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom); writer.WriteLine($"return new HashMap<>({(inherits ? "super." + codeElement.Name.ToFirstCharacterLowerCase()+ "()" : fieldToSerialize.Count())}) {{{{"); if(fieldToSerialize.Any()) { @@ -222,10 +221,20 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ WriteGeneratorMethodCall(codeElement, requestParams, writer, $"final RequestInformation {RequestInfoVarName} = "); var sendMethodName = GetSendRequestMethodName(codeElement.ReturnType.IsCollection, returnType); var responseHandlerParam = codeElement.Parameters.FirstOrDefault(x => x.IsOfKind(CodeParameterKind.ResponseHandler)); + var errorMappingVarName = "null"; + if(codeElement.ErrorMappings.Any()) { + errorMappingVarName = "errorMapping"; + writer.WriteLine($"final HashMap> {errorMappingVarName} = new HashMap>({codeElement.ErrorMappings.Count}) {{{{"); + writer.IncreaseIndent(); + foreach(var errorMapping in codeElement.ErrorMappings) { + writer.WriteLine($"put(\"{errorMapping.Key.ToUpperInvariant()}\", {errorMapping.Value.Name.ToFirstCharacterUpperCase()}.class);"); + } + writer.CloseBlock("}};"); + } if(codeElement.Parameters.Any(x => x.IsOfKind(CodeParameterKind.ResponseHandler))) - writer.WriteLine($"return this.requestAdapter.{sendMethodName}({RequestInfoVarName}, {returnType}.class, {responseHandlerParam.Name});"); + writer.WriteLine($"return this.requestAdapter.{sendMethodName}({RequestInfoVarName}, {returnType}.class, {responseHandlerParam.Name}, {errorMappingVarName});"); else - writer.WriteLine($"return this.requestAdapter.{sendMethodName}({RequestInfoVarName}, {returnType}.class, null);"); + writer.WriteLine($"return this.requestAdapter.{sendMethodName}({RequestInfoVarName}, {returnType}.class, null, {errorMappingVarName});"); writer.DecreaseIndent(); writer.WriteLine("} catch (URISyntaxException ex) {"); writer.IncreaseIndent(); @@ -306,9 +315,9 @@ private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams req writer.WriteLine($"return {RequestInfoVarName};"); } private static string GetPropertyCall(CodeProperty property, string defaultValue) => property == null ? defaultValue : $"{property.Name.ToFirstCharacterLowerCase()}"; - private void WriteSerializerBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer) { + private void WriteSerializerBody(CodeClass parentClass, CodeMethod method, LanguageWriter writer, bool inherits) { var additionalDataProperty = parentClass.GetPropertyOfKind(CodePropertyKind.AdditionalData); - if((parentClass.StartBlock as CodeClass.Declaration).Inherits != null) + if(inherits) writer.WriteLine("super.serialize(writer);"); foreach(var otherProp in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom)) { var accessorName = otherProp.IsNameEscaped && !string.IsNullOrEmpty(otherProp.SerializationName) ? otherProp.SerializationName : otherProp.Name.ToFirstCharacterLowerCase(); diff --git a/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs index 5bb690cef8..d762b64b91 100644 --- a/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs @@ -6,605 +6,633 @@ using Kiota.Builder.Tests; using Xunit; -namespace Kiota.Builder.Writers.Java.Tests { - public class CodeMethodWriterTests : IDisposable { - private const string DefaultPath = "./"; - private const string DefaultName = "name"; - private readonly StringWriter tw; - private readonly LanguageWriter writer; - private readonly CodeMethod method; - private readonly CodeClass parentClass; - private readonly CodeNamespace root; - private const string MethodName = "methodName"; - private const string ReturnTypeName = "Somecustomtype"; - private const string MethodDescription = "some description"; - private const string ParamDescription = "some parameter description"; - private const string ParamName = "paramName"; - public CodeMethodWriterTests() - { - writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Java, DefaultPath, DefaultName); - tw = new StringWriter(); - writer.SetTextWriter(tw); - root = CodeNamespace.InitRootNamespace(); - parentClass = new CodeClass { - Name = "parentClass" - }; - root.AddClass(parentClass); - method = new CodeMethod { - Name = MethodName, - }; - method.ReturnType = new CodeType { - Name = ReturnTypeName - }; - parentClass.AddMethod(method); - } - public void Dispose() - { - tw?.Dispose(); - GC.SuppressFinalize(this); - } - private void AddRequestProperties() { - parentClass.AddProperty(new CodeProperty { - Name = "requestAdapter", - PropertyKind = CodePropertyKind.RequestAdapter, - }); - parentClass.AddProperty(new CodeProperty { - Name = "pathParameters", - PropertyKind = CodePropertyKind.PathParameters, - Type = new CodeType { - Name = "string", - } - }); - parentClass.AddProperty(new CodeProperty { - Name = "urlTemplate", - PropertyKind = CodePropertyKind.UrlTemplate, - }); - } - private void AddSerializationProperties() { - var addData = parentClass.AddProperty(new CodeProperty { - Name = "additionalData", - PropertyKind = CodePropertyKind.AdditionalData, - }).First(); - addData.Type = new CodeType { - Name = "string" - }; - var dummyProp = parentClass.AddProperty(new CodeProperty { - Name = "dummyProp", - }).First(); - dummyProp.Type = new CodeType { - Name = "string" - }; - var dummyCollectionProp = parentClass.AddProperty(new CodeProperty { - Name = "dummyColl", - }).First(); - dummyCollectionProp.Type = new CodeType { - Name = "string", - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, - }; - var dummyComplexCollection = parentClass.AddProperty(new CodeProperty { - Name = "dummyComplexColl" - }).First(); - dummyComplexCollection.Type = new CodeType { - Name = "Complex", - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, - TypeDefinition = new CodeClass { - Name = "SomeComplexType" - } - }; - var dummyEnumProp = parentClass.AddProperty(new CodeProperty{ - Name = "dummyEnumCollection", - }).First(); - dummyEnumProp.Type = new CodeType { - Name = "SomeEnum", - TypeDefinition = new CodeEnum { - Name = "EnumType" - } - }; - } - private void AddInheritanceClass() { - (parentClass.StartBlock as CodeClass.Declaration).Inherits = new CodeType { - Name = "someParentClass" - }; - } - private void AddRequestBodyParameters() { - var stringType = new CodeType { +namespace Kiota.Builder.Writers.Java.Tests; +public class CodeMethodWriterTests : IDisposable { + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeMethod method; + private readonly CodeClass parentClass; + private readonly CodeNamespace root; + private const string MethodName = "methodName"; + private const string ReturnTypeName = "Somecustomtype"; + private const string MethodDescription = "some description"; + private const string ParamDescription = "some parameter description"; + private const string ParamName = "paramName"; + public CodeMethodWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Java, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + parentClass = new CodeClass { + Name = "parentClass" + }; + root.AddClass(parentClass); + method = new CodeMethod { + Name = MethodName, + }; + method.ReturnType = new CodeType { + Name = ReturnTypeName + }; + parentClass.AddMethod(method); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + private void AddRequestProperties() { + parentClass.AddProperty(new CodeProperty { + Name = "requestAdapter", + PropertyKind = CodePropertyKind.RequestAdapter, + }); + parentClass.AddProperty(new CodeProperty { + Name = "pathParameters", + PropertyKind = CodePropertyKind.PathParameters, + Type = new CodeType { Name = "string", - }; - method.AddParameter(new CodeParameter { - Name = "h", - ParameterKind = CodeParameterKind.Headers, - Type = stringType, - }); - method.AddParameter(new CodeParameter{ - Name = "q", - ParameterKind = CodeParameterKind.QueryParameter, - Type = stringType, - }); - method.AddParameter(new CodeParameter{ - Name = "b", - ParameterKind = CodeParameterKind.RequestBody, - Type = stringType, - }); - method.AddParameter(new CodeParameter{ - Name = "r", - ParameterKind = CodeParameterKind.ResponseHandler, - Type = stringType, - }); - method.AddParameter(new CodeParameter { - Name = "o", - ParameterKind = CodeParameterKind.Options, - Type = stringType, - }); - } - [Fact] - public void WritesNullableVoidTypeForExecutor(){ - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - method.ReturnType = new CodeType { - Name = "void", - }; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("CompletableFuture", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestBuilder() { - method.MethodKind = CodeMethodKind.RequestBuilderBackwardCompatibility; - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void WritesRequestBodiesThrowOnNullHttpMethod() { - method.MethodKind = CodeMethodKind.RequestExecutor; - Assert.Throws(() => writer.Write(method)); - method.MethodKind = CodeMethodKind.RequestGenerator; - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void WritesRequestExecutorBody() { - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("final RequestInformation requestInfo", result); - Assert.Contains("sendAsync", result); - Assert.Contains("CompletableFuture.failedFuture(ex)", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestExecutorBodyForCollections() { - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - method.ReturnType.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("sendCollectionAsync", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestGeneratorBody() { - method.MethodKind = CodeMethodKind.RequestGenerator; - method.HttpMethod = HttpMethod.Get; - AddRequestProperties(); - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("final RequestInformation requestInfo = new RequestInformation()", result); - Assert.Contains("urlTemplate =", result); - Assert.Contains("pathParameters =", result); - Assert.Contains("httpMethod = HttpMethod.GET", result); - Assert.Contains("h.accept(requestInfo.headers)", result); - Assert.Contains("AddQueryParameters", result); - Assert.Contains("setContentFromParsable", result); - Assert.Contains("addRequestOptions", result); - Assert.Contains("return requestInfo;", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestGeneratorOverloadBody() { - method.MethodKind = CodeMethodKind.RequestGenerator; - method.HttpMethod = HttpMethod.Get; - method.OriginalMethod = method; - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain("final RequestInformation requestInfo = new RequestInformation()", result); - Assert.DoesNotContain("httpMethod = HttpMethod.GET", result); - Assert.DoesNotContain("h.accept(requestInfo.headers)", result); - Assert.DoesNotContain("AddQueryParameters", result); - Assert.DoesNotContain("setContentFromParsable", result); - Assert.DoesNotContain("addRequestOptions", result); - Assert.DoesNotContain("return requestInfo;", result); - Assert.Contains("return methodName(b, q, h, o)", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesInheritedDeSerializerBody() { - method.MethodKind = CodeMethodKind.Deserializer; - method.IsAsync = false; - AddSerializationProperties(); - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("super.methodName()", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesDeSerializerBody() { - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { - Name = "string" - }; - method.MethodKind = CodeMethodKind.Deserializer; - method.IsAsync = false; - AddSerializationProperties(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("getStringValue", result); - Assert.Contains("getCollectionOfPrimitiveValues", result); - Assert.Contains("getCollectionOfObjectValues", result); - Assert.Contains("getEnumValue", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesInheritedSerializerBody() { - method.MethodKind = CodeMethodKind.Serializer; - method.IsAsync = false; - AddSerializationProperties(); - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("super.serialize", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesSerializerBody() { - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { - Name = "string" - }; - method.MethodKind = CodeMethodKind.Serializer; - method.IsAsync = false; - AddSerializationProperties(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("writeStringValue", result); - Assert.Contains("writeCollectionOfPrimitiveValues", result); - Assert.Contains("writeCollectionOfObjectValues", result); - Assert.Contains("writeEnumValue", result); - Assert.Contains("writeAdditionalData(this.getAdditionalData());", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesMethodAsyncDescription() { - - method.Description = MethodDescription; - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { + } + }); + parentClass.AddProperty(new CodeProperty { + Name = "urlTemplate", + PropertyKind = CodePropertyKind.UrlTemplate, + }); + } + private void AddSerializationProperties() { + var addData = parentClass.AddProperty(new CodeProperty { + Name = "additionalData", + PropertyKind = CodePropertyKind.AdditionalData, + }).First(); + addData.Type = new CodeType { + Name = "string" + }; + var dummyProp = parentClass.AddProperty(new CodeProperty { + Name = "dummyProp", + }).First(); + dummyProp.Type = new CodeType { + Name = "string" + }; + var dummyCollectionProp = parentClass.AddProperty(new CodeProperty { + Name = "dummyColl", + }).First(); + dummyCollectionProp.Type = new CodeType { + Name = "string", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }; + var dummyComplexCollection = parentClass.AddProperty(new CodeProperty { + Name = "dummyComplexColl" + }).First(); + dummyComplexCollection.Type = new CodeType { + Name = "Complex", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + TypeDefinition = new CodeClass { + Name = "SomeComplexType" + } + }; + var dummyEnumProp = parentClass.AddProperty(new CodeProperty{ + Name = "dummyEnumCollection", + }).First(); + dummyEnumProp.Type = new CodeType { + Name = "SomeEnum", + TypeDefinition = new CodeEnum { + Name = "EnumType" + } + }; + } + private void AddInheritanceClass() { + (parentClass.StartBlock as CodeClass.Declaration).Inherits = new CodeType { + Name = "someParentClass" + }; + } + private void AddRequestBodyParameters() { + var stringType = new CodeType { + Name = "string", + }; + method.AddParameter(new CodeParameter { + Name = "h", + ParameterKind = CodeParameterKind.Headers, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "q", + ParameterKind = CodeParameterKind.QueryParameter, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "b", + ParameterKind = CodeParameterKind.RequestBody, + Type = stringType, + }); + method.AddParameter(new CodeParameter{ + Name = "r", + ParameterKind = CodeParameterKind.ResponseHandler, + Type = stringType, + }); + method.AddParameter(new CodeParameter { + Name = "o", + ParameterKind = CodeParameterKind.Options, + Type = stringType, + }); + } + [Fact] + public void WritesNullableVoidTypeForExecutor(){ + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + method.ReturnType = new CodeType { + Name = "void", + }; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("CompletableFuture", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestBuilder() { + method.MethodKind = CodeMethodKind.RequestBuilderBackwardCompatibility; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestBodiesThrowOnNullHttpMethod() { + method.MethodKind = CodeMethodKind.RequestExecutor; + Assert.Throws(() => writer.Write(method)); + method.MethodKind = CodeMethodKind.RequestGenerator; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestExecutorBody() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass{ + Name = "Error4XX", + }).First(); + var error5XX = root.AddClass(new CodeClass{ + Name = "Error5XX", + }).First(); + var error401 = root.AddClass(new CodeClass{ + Name = "Error401", + }).First(); + method.ErrorMappings = new () { + {"4XX", new CodeType {Name = "Error4XX", TypeDefinition = error4XX}}, + {"5XX", new CodeType {Name = "Error5XX", TypeDefinition = error5XX}}, + {"403", new CodeType {Name = "Error403", TypeDefinition = error401}}, + }; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("final RequestInformation requestInfo", result); + Assert.Contains("final HashMap> errorMapping = new HashMap>", result); + Assert.Contains("put(\"4XX\", Error4XX.class);", result); + Assert.Contains("put(\"5XX\", Error5XX.class);", result); + Assert.Contains("put(\"403\", Error403.class);", result); + Assert.Contains("sendAsync", result); + Assert.Contains("CompletableFuture.failedFuture(ex)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesntCreateDictionaryOnEmptyErrorMapping() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("final HashMap> errorMapping = new HashMap>", result); + Assert.Contains("null", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestExecutorBodyForCollections() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + method.ReturnType.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("sendCollectionAsync", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorBody() { + method.MethodKind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + AddRequestProperties(); + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("final RequestInformation requestInfo = new RequestInformation()", result); + Assert.Contains("urlTemplate =", result); + Assert.Contains("pathParameters =", result); + Assert.Contains("httpMethod = HttpMethod.GET", result); + Assert.Contains("h.accept(requestInfo.headers)", result); + Assert.Contains("AddQueryParameters", result); + Assert.Contains("setContentFromParsable", result); + Assert.Contains("addRequestOptions", result); + Assert.Contains("return requestInfo;", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestGeneratorOverloadBody() { + method.MethodKind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + method.OriginalMethod = method; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("final RequestInformation requestInfo = new RequestInformation()", result); + Assert.DoesNotContain("httpMethod = HttpMethod.GET", result); + Assert.DoesNotContain("h.accept(requestInfo.headers)", result); + Assert.DoesNotContain("AddQueryParameters", result); + Assert.DoesNotContain("setContentFromParsable", result); + Assert.DoesNotContain("addRequestOptions", result); + Assert.DoesNotContain("return requestInfo;", result); + Assert.Contains("return methodName(b, q, h, o)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedDeSerializerBody() { + method.MethodKind = CodeMethodKind.Deserializer; + method.IsAsync = false; + AddSerializationProperties(); + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("super.methodName()", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesDeSerializerBody() { + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.MethodKind = CodeMethodKind.Deserializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("getStringValue", result); + Assert.Contains("getCollectionOfPrimitiveValues", result); + Assert.Contains("getCollectionOfObjectValues", result); + Assert.Contains("getEnumValue", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedSerializerBody() { + method.MethodKind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("super.serialize", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesSerializerBody() { + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.MethodKind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("writeStringValue", result); + Assert.Contains("writeCollectionOfPrimitiveValues", result); + Assert.Contains("writeCollectionOfObjectValues", result); + Assert.Contains("writeEnumValue", result); + Assert.Contains("writeAdditionalData(this.getAdditionalData());", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesMethodAsyncDescription() { + + method.Description = MethodDescription; + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.AddParameter(parameter); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("/**", result); + Assert.Contains(MethodDescription, result); + Assert.Contains("@param ", result); + Assert.Contains(ParamName, result); + Assert.Contains(ParamDescription, result); + Assert.Contains("@return a CompletableFuture of", result); + Assert.Contains("*/", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesMethodSyncDescription() { + + method.Description = MethodDescription; + method.IsAsync = false; + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.AddParameter(parameter); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("@return a CompletableFuture of", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void Defensive() { + var codeMethodWriter = new CodeMethodWriter(new JavaConventionService()); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); + var originalParent = method.Parent; + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + method.Parent = originalParent; + method.ReturnType = null; + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + } + [Fact] + public void ThrowsIfParentIsNotClass() { + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void ThrowsIfReturnTypeIsMissing() { + method.ReturnType = null; + Assert.Throws(() => writer.Write(method)); + } + private const string TaskPrefix = "CompletableFuture<"; + [Fact] + public void WritesReturnType() { + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"{TaskPrefix}{ReturnTypeName}> {MethodName}", result);// async default + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesNotAddAsyncInformationOnSyncMethods() { + method.IsAsync = false; + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain(TaskPrefix, result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPublicMethodByDefault() { + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("public ", result);// public default + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPrivateMethod() { + method.Access = AccessModifier.Private; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("private ", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesProtectedMethod() { + method.Access = AccessModifier.Protected; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("protected ", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesIndexer() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.IndexerBackwardCompatibility; + method.OriginalIndexer = new CodeIndexer { + Name = "idx", + IndexType = new CodeType { + Name = "int" + }, + ParameterName = "collectionId" + }; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("collectionId", result); + Assert.Contains("requestAdapter", result); + Assert.Contains("pathParameters", result); + Assert.Contains("id", result); + Assert.Contains("return new", result); + } + [Fact] + public void WritesPathParameterRequestBuilder() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; + method.AddParameter(new CodeParameter { + Name = "pathParam", + ParameterKind = CodeParameterKind.Path, + Type = new CodeType { Name = "string" - }; - method.AddParameter(parameter); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("/**", result); - Assert.Contains(MethodDescription, result); - Assert.Contains("@param ", result); - Assert.Contains(ParamName, result); - Assert.Contains(ParamDescription, result); - Assert.Contains("@return a CompletableFuture of", result); - Assert.Contains("*/", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesMethodSyncDescription() { - - method.Description = MethodDescription; - method.IsAsync = false; - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("requestAdapter", result); + Assert.Contains("pathParameters", result); + Assert.Contains("pathParam", result); + Assert.Contains("return new", result); + } + [Fact] + public void WritesGetterToBackingStore() { + parentClass.AddBackingStoreProperty(); + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Getter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("this.getBackingStore().get(\"someProperty\")", result); + } + [Fact] + public void WritesGetterToBackingStoreWithNonnullProperty() { + method.AddAccessedProperty(); + parentClass.AddBackingStoreProperty(); + method.AccessedProperty.Type = new CodeType { + Name = "string", + IsNullable = false, + }; + var defaultValue = "someDefaultValue"; + method.AccessedProperty.DefaultValue = defaultValue; + method.MethodKind = CodeMethodKind.Getter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("if(value == null)", result); + Assert.Contains(defaultValue, result); + } + [Fact] + public void WritesSetterToBackingStore() { + parentClass.AddBackingStoreProperty(); + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Setter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("this.getBackingStore().set(\"someProperty\", value)", result); + } + [Fact] + public void WritesGetterToField() { + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Getter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("this.someProperty", result); + } + [Fact] + public void WritesSetterToField() { + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Setter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("this.someProperty = value", result); + } + [Fact] + public void WritesConstructor() { + method.MethodKind = CodeMethodKind.Constructor; + var defaultValue = "someVal"; + var propName = "propWithDefaultValue"; + parentClass.ClassKind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty { + Name = propName, + DefaultValue = defaultValue, + PropertyKind = CodePropertyKind.UrlTemplate, + }); + AddRequestProperties(); + method.AddParameter(new CodeParameter { + Name = "pathParameters", + ParameterKind = CodeParameterKind.PathParameters, + Type = new CodeType { + Name = "Map" + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains($"this.{propName} = {defaultValue}", result); + Assert.Contains("new Map(pathParameters)", result); + } + [Fact] + public void WritesRawUrlConstructor() { + method.MethodKind = CodeMethodKind.RawUrlConstructor; + var defaultValue = "someVal"; + var propName = "propWithDefaultValue"; + parentClass.ClassKind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty { + Name = propName, + DefaultValue = defaultValue, + PropertyKind = CodePropertyKind.UrlTemplate, + }); + AddRequestProperties(); + method.AddParameter(new CodeParameter { + Name = "rawUrl", + ParameterKind = CodeParameterKind.RawUrl, + Type = new CodeType { Name = "string" - }; - method.AddParameter(parameter); - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain("@return a CompletableFuture of", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void Defensive() { - var codeMethodWriter = new CodeMethodWriter(new JavaConventionService()); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); - var originalParent = method.Parent; - method.Parent = CodeNamespace.InitRootNamespace(); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); - method.Parent = originalParent; - method.ReturnType = null; - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); - } - [Fact] - public void ThrowsIfParentIsNotClass() { - method.Parent = CodeNamespace.InitRootNamespace(); - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void ThrowsIfReturnTypeIsMissing() { - method.ReturnType = null; - Assert.Throws(() => writer.Write(method)); - } - private const string TaskPrefix = "CompletableFuture<"; - [Fact] - public void WritesReturnType() { - writer.Write(method); - var result = tw.ToString(); - Assert.Contains($"{TaskPrefix}{ReturnTypeName}> {MethodName}", result);// async default - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void DoesNotAddAsyncInformationOnSyncMethods() { - method.IsAsync = false; - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain(TaskPrefix, result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesPublicMethodByDefault() { - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("public ", result);// public default - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesPrivateMethod() { - method.Access = AccessModifier.Private; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("private ", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesProtectedMethod() { - method.Access = AccessModifier.Protected; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("protected ", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesIndexer() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.IndexerBackwardCompatibility; - method.OriginalIndexer = new CodeIndexer { - Name = "idx", - IndexType = new CodeType { - Name = "int" - }, - ParameterName = "collectionId" - }; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("collectionId", result); - Assert.Contains("requestAdapter", result); - Assert.Contains("pathParameters", result); - Assert.Contains("id", result); - Assert.Contains("return new", result); - } - [Fact] - public void WritesPathParameterRequestBuilder() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; - method.AddParameter(new CodeParameter { - Name = "pathParam", - ParameterKind = CodeParameterKind.Path, - Type = new CodeType { - Name = "string" - } - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("requestAdapter", result); - Assert.Contains("pathParameters", result); - Assert.Contains("pathParam", result); - Assert.Contains("return new", result); - } - [Fact] - public void WritesGetterToBackingStore() { - parentClass.AddBackingStoreProperty(); - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Getter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("this.getBackingStore().get(\"someProperty\")", result); - } - [Fact] - public void WritesGetterToBackingStoreWithNonnullProperty() { - method.AddAccessedProperty(); - parentClass.AddBackingStoreProperty(); - method.AccessedProperty.Type = new CodeType { - Name = "string", - IsNullable = false, - }; - var defaultValue = "someDefaultValue"; - method.AccessedProperty.DefaultValue = defaultValue; - method.MethodKind = CodeMethodKind.Getter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("if(value == null)", result); - Assert.Contains(defaultValue, result); - } - [Fact] - public void WritesSetterToBackingStore() { - parentClass.AddBackingStoreProperty(); - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Setter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("this.getBackingStore().set(\"someProperty\", value)", result); - } - [Fact] - public void WritesGetterToField() { - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Getter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("this.someProperty", result); - } - [Fact] - public void WritesSetterToField() { - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Setter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("this.someProperty = value", result); - } - [Fact] - public void WritesConstructor() { - method.MethodKind = CodeMethodKind.Constructor; - var defaultValue = "someVal"; - var propName = "propWithDefaultValue"; - parentClass.ClassKind = CodeClassKind.RequestBuilder; - parentClass.AddProperty(new CodeProperty { - Name = propName, - DefaultValue = defaultValue, - PropertyKind = CodePropertyKind.UrlTemplate, - }); - AddRequestProperties(); - method.AddParameter(new CodeParameter { - Name = "pathParameters", - ParameterKind = CodeParameterKind.PathParameters, - Type = new CodeType { - Name = "Map" - } - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains($"this.{propName} = {defaultValue}", result); - Assert.Contains("new Map(pathParameters)", result); - } - [Fact] - public void WritesRawUrlConstructor() { - method.MethodKind = CodeMethodKind.RawUrlConstructor; - var defaultValue = "someVal"; - var propName = "propWithDefaultValue"; - parentClass.ClassKind = CodeClassKind.RequestBuilder; - parentClass.AddProperty(new CodeProperty { - Name = propName, - DefaultValue = defaultValue, - PropertyKind = CodePropertyKind.UrlTemplate, - }); - AddRequestProperties(); - method.AddParameter(new CodeParameter { - Name = "rawUrl", - ParameterKind = CodeParameterKind.RawUrl, - Type = new CodeType { - Name = "string" - } - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains($"this.{propName} = {defaultValue}", result); - Assert.Contains($"urlTplParams.put(\"request-raw-url\", rawUrl);", result); - } - [Fact] - public void WritesApiConstructor() { - method.MethodKind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty { - Name = "core", - PropertyKind = CodePropertyKind.RequestAdapter, - }).First(); - coreProp.Type = new CodeType { - Name = "HttpCore", - IsExternal = true, - }; - method.AddParameter(new CodeParameter { - Name = "core", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, - }); - method.DeserializerModules = new() {"com.microsoft.kiota.serialization.Deserializer"}; - method.SerializerModules = new() {"com.microsoft.kiota.serialization.Serializer"}; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains("registerDefaultSerializer", result); - Assert.Contains("registerDefaultDeserializer", result); - } - [Fact] - public void WritesApiConstructorWithBackingStore() { - method.MethodKind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty { - Name = "core", - PropertyKind = CodePropertyKind.RequestAdapter, - }).First(); - coreProp.Type = new CodeType { - Name = "HttpCore", - IsExternal = true, - }; - method.AddParameter(new CodeParameter { - Name = "core", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, - }); - var backingStoreParam = new CodeParameter { - Name = "backingStore", - ParameterKind = CodeParameterKind.BackingStore, - }; - backingStoreParam.Type = new CodeType { - Name = "BackingStore", - IsExternal = true, - }; - method.AddParameter(backingStoreParam); - var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Java, DefaultPath, DefaultName); - tempWriter.SetTextWriter(tw); - tempWriter.Write(method); - var result = tw.ToString(); - Assert.Contains("enableBackingStore", result); - } - [Fact] - public void AccessorsTargetingEscapedPropertiesAreNotEscapedThemselves() { - var model = root.AddClass(new CodeClass { - Name = "SomeClass", - ClassKind = CodeClassKind.Model - }).First(); - model.AddProperty(new CodeProperty { - Name = "short", - Type = new CodeType { Name = "string" }, - Access = AccessModifier.Public, - PropertyKind = CodePropertyKind.Custom, - }); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); - var getter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Getter)); - var setter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Setter)); - var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Java, DefaultPath, DefaultName); - tempWriter.SetTextWriter(tw); - tempWriter.Write(getter); - var result = tw.ToString(); - Assert.Contains("getShort", result); - Assert.DoesNotContain("getShort_escaped", result); - - using var tw2 = new StringWriter(); - tempWriter.SetTextWriter(tw2); - tempWriter.Write(setter); - result = tw2.ToString(); - Assert.Contains("setShort", result); - Assert.DoesNotContain("setShort_escaped", result); - } + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains($"this.{propName} = {defaultValue}", result); + Assert.Contains($"urlTplParams.put(\"request-raw-url\", rawUrl);", result); + } + [Fact] + public void WritesApiConstructor() { + method.MethodKind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty { + Name = "core", + PropertyKind = CodePropertyKind.RequestAdapter, + }).First(); + coreProp.Type = new CodeType { + Name = "HttpCore", + IsExternal = true, + }; + method.AddParameter(new CodeParameter { + Name = "core", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + method.DeserializerModules = new() {"com.microsoft.kiota.serialization.Deserializer"}; + method.SerializerModules = new() {"com.microsoft.kiota.serialization.Serializer"}; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains("registerDefaultSerializer", result); + Assert.Contains("registerDefaultDeserializer", result); + } + [Fact] + public void WritesApiConstructorWithBackingStore() { + method.MethodKind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty { + Name = "core", + PropertyKind = CodePropertyKind.RequestAdapter, + }).First(); + coreProp.Type = new CodeType { + Name = "HttpCore", + IsExternal = true, + }; + method.AddParameter(new CodeParameter { + Name = "core", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + var backingStoreParam = new CodeParameter { + Name = "backingStore", + ParameterKind = CodeParameterKind.BackingStore, + }; + backingStoreParam.Type = new CodeType { + Name = "BackingStore", + IsExternal = true, + }; + method.AddParameter(backingStoreParam); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Java, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(method); + var result = tw.ToString(); + Assert.Contains("enableBackingStore", result); + } + [Fact] + public void AccessorsTargetingEscapedPropertiesAreNotEscapedThemselves() { + var model = root.AddClass(new CodeClass { + Name = "SomeClass", + ClassKind = CodeClassKind.Model + }).First(); + model.AddProperty(new CodeProperty { + Name = "short", + Type = new CodeType { Name = "string" }, + Access = AccessModifier.Public, + PropertyKind = CodePropertyKind.Custom, + }); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Java }, root); + var getter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Getter)); + var setter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Setter)); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Java, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(getter); + var result = tw.ToString(); + Assert.Contains("getShort", result); + Assert.DoesNotContain("getShort_escaped", result); + + using var tw2 = new StringWriter(); + tempWriter.SetTextWriter(tw2); + tempWriter.Write(setter); + result = tw2.ToString(); + Assert.Contains("setShort", result); + Assert.DoesNotContain("setShort_escaped", result); } } From 85424b88444d54fc92ee22386f395f8dd3d21aa5 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 12:26:49 -0500 Subject: [PATCH 16/36] - adds intermediate exception type for Java --- .../com/microsoft/kiota/ApiException.java | 21 +++++++++++++++++++ .../kiota/http/OkHttpRequestAdapter.java | 5 +++-- src/Kiota.Builder/Refiners/JavaRefiner.cs | 4 ++-- .../Refiners/JavaLanguageRefinerTests.cs | 4 ++-- 4 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 abstractions/java/lib/src/main/java/com/microsoft/kiota/ApiException.java diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/ApiException.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/ApiException.java new file mode 100644 index 0000000000..78c0a926b7 --- /dev/null +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/ApiException.java @@ -0,0 +1,21 @@ +package com.microsoft.kiota; + +/** Parent type for exceptions thrown by the client when receiving failed responses to its requests. */ +public class ApiException extends Exception { + /** {@inheritdoc} */ + public ApiException() { + super(); + } + /** {@inheritdoc} */ + public ApiException(String message) { + super(message); + } + /** {@inheritdoc} */ + public ApiException(String message, Throwable cause) { + super(message, cause); + } + /** {@inheritdoc} */ + public ApiException(Throwable cause) { + super(cause); + } +} diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index a5f163942e..57c1eba356 100644 --- a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -16,6 +16,7 @@ import javax.annotation.Nullable; import com.microsoft.kiota.ApiClientBuilder; +import com.microsoft.kiota.ApiException; import com.microsoft.kiota.RequestInformation; import com.microsoft.kiota.RequestOption; import com.microsoft.kiota.ResponseHandler; @@ -221,7 +222,7 @@ private CompletableFuture throwFailedResponse(final Response response, !errorMappings.containsKey(statusCodeAsString) && !(statusCode >= 400 && statusCode < 500 && errorMappings.containsKey("4XX")) && !(statusCode >= 500 && statusCode < 600 && errorMappings.containsKey("5XX"))) { - return CompletableFuture.failedFuture(new RuntimeException("the server returned an unexpected status code and no error class is registered for this code " + statusCode)); + return CompletableFuture.failedFuture(new ApiException("the server returned an unexpected status code and no error class is registered for this code " + statusCode)); } final Class errorClass = errorMappings.containsKey(statusCodeAsString) ? errorMappings.get(statusCodeAsString) : @@ -234,7 +235,7 @@ private CompletableFuture throwFailedResponse(final Response response, if (error instanceof Exception) { return CompletableFuture.failedFuture((Exception)error); } else { - return CompletableFuture.failedFuture(new RuntimeException("unexpected error type " + error.getClass().getName())); + return CompletableFuture.failedFuture(new ApiException("unexpected error type " + error.getClass().getName())); } } catch (IOException ex) { return CompletableFuture.failedFuture(ex); diff --git a/src/Kiota.Builder/Refiners/JavaRefiner.cs b/src/Kiota.Builder/Refiners/JavaRefiner.cs index 86b24188d0..17fb78e16f 100644 --- a/src/Kiota.Builder/Refiners/JavaRefiner.cs +++ b/src/Kiota.Builder/Refiners/JavaRefiner.cs @@ -39,8 +39,8 @@ public override void Refine(CodeNamespace generatedCode) new [] { "com.microsoft.kiota.serialization.ParseNodeFactoryRegistry" }); AddParentClassToErrorClasses( generatedCode, - "Exception", - "java.lang" + "ApiException", + "com.microsoft.kiota" ); } private static void SetSetterParametersToNullable(CodeElement currentElement, params Tuple[] accessorPairs) { diff --git a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs index 9649894691..b688fdbfca 100644 --- a/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/JavaLanguageRefinerTests.cs @@ -17,8 +17,8 @@ public void AddsExceptionInheritanceOnErrorClasses() { var declaration = model.StartBlock as CodeClass.Declaration; - Assert.Contains("Exception", declaration.Usings.Select(x => x.Name)); - Assert.Equal("Exception", declaration.Inherits.Name); + Assert.Contains("ApiException", declaration.Usings.Select(x => x.Name)); + Assert.Equal("ApiException", declaration.Inherits.Name); } [Fact] public void FailsExceptionInheritanceOnErrorClassesWhichAlreadyInherit() { From 1c31f32707eac81ad0e7c1f10a2b2dca7df55a6b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 12:27:27 -0500 Subject: [PATCH 17/36] - adds intermediate exception type for CSharp --- abstractions/dotnet/src/ApiException.cs | 26 +++++++++++++++++++ src/Kiota.Builder/Refiners/CSharpRefiner.cs | 4 +-- .../Refiners/CSharpLanguageRefinerTests.cs | 4 +-- 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 abstractions/dotnet/src/ApiException.cs diff --git a/abstractions/dotnet/src/ApiException.cs b/abstractions/dotnet/src/ApiException.cs new file mode 100644 index 0000000000..1af85e0e48 --- /dev/null +++ b/abstractions/dotnet/src/ApiException.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ +using System; + +namespace Microsoft.Kiota.Abstractions; + +/// +/// Parent type for exceptions thrown by the client when receiving failed responses to its requests. +/// +public class ApiException : Exception +{ + /// + public ApiException(): base() + { + + } + /// + public ApiException(string message) : base(message) + { + } + /// + public ApiException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/Kiota.Builder/Refiners/CSharpRefiner.cs b/src/Kiota.Builder/Refiners/CSharpRefiner.cs index 6e736a31a7..2a73363c08 100644 --- a/src/Kiota.Builder/Refiners/CSharpRefiner.cs +++ b/src/Kiota.Builder/Refiners/CSharpRefiner.cs @@ -35,8 +35,8 @@ public override void Refine(CodeNamespace generatedCode) AddSerializationModulesImport(generatedCode); AddParentClassToErrorClasses( generatedCode, - "Exception", - "System" + "ApiException", + "Microsoft.Kiota.Abstractions" ); } private static void DisambiguatePropertiesWithClassNames(CodeElement currentElement) { diff --git a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs index 743afcc5c0..22331aba28 100644 --- a/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/CSharpLanguageRefinerTests.cs @@ -17,8 +17,8 @@ public void AddsExceptionInheritanceOnErrorClasses() { var declaration = model.StartBlock as CodeClass.Declaration; - Assert.Contains("Exception", declaration.Usings.Select(x => x.Name)); - Assert.Equal("Exception", declaration.Inherits.Name); + Assert.Contains("ApiException", declaration.Usings.Select(x => x.Name)); + Assert.Equal("ApiException", declaration.Inherits.Name); } [Fact] public void FailsExceptionInheritanceOnErrorClassesWhichAlreadyInherit() { From 3c3479cc95889d9f3aac17988f9ab19e9fd04132 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 13:20:17 -0500 Subject: [PATCH 18/36] - fixes error mapping declaration for inheritance Signed-off-by: Vincent Biret --- .../com/microsoft/kiota/NativeResponseHandler.java | 4 ++-- .../java/com/microsoft/kiota/RequestAdapter.java | 8 ++++---- .../java/com/microsoft/kiota/ResponseHandler.java | 2 +- .../microsoft/kiota/http/OkHttpRequestAdapter.java | 12 ++++++------ src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs | 7 +++++-- .../Writers/Java/CodeMethodWriterTests.cs | 4 ++-- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java index 82ff8a3677..b370c6b36a 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/NativeResponseHandler.java @@ -16,14 +16,14 @@ public class NativeResponseHandler implements ResponseHandler { /** The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. */ @Nullable - public HashMap> errorMappings; + public HashMap> errorMappings; /** {@inheritdoc} */ @Nonnull @Override public CompletableFuture handleResponseAsync( NativeResponseType response, - HashMap> errorMappings) { + HashMap> errorMappings) { this.value = response; this.errorMappings = errorMappings; return CompletableFuture.completedFuture(null); diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java index 44d062f2c6..828387fa94 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/RequestAdapter.java @@ -32,7 +32,7 @@ public interface RequestAdapter { * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized response model. */ - CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); + CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** * Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. * @param requestInfo the request info to execute. @@ -42,7 +42,7 @@ public interface RequestAdapter { * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized response model collection. */ - CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); + CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** * Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. * @param requestInfo the request info to execute. @@ -52,7 +52,7 @@ public interface RequestAdapter { * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized primitive response model. */ - CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); + CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** * Executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive collection response model. * @param requestInfo the request info to execute. @@ -62,7 +62,7 @@ public interface RequestAdapter { * @param the type of the response model to deserialize the response into. * @return a {@link CompletableFuture} with the deserialized primitive collection response model. */ - CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); + CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings); /** * Sets The base url for every request. * @param baseUrl The base url for every request. diff --git a/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java b/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java index d24c28f18f..bcb99d3801 100644 --- a/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java +++ b/abstractions/java/lib/src/main/java/com/microsoft/kiota/ResponseHandler.java @@ -19,5 +19,5 @@ public interface ResponseHandler { * @return A CompletableFuture that represents the asynchronous operation and contains the deserialized response. */ @Nonnull - CompletableFuture handleResponseAsync(@Nonnull final NativeResponseType response, @Nullable final HashMap> errorMappings); + CompletableFuture handleResponseAsync(@Nonnull final NativeResponseType response, @Nullable final HashMap> errorMappings); } \ No newline at end of file diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index 57c1eba356..e381ca8e37 100644 --- a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -93,7 +93,7 @@ public void enableBackingStore(@Nullable final BackingStoreFactory backingStoreF } } @Nonnull - public CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { + public CompletableFuture> sendCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); return this.getHttpResponseMessage(requestInfo) @@ -115,7 +115,7 @@ public CompletableFuture> sendC }); } @Nonnull - public CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { + public CompletableFuture sendAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); return this.getHttpResponseMessage(requestInfo) @@ -140,7 +140,7 @@ private String getMediaTypeAndSubType(final MediaType mediaType) { return mediaType.type() + "/" + mediaType.subtype(); } @Nonnull - public CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { + public CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { return this.getHttpResponseMessage(requestInfo) .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) .thenCompose(response -> { @@ -185,7 +185,7 @@ public CompletableFuture sendPrimitiveAsync(@Nonnull fina } }); } - public CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { + public CompletableFuture> sendPrimitiveCollectionAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); return this.getHttpResponseMessage(requestInfo) @@ -213,7 +213,7 @@ private ParseNode getRootParseNode(final Response response) throws IOException { return rootNode; } } - private CompletableFuture throwFailedResponse(final Response response, final HashMap> errorMappings) { + private CompletableFuture throwFailedResponse(final Response response, final HashMap> errorMappings) { if (response.isSuccessful()) return CompletableFuture.completedFuture(response); final String statusCodeAsString = Integer.toString(response.code()); @@ -224,7 +224,7 @@ private CompletableFuture throwFailedResponse(final Response response, !(statusCode >= 500 && statusCode < 600 && errorMappings.containsKey("5XX"))) { return CompletableFuture.failedFuture(new ApiException("the server returned an unexpected status code and no error class is registered for this code " + statusCode)); } - final Class errorClass = errorMappings.containsKey(statusCodeAsString) ? + final Class errorClass = errorMappings.containsKey(statusCodeAsString) ? errorMappings.get(statusCodeAsString) : (statusCode >= 400 && statusCode < 500 ? errorMappings.get("4XX") : diff --git a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs index 2700ec89a1..f1b93edc32 100644 --- a/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Java/CodeMethodWriter.cs @@ -224,7 +224,7 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ var errorMappingVarName = "null"; if(codeElement.ErrorMappings.Any()) { errorMappingVarName = "errorMapping"; - writer.WriteLine($"final HashMap> {errorMappingVarName} = new HashMap>({codeElement.ErrorMappings.Count}) {{{{"); + writer.WriteLine($"final HashMap> {errorMappingVarName} = new HashMap>({codeElement.ErrorMappings.Count}) {{{{"); writer.IncreaseIndent(); foreach(var errorMapping in codeElement.ErrorMappings) { writer.WriteLine($"put(\"{errorMapping.Key.ToUpperInvariant()}\", {errorMapping.Value.Name.ToFirstCharacterUpperCase()}.class);"); @@ -344,7 +344,10 @@ CodeMethodKind.Setter when (code.AccessedProperty?.IsNameEscaped ?? false) && !s _ => code.Name.ToFirstCharacterLowerCase() }; var parameters = string.Join(", ", code.Parameters.OrderBy(x => x, parameterOrderComparer).Select(p=> conventions.GetParameterSignature(p, code)).ToList()); - var throwableDeclarations = code.IsOfKind(CodeMethodKind.RequestGenerator) ? "throws URISyntaxException ": string.Empty; + var throwableDeclarations = code.MethodKind switch { + CodeMethodKind.RequestGenerator => "throws URISyntaxException ", + _ => string.Empty + }; var collectionCorrectedReturnType = code.ReturnType.IsArray && code.IsOfKind(CodeMethodKind.RequestExecutor) ? $"Iterable<{returnType.StripArraySuffix()}>" : returnType; diff --git a/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs index d762b64b91..c8a79df70a 100644 --- a/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Java/CodeMethodWriterTests.cs @@ -182,7 +182,7 @@ public void WritesRequestExecutorBody() { writer.Write(method); var result = tw.ToString(); Assert.Contains("final RequestInformation requestInfo", result); - Assert.Contains("final HashMap> errorMapping = new HashMap>", result); + Assert.Contains("final HashMap> errorMapping = new HashMap>", result); Assert.Contains("put(\"4XX\", Error4XX.class);", result); Assert.Contains("put(\"5XX\", Error5XX.class);", result); Assert.Contains("put(\"403\", Error403.class);", result); @@ -197,7 +197,7 @@ public void DoesntCreateDictionaryOnEmptyErrorMapping() { AddRequestBodyParameters(); writer.Write(method); var result = tw.ToString(); - Assert.DoesNotContain("final HashMap> errorMapping = new HashMap>", result); + Assert.DoesNotContain("final HashMap> errorMapping = new HashMap>", result); Assert.Contains("null", result); AssertExtensions.CurlyBracesAreClosed(result); } From 5ef93139190733f45a76358eba69ff8468f90cef Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 14:12:05 -0500 Subject: [PATCH 19/36] - adds intermediat error type for typescript - adds error mapping abstractions and implements deserialization of errors for typescript http Signed-off-by: Vincent Biret --- abstractions/typescript/src/apiError.ts | 11 +++ abstractions/typescript/src/index.ts | 1 + .../typescript/src/nativeResponseHandler.ts | 7 +- abstractions/typescript/src/requestAdapter.ts | 20 +++-- .../typescript/src/responseHandler.ts | 6 +- .../src/HttpClientRequestAdapter.cs | 4 +- .../fetch/src/fetchRequestAdapter.ts | 84 +++++++++++-------- 7 files changed, 89 insertions(+), 44 deletions(-) create mode 100644 abstractions/typescript/src/apiError.ts diff --git a/abstractions/typescript/src/apiError.ts b/abstractions/typescript/src/apiError.ts new file mode 100644 index 0000000000..d0389c2a09 --- /dev/null +++ b/abstractions/typescript/src/apiError.ts @@ -0,0 +1,11 @@ +/** Parent interface for errors thrown by the client when receiving failed responses to its requests. */ +interface ApiError extends Error { +} + +interface ApiErrorConstructor extends ErrorConstructor { + new(message?: string): ApiError; + (message?: string): ApiError; + readonly prototype: ApiError; +} + +export var ApiError: ApiErrorConstructor; \ No newline at end of file diff --git a/abstractions/typescript/src/index.ts b/abstractions/typescript/src/index.ts index 65f1372969..33e1f86925 100644 --- a/abstractions/typescript/src/index.ts +++ b/abstractions/typescript/src/index.ts @@ -1,4 +1,5 @@ export * from "./apiClientBuilder"; +export * from "./apiError"; export * from "./authentication"; export * from "./dateOnly"; export * from "./duration"; diff --git a/abstractions/typescript/src/nativeResponseHandler.ts b/abstractions/typescript/src/nativeResponseHandler.ts index dda482fe2f..631b044fd6 100644 --- a/abstractions/typescript/src/nativeResponseHandler.ts +++ b/abstractions/typescript/src/nativeResponseHandler.ts @@ -1,13 +1,18 @@ import { ResponseHandler } from "./responseHandler"; +import { Parsable } from "./serialization"; /** Default response handler to access the native response object. */ export class NativeResponseHandler implements ResponseHandler { /** Native response object as returned by the core service */ public value?: any; + /** The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. */ + public errorMappings: Record Parsable> | undefined public handleResponseAsync( - response: NativeResponseType + response: NativeResponseType, + errorMappings: Record Parsable> | undefined ): Promise { this.value = response; + this.errorMappings = errorMappings; return Promise.resolve(undefined as any); } } diff --git a/abstractions/typescript/src/requestAdapter.ts b/abstractions/typescript/src/requestAdapter.ts index f90a95eb1e..ec1e452c54 100644 --- a/abstractions/typescript/src/requestAdapter.ts +++ b/abstractions/typescript/src/requestAdapter.ts @@ -14,6 +14,7 @@ export interface RequestAdapter { * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param type the class of the response model to deserialize the response into. * @typeParam ModelType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized response model. @@ -21,12 +22,14 @@ export interface RequestAdapter { sendAsync( requestInfo: RequestInformation, type: new () => ModelType, - responseHandler: ResponseHandler | undefined + responseHandler: ResponseHandler | undefined, + errorMappings: Record Parsable> | undefined ): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param type the class of the response model to deserialize the response into. * @typeParam ModelType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized response model collection. @@ -34,13 +37,15 @@ export interface RequestAdapter { sendCollectionAsync( requestInfo: RequestInformation, type: new () => ModelType, - responseHandler: ResponseHandler | undefined + responseHandler: ResponseHandler | undefined, + errorMappings: Record Parsable> | undefined ): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. * @param requestInfo the request info to execute. * @param responseType the class of the response model to deserialize the response into. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param type the class of the response model to deserialize the response into. * @typeParam ResponseType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized response model collection. @@ -48,12 +53,14 @@ export interface RequestAdapter { sendCollectionOfPrimitiveAsync( requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", - responseHandler: ResponseHandler | undefined + responseHandler: ResponseHandler | undefined, + errorMappings: Record Parsable> | undefined ): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @param responseType the class of the response model to deserialize the response into. * @typeParam ResponseType the type of the response model to deserialize the response into. * @return a {@link Promise} with the deserialized primitive response model. @@ -61,17 +68,20 @@ export interface RequestAdapter { sendPrimitiveAsync( requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", - responseHandler: ResponseHandler | undefined + responseHandler: ResponseHandler | undefined, + errorMappings: Record Parsable> | undefined ): Promise; /** * Excutes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. * @param requestInfo the request info to execute. * @param responseHandler The response handler to use for the HTTP request instead of the default handler. + * @param errorMappings the error factories mapping to use in case of a failed request. * @return a {@link Promise} of void. */ sendNoResponseContentAsync( requestInfo: RequestInformation, - responseHandler: ResponseHandler | undefined + responseHandler: ResponseHandler | undefined, + errorMappings: Record Parsable> | undefined ): Promise; /** * Enables the backing store proxies for the SerializationWriters and ParseNodes in use. diff --git a/abstractions/typescript/src/responseHandler.ts b/abstractions/typescript/src/responseHandler.ts index 84ab703b86..4e39734f79 100644 --- a/abstractions/typescript/src/responseHandler.ts +++ b/abstractions/typescript/src/responseHandler.ts @@ -1,13 +1,17 @@ +import { Parsable } from "./serialization"; + /** Defines the contract for a response handler. */ export interface ResponseHandler { /** * Callback method that is invoked when a response is received. * @param response The native response object. + * @param errorMappings the error factories mapping to use in case of a failed request. * @typeParam NativeResponseType The type of the native response object. * @typeParam ModelType The type of the response model object. * @return A {@link Promise} that represents the asynchronous operation and contains the deserialized response. */ handleResponseAsync( - response: NativeResponseType + response: NativeResponseType, + errorMappings: Record Parsable> | undefined ): Promise; } diff --git a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs index f0d49fab1d..f648c90846 100644 --- a/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs +++ b/http/dotnet/httpclient/src/HttpClientRequestAdapter.cs @@ -205,12 +205,12 @@ private async Task ThrowFailedResponse(HttpResponseMessage response, Dictionary< !errorMapping.TryGetValue(statusCodeAsString, out errorFactory) && !(statusCodeAsInt >= 400 && statusCodeAsInt < 500 && errorMapping.TryGetValue("4XX", out errorFactory)) && !(statusCodeAsInt >= 500 && statusCodeAsInt < 600 && errorMapping.TryGetValue("5XX", out errorFactory))) - throw new HttpRequestException($"The server returned an unexpected status code and no error factory is registered for this code: {statusCodeAsString}"); + throw new ApiException($"The server returned an unexpected status code and no error factory is registered for this code: {statusCodeAsString}"); var rootNode = await GetRootParseNode(response); var result = rootNode.GetErrorValue(errorFactory); if(result is not Exception ex) - throw new HttpRequestException($"The server returned an unexpected status code and the error registered for this code failed to deserialize: {statusCodeAsString}"); + throw new ApiException($"The server returned an unexpected status code and the error registered for this code failed to deserialize: {statusCodeAsString}"); else throw ex; } private async Task GetRootParseNode(HttpResponseMessage response) diff --git a/http/typescript/fetch/src/fetchRequestAdapter.ts b/http/typescript/fetch/src/fetchRequestAdapter.ts index 1fb8ecec95..40f639affe 100644 --- a/http/typescript/fetch/src/fetchRequestAdapter.ts +++ b/http/typescript/fetch/src/fetchRequestAdapter.ts @@ -1,4 +1,4 @@ -import { AuthenticationProvider, BackingStoreFactory, BackingStoreFactorySingleton, RequestAdapter, Parsable, ParseNodeFactory, RequestInformation, ResponseHandler, ParseNodeFactoryRegistry, enableBackingStoreForParseNodeFactory, SerializationWriterFactoryRegistry, enableBackingStoreForSerializationWriterFactory, SerializationWriterFactory } from '@microsoft/kiota-abstractions'; +import { ApiError, AuthenticationProvider, BackingStoreFactory, BackingStoreFactorySingleton, RequestAdapter, Parsable, ParseNodeFactory, RequestInformation, ResponseHandler, ParseNodeFactoryRegistry, enableBackingStoreForParseNodeFactory, SerializationWriterFactoryRegistry, enableBackingStoreForSerializationWriterFactory, SerializationWriterFactory, ParseNode } from '@microsoft/kiota-abstractions'; import { HttpClient } from './httpClient'; export class FetchRequestAdapter implements RequestAdapter { @@ -35,25 +35,21 @@ export class FetchRequestAdapter implements RequestAdapter { if (segments.length === 0) return undefined; else return segments[0]; } - public sendCollectionOfPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", responseHandler: ResponseHandler | undefined): Promise => { + public sendCollectionOfPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date", responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { + await this.throwFailedResponses(response, errorMappings); switch (responseType) { case 'string': case 'number': case 'boolean': case 'Date': - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + const rootNode = await this.getRootParseNode(response); if (responseType === 'string') { return rootNode.getCollectionOfPrimitiveValues() as unknown as ResponseType[]; } else if (responseType === 'number') { @@ -68,50 +64,43 @@ export class FetchRequestAdapter implements RequestAdapter { } } } - public sendCollectionAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined): Promise => { + public sendCollectionAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + await this.throwFailedResponses(response, errorMappings); + const rootNode = await this.getRootParseNode(response); const result = rootNode.getCollectionOfObjectValues(type); return result as unknown as ModelType[]; } } - public sendAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined): Promise => { + public sendAsync = async (requestInfo: RequestInformation, type: new () => ModelType, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + await this.throwFailedResponses(response, errorMappings); + const rootNode = await this.getRootParseNode(response); const result = rootNode.getObjectValue(type); return result as unknown as ModelType; } } - public sendPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", responseHandler: ResponseHandler | undefined): Promise => { + public sendPrimitiveAsync = async (requestInfo: RequestInformation, responseType: "string" | "number" | "boolean" | "Date" | "ArrayBuffer", responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } else { + await this.throwFailedResponses(response, errorMappings); switch (responseType) { case "ArrayBuffer": return await response.arrayBuffer() as unknown as ResponseType; @@ -119,12 +108,7 @@ export class FetchRequestAdapter implements RequestAdapter { case 'number': case 'boolean': case 'Date': - const payload = await response.arrayBuffer(); - const responseContentType = this.getResponseContentType(response); - if (!responseContentType) - throw new Error("no response content type found for deserialization"); - - const rootNode = this.parseNodeFactory.getRootParseNode(responseContentType, payload); + const rootNode = await this.getRootParseNode(response); if (responseType === 'string') { return rootNode.getStringValue() as unknown as ResponseType; } else if (responseType === 'number') { @@ -139,14 +123,15 @@ export class FetchRequestAdapter implements RequestAdapter { } } } - public sendNoResponseContentAsync = async (requestInfo: RequestInformation, responseHandler: ResponseHandler | undefined): Promise => { + public sendNoResponseContentAsync = async (requestInfo: RequestInformation, responseHandler: ResponseHandler | undefined, errorMappings: Record Parsable> | undefined): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); } const response = await this.getHttpResponseMessage(requestInfo); if (responseHandler) { - return await responseHandler.handleResponseAsync(response); + return await responseHandler.handleResponseAsync(response, errorMappings); } + await this.throwFailedResponses(response, errorMappings); } public enableBackingStore = (backingStoreFactory?: BackingStoreFactory | undefined): void => { this.parseNodeFactory = enableBackingStoreForParseNodeFactory(this.parseNodeFactory); @@ -157,6 +142,35 @@ export class FetchRequestAdapter implements RequestAdapter { BackingStoreFactorySingleton.instance = backingStoreFactory; } } + private getRootParseNode = async (response: Response) : Promise => { + const payload = await response.arrayBuffer(); + const responseContentType = this.getResponseContentType(response); + if (!responseContentType) + throw new Error("no response content type found for deserialization"); + + return this.parseNodeFactory.getRootParseNode(responseContentType, payload); + } + private throwFailedResponses = async (response: Response, errorMappings: Record Parsable> | undefined): Promise => { + if(response.ok) return; + + const statusCode = response.status; + const statusCodeAsString = statusCode.toString(); + if(!errorMappings || + !errorMappings[statusCodeAsString] && + !(statusCode >= 400 && statusCode < 500 && errorMappings['4XX']) && + !(statusCode >= 500 && statusCode < 600 && errorMappings['5XX'])) + throw new ApiError("the server returned an unexpected status code and no error class is registered for this code " + statusCode); + + const factory = errorMappings[statusCodeAsString] ?? + (statusCode >= 400 && statusCode < 500 ? errorMappings['4XX'] : undefined) ?? + (statusCode >= 500 && statusCode < 600 ? errorMappings['5XX'] : undefined); + + const rootNode = await this.getRootParseNode(response); + const error = rootNode.getObjectValue(factory); + + if(error instanceof Error) throw error; + else throw new ApiError("unexpected error type" + typeof(error)) + } private getHttpResponseMessage = async (requestInfo: RequestInformation): Promise => { if (!requestInfo) { throw new Error('requestInfo cannot be null'); From 90c63b848303c5daaeefb628d8985c5c276187a1 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 15:03:31 -0500 Subject: [PATCH 20/36] - implements error mapping in typescript code-generation Signed-off-by: Vincent Biret --- abstractions/typescript/src/apiError.ts | 18 +++--- .../Refiners/CommonLanguageRefiner.cs | 10 ++- .../Refiners/TypeScriptRefiner.cs | 5 ++ .../TypeScriptReservedNamesProvider.cs | 3 +- .../Writers/TypeScript/CodeMethodWriter.cs | 21 +++++-- .../TypeScriptLanguageRefinerTests.cs | 62 +++++++++++++++++++ .../TypeScript/CodeMethodWriterTests.cs | 32 +++++++++- 7 files changed, 132 insertions(+), 19 deletions(-) diff --git a/abstractions/typescript/src/apiError.ts b/abstractions/typescript/src/apiError.ts index d0389c2a09..dfc158f35f 100644 --- a/abstractions/typescript/src/apiError.ts +++ b/abstractions/typescript/src/apiError.ts @@ -1,11 +1,9 @@ /** Parent interface for errors thrown by the client when receiving failed responses to its requests. */ -interface ApiError extends Error { -} - -interface ApiErrorConstructor extends ErrorConstructor { - new(message?: string): ApiError; - (message?: string): ApiError; - readonly prototype: ApiError; -} - -export var ApiError: ApiErrorConstructor; \ No newline at end of file +export class ApiError implements Error { + public name: string; + public message: string; + public stack?: string; + public constructor(message?: string) { + this.message = message || ""; + } +} \ No newline at end of file diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index 0427cd004d..5e139dc92d 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -180,7 +180,9 @@ protected static void ReplaceReservedNames(CodeElement current, IReservedNamesPr !returnType.IsExternal && provider.ReservedNames.Contains(returnType.Name)) returnType.Name = replacement.Invoke(returnType.Name); - ReplaceReservedParameterNamesTypes(currentMethod, provider, replacement); + ReplaceReservedParameterNamesTypes(currentMethod, provider, replacement); + if(currentMethod.ErrorMappings.Values.Select(x => x.Name).Any(x => provider.ReservedNames.Contains(x))) + ReplaceErrorMappingNames(currentMethod, provider, replacement); } else if (current is CodeProperty currentProperty && isNotInExceptions && shouldReplace && @@ -225,6 +227,12 @@ private static void ReplaceReservedNamespaceSegments(CodeNamespace currentNamesp x) .Aggregate((x, y) => $"{x}.{y}"); } + private static void ReplaceErrorMappingNames(CodeMethod currentMethod, IReservedNamesProvider provider, Func replacement) + { + currentMethod.ErrorMappings.Values.Where(x => provider.ReservedNames.Contains(x.Name)) + .ToList() + .ForEach(x => x.Name = replacement.Invoke(x.Name)); + } private static void ReplaceReservedParameterNamesTypes(CodeMethod currentMethod, IReservedNamesProvider provider, Func replacement) { currentMethod.Parameters.Where(x => x.Type is CodeType parameterType && diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs index 4666bff1b9..6f5cca3b31 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs @@ -32,6 +32,11 @@ public override void Refine(CodeNamespace generatedCode) $"{AbstractionsPackageName}.SerializationWriterFactoryRegistry"}, new[] { $"{AbstractionsPackageName}.registerDefaultDeserializer", $"{AbstractionsPackageName}.ParseNodeFactoryRegistry" }); + AddParentClassToErrorClasses( + generatedCode, + "ApiError", + "@microsoft/kiota-abstractions" + ); } private static readonly CodeUsingDeclarationNameComparer usingComparer = new(); private static void AliasUsingsWithSameSymbol(CodeElement currentElement) { diff --git a/src/Kiota.Builder/Refiners/TypeScriptReservedNamesProvider.cs b/src/Kiota.Builder/Refiners/TypeScriptReservedNamesProvider.cs index ffa751a636..a0e7348bd3 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptReservedNamesProvider.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptReservedNamesProvider.cs @@ -15,6 +15,7 @@ public class TypeScriptReservedNamesProvider : IReservedNamesProvider { "do", "else", "enum", + "error", "export", "extends", "false", @@ -24,7 +25,7 @@ public class TypeScriptReservedNamesProvider : IReservedNamesProvider { "If", "import", "in", - "istanceOf", + "instanceOf", "new", "null", "return", diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs index c82b588967..831bc48091 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs @@ -23,7 +23,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri WriteMethodPrototype(codeElement, writer, returnType, isVoid); writer.IncreaseIndent(); var parentClass = codeElement.Parent as CodeClass; - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + var inherits = parentClass.StartBlock is CodeClass.Declaration declaration && declaration.Inherits != null && !parentClass.IsErrorDefinition; var requestBodyParam = codeElement.Parameters.OfKind(CodeParameterKind.RequestBody); var queryStringParam = codeElement.Parameters.OfKind(CodeParameterKind.QueryParameter); var headersParam = codeElement.Parameters.OfKind(CodeParameterKind.Headers); @@ -39,7 +39,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri WriteIndexerBody(codeElement, parentClass, returnType, writer); break; case CodeMethodKind.Deserializer: - WriteDeserializerBody(codeElement, parentClass, writer); + WriteDeserializerBody(codeElement, parentClass, writer, inherits); break; case CodeMethodKind.Serializer: WriteSerializerBody(inherits, parentClass, writer); @@ -105,7 +105,7 @@ private static void WriteSerializationRegistration(List serializationMod writer.WriteLine($"{methodName}({module});"); } private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer, bool inherits) { - if(inherits) + if(inherits || parentClass.IsErrorDefinition) writer.WriteLine("super();"); var propertiesWithDefaultValues = new List { CodePropertyKind.AdditionalData, @@ -176,8 +176,7 @@ private static void WriteDefaultMethodBody(CodeMethod codeElement, LanguageWrite var promiseSuffix = codeElement.IsAsync ? ")" : string.Empty; writer.WriteLine($"return {promisePrefix}{(codeElement.ReturnType.Name.Equals("string") ? "''" : "{} as any")}{promiseSuffix};"); } - private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer, bool inherits) { writer.WriteLine($"return new Map void>([{(inherits ? $"...super.{codeElement.Name.ToFirstCharacterLowerCase()}()," : string.Empty)}"); writer.IncreaseIndent(); var parentClassName = parentClass.Name.ToFirstCharacterUpperCase(); @@ -208,7 +207,17 @@ private void WriteRequestExecutorBody(CodeMethod codeElement, RequestParams requ var returnTypeWithoutCollectionSymbol = GetReturnTypeWithoutCollectionSymbol(codeElement, returnType); var genericTypeForSendMethod = GetSendRequestMethodName(isVoid, isStream, codeElement.ReturnType.IsCollection, returnTypeWithoutCollectionSymbol); var newFactoryParameter = GetTypeFactory(isVoid, isStream, returnTypeWithoutCollectionSymbol); - writer.WriteLine($"return this.requestAdapter?.{genericTypeForSendMethod}(requestInfo,{newFactoryParameter} responseHandler) ?? Promise.reject(new Error('http core is null'));"); + var errorMappingVarName = "undefined"; + if(codeElement.ErrorMappings.Any()) { + errorMappingVarName = "errorMapping"; + writer.WriteLine($"const {errorMappingVarName}: Record Parsable> = {{"); + writer.IncreaseIndent(); + foreach(var errorMapping in codeElement.ErrorMappings) { + writer.WriteLine($"\"{errorMapping.Key.ToUpperInvariant()}\": {errorMapping.Value.Name.ToFirstCharacterUpperCase()},"); + } + writer.CloseBlock("};"); + } + writer.WriteLine($"return this.requestAdapter?.{genericTypeForSendMethod}(requestInfo,{newFactoryParameter} responseHandler, {errorMappingVarName}) ?? Promise.reject(new Error('http core is null'));"); } private string GetReturnTypeWithoutCollectionSymbol(CodeMethod codeElement, string fullTypeName) { if(!codeElement.ReturnType.IsCollection) return fullTypeName; diff --git a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs index 371982f997..d212b768b7 100644 --- a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Xunit; @@ -14,6 +15,67 @@ public TypeScriptLanguageRefinerTests() { }; graphNS.AddClass(parentClass); } +#region commonrefiner + [Fact] + public void AddsExceptionInheritanceOnErrorClasses() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.TypeScript }, root); + + var declaration = model.StartBlock as CodeClass.Declaration; + + Assert.Contains("ApiError", declaration.Usings.Select(x => x.Name)); + Assert.Equal("ApiError", declaration.Inherits.Name); + } + [Fact] + public void FailsExceptionInheritanceOnErrorClassesWhichAlreadyInherit() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var declaration = model.StartBlock as CodeClass.Declaration; + declaration.Inherits = new CodeType { + Name = "SomeOtherModel" + }; + Assert.Throws(() => ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.TypeScript }, root)); + } + [Fact] + public void AddsUsingsForErrorTypesForRequestExecutor() { + var requestBuilder = root.AddClass(new CodeClass { + Name = "somerequestbuilder", + ClassKind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = root.AddNamespace($"{root.Name}.subns"); // otherwise the import gets trimmed + var errorClass = subNS.AddClass(new CodeClass { + Name = "Error4XX", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var requestExecutor = requestBuilder.AddMethod(new CodeMethod { + Name = "get", + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string" + }, + ErrorMappings = new () { + { "4XX", new CodeType { + Name = "Error4XX", + TypeDefinition = errorClass, + } + }, + }, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.TypeScript }, root); + + var declaration = requestBuilder.StartBlock as CodeClass.Declaration; + + Assert.Contains("Error4XX", declaration.Usings.Select(x => x.Declaration?.Name)); + } +#endregion #region typescript private const string HttpCoreDefaultName = "IRequestAdapter"; private const string FactoryDefaultName = "ISerializationWriterFactory"; diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeMethodWriterTests.cs index aa4a57766a..161034efbc 100644 --- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeMethodWriterTests.cs @@ -12,6 +12,7 @@ public class CodeMethodWriterTests : IDisposable { private readonly LanguageWriter writer; private readonly CodeMethod method; private readonly CodeClass parentClass; + private readonly CodeNamespace root; private const string MethodName = "methodName"; private const string ReturnTypeName = "Somecustomtype"; private const string MethodDescription = "some description"; @@ -22,7 +23,7 @@ public CodeMethodWriterTests() writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName); tw = new StringWriter(); writer.SetTextWriter(tw); - var root = CodeNamespace.InitRootNamespace(); + root = CodeNamespace.InitRootNamespace(); parentClass = new CodeClass { Name = "parentClass" }; @@ -149,15 +150,44 @@ public void WritesRequestBodiesThrowOnNullHttpMethod() { public void WritesRequestExecutorBody() { method.MethodKind = CodeMethodKind.RequestExecutor; method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass{ + Name = "Error4XX", + }).First(); + var error5XX = root.AddClass(new CodeClass{ + Name = "Error5XX", + }).First(); + var error401 = root.AddClass(new CodeClass{ + Name = "Error401", + }).First(); + method.ErrorMappings = new () { + {"4XX", new CodeType {Name = "Error4XX", TypeDefinition = error4XX}}, + {"5XX", new CodeType {Name = "Error5XX", TypeDefinition = error5XX}}, + {"403", new CodeType {Name = "Error403", TypeDefinition = error401}}, + }; AddRequestBodyParameters(); writer.Write(method); var result = tw.ToString(); Assert.Contains("const requestInfo", result); + Assert.Contains("const errorMapping: Record Parsable> =", result); + Assert.Contains("\"4XX\": Error4XX,", result); + Assert.Contains("\"5XX\": Error5XX,", result); + Assert.Contains("\"403\": Error403,", result); Assert.Contains("sendAsync", result); Assert.Contains("Promise.reject", result); AssertExtensions.CurlyBracesAreClosed(result); } [Fact] + public void DoesntCreateDictionaryOnEmptyErrorMapping() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("const errorMapping: Record Parsable> =", result); + Assert.Contains("undefined", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] public void WritesRequestExecutorBodyForCollections() { method.MethodKind = CodeMethodKind.RequestExecutor; method.HttpMethod = HttpMethod.Get; From 3798eb5c17fc2ebd52c1d2e824a48aef00a65b9b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 4 Feb 2022 15:42:01 -0500 Subject: [PATCH 21/36] - fixes error throw in typescript request adapter Signed-off-by: Vincent Biret --- abstractions/typescript/src/apiError.ts | 1 + http/typescript/fetch/src/fetchRequestAdapter.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/abstractions/typescript/src/apiError.ts b/abstractions/typescript/src/apiError.ts index dfc158f35f..5a990f1c83 100644 --- a/abstractions/typescript/src/apiError.ts +++ b/abstractions/typescript/src/apiError.ts @@ -5,5 +5,6 @@ export class ApiError implements Error { public stack?: string; public constructor(message?: string) { this.message = message || ""; + this.name = "ApiError"; } } \ No newline at end of file diff --git a/http/typescript/fetch/src/fetchRequestAdapter.ts b/http/typescript/fetch/src/fetchRequestAdapter.ts index 40f639affe..9daabcc2fa 100644 --- a/http/typescript/fetch/src/fetchRequestAdapter.ts +++ b/http/typescript/fetch/src/fetchRequestAdapter.ts @@ -168,7 +168,7 @@ export class FetchRequestAdapter implements RequestAdapter { const rootNode = await this.getRootParseNode(response); const error = rootNode.getObjectValue(factory); - if(error instanceof Error) throw error; + if(error) throw error; else throw new ApiError("unexpected error type" + typeof(error)) } private getHttpResponseMessage = async (requestInfo: RequestInformation): Promise => { From dbd72ec42d78c620856e2c5001bff73265bf5b6a Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 10:22:53 -0500 Subject: [PATCH 22/36] - adds error mapping to go request adapter - adds errors handler to go request adapter - adds api error type to go abstactions Signed-off-by: Vincent Biret --- abstractions/go/api_error.go | 16 +++++ abstractions/go/request_adapter.go | 16 +++-- abstractions/go/response_handler.go | 2 +- http/go/nethttp/nethttp_request_adapter.go | 70 ++++++++++++++++------ 4 files changed, 80 insertions(+), 24 deletions(-) create mode 100644 abstractions/go/api_error.go diff --git a/abstractions/go/api_error.go b/abstractions/go/api_error.go new file mode 100644 index 0000000000..3e2ace3a9d --- /dev/null +++ b/abstractions/go/api_error.go @@ -0,0 +1,16 @@ +package abstractions + +import "fmt" + +// ApiError is the parent type for errors thrown by the client when receiving failed responses to its requests +type ApiError struct { + Message string +} + +func (e *ApiError) Error() string { + if len(e.Message) > 0 { + return fmt.Sprint(e.Message) + } else { + return "error status code received from the API" + } +} diff --git a/abstractions/go/request_adapter.go b/abstractions/go/request_adapter.go index 4891c81033..6a57c58494 100644 --- a/abstractions/go/request_adapter.go +++ b/abstractions/go/request_adapter.go @@ -12,18 +12,24 @@ import ( s "github.com/microsoft/kiota/abstractions/go/serialization" ) +// ParsableFactory is a factory for creating Parsable. +type ParsableFactory func() s.Parsable + +// ErrorMappings is a mapping of status codes to error types factories. +type ErrorMappings map[string]ParsableFactory + // RequestAdapter is the service responsible for translating abstract RequestInformation into native HTTP requests. type RequestAdapter interface { // SendAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model. - SendAsync(requestInfo RequestInformation, constructor func() s.Parsable, responseHandler ResponseHandler) (s.Parsable, error) + SendAsync(requestInfo RequestInformation, constructor ParsableFactory, responseHandler ResponseHandler, errorMappings ErrorMappings) (s.Parsable, error) // SendCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. - SendCollectionAsync(requestInfo RequestInformation, constructor func() s.Parsable, responseHandler ResponseHandler) ([]s.Parsable, error) + SendCollectionAsync(requestInfo RequestInformation, constructor ParsableFactory, responseHandler ResponseHandler, errorMappings ErrorMappings) ([]s.Parsable, error) // SendPrimitiveAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. - SendPrimitiveAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler) (interface{}, error) + SendPrimitiveAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler, errorMappings ErrorMappings) (interface{}, error) // SendPrimitiveCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. - SendPrimitiveCollectionAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler) ([]interface{}, error) + SendPrimitiveCollectionAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler, errorMappings ErrorMappings) ([]interface{}, error) // SendNoContentAsync executes the HTTP request specified by the given RequestInformation with no return content. - SendNoContentAsync(requestInfo RequestInformation, responseHandler ResponseHandler) error + SendNoContentAsync(requestInfo RequestInformation, responseHandler ResponseHandler, errorMappings ErrorMappings) error // GetSerializationWriterFactory returns the serialization writer factory currently in use for the request adapter service. GetSerializationWriterFactory() s.SerializationWriterFactory // EnableBackingStore enables the backing store proxies for the SerializationWriters and ParseNodes in use. diff --git a/abstractions/go/response_handler.go b/abstractions/go/response_handler.go index 7623213ced..7c729219ba 100644 --- a/abstractions/go/response_handler.go +++ b/abstractions/go/response_handler.go @@ -1,4 +1,4 @@ package abstractions // ResponseHandler handler to implement when a request's response should be handled a specific way. -type ResponseHandler func(response interface{}) (interface{}, error) +type ResponseHandler func(response interface{}, errorMappings ErrorMappings) (interface{}, error) diff --git a/http/go/nethttp/nethttp_request_adapter.go b/http/go/nethttp/nethttp_request_adapter.go index 1936e8a7a1..416088bea8 100644 --- a/http/go/nethttp/nethttp_request_adapter.go +++ b/http/go/nethttp/nethttp_request_adapter.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "io/ioutil" + "strconv" "strings" ctx "context" @@ -138,23 +139,23 @@ func (a *NetHttpRequestAdapter) getRequestFromRequestInformation(requestInfo abs } // SendAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model. -func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, constructor func() absser.Parsable, responseHandler abs.ResponseHandler) (absser.Parsable, error) { +func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, constructor abs.ParsableFactory, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) (absser.Parsable, error) { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return nil, err } if responseHandler != nil { - result, err := responseHandler(response) + result, err := responseHandler(response, errorMappings) if err != nil { return nil, err } return result.(absser.Parsable), nil } else if response != nil { - body, err := ioutil.ReadAll(response.Body) + err = a.throwFailedResponses(response, errorMappings) if err != nil { return nil, err } - parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + parseNode, err := a.getRootParseNode(response) if err != nil { return nil, err } @@ -166,23 +167,23 @@ func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, co } // SendCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. -func (a *NetHttpRequestAdapter) SendCollectionAsync(requestInfo abs.RequestInformation, constructor func() absser.Parsable, responseHandler abs.ResponseHandler) ([]absser.Parsable, error) { +func (a *NetHttpRequestAdapter) SendCollectionAsync(requestInfo abs.RequestInformation, constructor abs.ParsableFactory, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) ([]absser.Parsable, error) { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return nil, err } if responseHandler != nil { - result, err := responseHandler(response) + result, err := responseHandler(response, errorMappings) if err != nil { return nil, err } return result.([]absser.Parsable), nil } else if response != nil { - body, err := ioutil.ReadAll(response.Body) + err = a.throwFailedResponses(response, errorMappings) if err != nil { return nil, err } - parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + parseNode, err := a.getRootParseNode(response) if err != nil { return nil, err } @@ -194,23 +195,26 @@ func (a *NetHttpRequestAdapter) SendCollectionAsync(requestInfo abs.RequestInfor } // SendPrimitiveAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. -func (a *NetHttpRequestAdapter) SendPrimitiveAsync(requestInfo abs.RequestInformation, typeName string, responseHandler abs.ResponseHandler) (interface{}, error) { +func (a *NetHttpRequestAdapter) SendPrimitiveAsync(requestInfo abs.RequestInformation, typeName string, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) (interface{}, error) { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return nil, err } if responseHandler != nil { - result, err := responseHandler(response) + result, err := responseHandler(response, errorMappings) if err != nil { return nil, err } return result.(absser.Parsable), nil } else if response != nil { - body, err := ioutil.ReadAll(response.Body) + err = a.throwFailedResponses(response, errorMappings) if err != nil { return nil, err } - parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + if typeName == "[]byte" { + return ioutil.ReadAll(response.Body) + } + parseNode, err := a.getRootParseNode(response) if err != nil { return nil, err } @@ -240,23 +244,23 @@ func (a *NetHttpRequestAdapter) SendPrimitiveAsync(requestInfo abs.RequestInform } // SendPrimitiveCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. -func (a *NetHttpRequestAdapter) SendPrimitiveCollectionAsync(requestInfo abs.RequestInformation, typeName string, responseHandler abs.ResponseHandler) ([]interface{}, error) { +func (a *NetHttpRequestAdapter) SendPrimitiveCollectionAsync(requestInfo abs.RequestInformation, typeName string, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) ([]interface{}, error) { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return nil, err } if responseHandler != nil { - result, err := responseHandler(response) + result, err := responseHandler(response, errorMappings) if err != nil { return nil, err } return result.([]interface{}), nil } else if response != nil { - body, err := ioutil.ReadAll(response.Body) + err = a.throwFailedResponses(response, errorMappings) if err != nil { return nil, err } - parseNode, err := a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) + parseNode, err := a.getRootParseNode(response) if err != nil { return nil, err } @@ -267,13 +271,13 @@ func (a *NetHttpRequestAdapter) SendPrimitiveCollectionAsync(requestInfo abs.Req } // SendNoContentAsync executes the HTTP request specified by the given RequestInformation with no return content. -func (a *NetHttpRequestAdapter) SendNoContentAsync(requestInfo abs.RequestInformation, responseHandler abs.ResponseHandler) error { +func (a *NetHttpRequestAdapter) SendNoContentAsync(requestInfo abs.RequestInformation, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) error { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return err } if responseHandler != nil { - _, err := responseHandler(response) + _, err := responseHandler(response, errorMappings) return err } else if response != nil { return nil @@ -281,3 +285,33 @@ func (a *NetHttpRequestAdapter) SendNoContentAsync(requestInfo abs.RequestInform return errors.New("response is nil") } } + +func (a *NetHttpRequestAdapter) getRootParseNode(response *nethttp.Response) (absser.ParseNode, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + return a.parseNodeFactory.GetRootParseNode(a.getResponsePrimaryContentType(response), body) +} + +func (a *NetHttpRequestAdapter) throwFailedResponses(response *nethttp.Response, errorMappings abs.ErrorMappings) error { + if response.StatusCode < 400 { + return nil + } + + statusAsString := strconv.Itoa(response.StatusCode) + if len(errorMappings) != 0 { + if errorMappings[statusAsString] != nil { + return (errorMappings[statusAsString]()).(error) + } else if response.StatusCode >= 400 && response.StatusCode < 500 && errorMappings["4XX"] != nil { + return (errorMappings["4XX"]()).(error) + } else if response.StatusCode >= 500 && response.StatusCode < 600 && errorMappings["5XX"] != nil { + return (errorMappings["5XX"]()).(error) + } + } + + return &abs.ApiError{ + Message: "The server returned an unexpected status code and no error factory is registered for this code: " + statusAsString, + } + +} From 5cce31924c27a9408ea09e71e34f823b7a7b187b Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 10:38:52 -0500 Subject: [PATCH 23/36] - adds custom error type inheritance in go refiner Signed-off-by: Vincent Biret --- src/Kiota.Builder/Refiners/GoRefiner.cs | 5 ++ .../Refiners/GoLanguageRefinerTests.cs | 68 ++++++++++++++++++- 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Refiners/GoRefiner.cs b/src/Kiota.Builder/Refiners/GoRefiner.cs index 630450e96f..9a902fa629 100644 --- a/src/Kiota.Builder/Refiners/GoRefiner.cs +++ b/src/Kiota.Builder/Refiners/GoRefiner.cs @@ -87,6 +87,11 @@ public override void Refine(CodeNamespace generatedCode) new string[] {"github.com/microsoft/kiota/abstractions/go/serialization.ParseNodeFactory", "github.com/microsoft/kiota/abstractions/go.RegisterDefaultDeserializer"}); ReplaceExecutorAndGeneratorParametersByParameterSets( generatedCode); + AddParentClassToErrorClasses( + generatedCode, + "ApiError", + "github.com/microsoft/kiota/serialization" + ); } private static void ReplaceExecutorAndGeneratorParametersByParameterSets(CodeElement currentElement) { if (currentElement is CodeMethod currentMethod && diff --git a/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs index 39c7f8b73c..dcd7e00109 100644 --- a/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/GoLanguageRefinerTests.cs @@ -1,10 +1,76 @@ -using System.Linq; +using System; +using System.Linq; using Xunit; namespace Kiota.Builder.Refiners.Tests; public class GoLanguageRefinerTests { private readonly CodeNamespace root = CodeNamespace.InitRootNamespace(); #region CommonLangRefinerTests + [Fact] + public void AddsExceptionInheritanceOnErrorClasses() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + + var declaration = model.StartBlock as CodeClass.Declaration; + + Assert.Contains("ApiError", declaration.Usings.Select(x => x.Name)); + Assert.Equal("ApiError", declaration.Inherits.Name); + } + [Fact] + public void FailsExceptionInheritanceOnErrorClassesWhichAlreadyInherit() { + var model = root.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var declaration = model.StartBlock as CodeClass.Declaration; + declaration.Inherits = new CodeType { + Name = "SomeOtherModel" + }; + Assert.Throws(() => ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Go }, root)); + } + [Fact] + public void AddsUsingsForErrorTypesForRequestExecutor() { + var main = root.AddNamespace("main"); + var models = main.AddNamespace($"{main.Name}.models"); + models.AddClass(new CodeClass { + Name = "somemodel", + ClassKind = CodeClassKind.Model, + }); // so move to models namespace finds the models namespace + var requestBuilder = main.AddClass(new CodeClass { + Name = "somerequestbuilder", + ClassKind = CodeClassKind.RequestBuilder, + }).First(); + var subNS = models.AddNamespace($"{models.Name}.subns"); // otherwise the import gets trimmed + var errorClass = subNS.AddClass(new CodeClass { + Name = "Error4XX", + ClassKind = CodeClassKind.Model, + IsErrorDefinition = true, + }).First(); + var requestExecutor = requestBuilder.AddMethod(new CodeMethod { + Name = "get", + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string" + }, + ErrorMappings = new () { + { "4XX", new CodeType { + Name = "Error4XX", + TypeDefinition = errorClass, + } + }, + }, + }).First(); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + + var declaration = requestBuilder.StartBlock as CodeClass.Declaration; + + Assert.Contains("Error4XX", declaration.Usings.Select(x => x.Declaration?.Name)); + } [Fact] public void DoesNotKeepCancellationParametersInRequestExecutors() { From 90510cb4086c8e4e0e21682bc0b2b321fd082f17 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 12:50:26 -0500 Subject: [PATCH 24/36] - adds missing parsable refrence to executor methods Signed-off-by: Vincent Biret --- src/Kiota.Builder/Refiners/GoRefiner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Refiners/GoRefiner.cs b/src/Kiota.Builder/Refiners/GoRefiner.cs index 9a902fa629..e9203fd92d 100644 --- a/src/Kiota.Builder/Refiners/GoRefiner.cs +++ b/src/Kiota.Builder/Refiners/GoRefiner.cs @@ -90,7 +90,7 @@ public override void Refine(CodeNamespace generatedCode) AddParentClassToErrorClasses( generatedCode, "ApiError", - "github.com/microsoft/kiota/serialization" + "github.com/microsoft/kiota/abstractions/go" ); } private static void ReplaceExecutorAndGeneratorParametersByParameterSets(CodeElement currentElement) { @@ -262,6 +262,8 @@ private static void AddErrorImportForEnums(CodeElement currentElement) { "github.com/microsoft/kiota/abstractions/go/serialization", "SerializationWriter"), new (x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.Deserializer), "github.com/microsoft/kiota/abstractions/go/serialization", "ParseNode", "Parsable"), + new (x => x is CodeMethod method && method.IsOfKind(CodeMethodKind.RequestExecutor) && method.ErrorMappings.Any(), + "github.com/microsoft/kiota/abstractions/go/serialization", "Parsable"), new (x => x is CodeEnum num, "ToUpper", "strings"), };//TODO add backing store types once we have them defined private static void CorrectMethodType(CodeMethod currentMethod) { From 646690e423cad4fe7263636feb92c4eba60dc172 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 12:50:46 -0500 Subject: [PATCH 25/36] - fixes inheritance declarion for Go Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs b/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs index 064b0df0b0..c28b4e4029 100644 --- a/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeClassDeclarationWriter.cs @@ -33,11 +33,12 @@ public override void WriteCodeElement(CodeClass.Declaration codeElement, Languag writer.WriteLines(")", string.Empty); } var className = codeElement.Name.ToFirstCharacterUpperCase(); - conventions.WriteShortDescription($"{className} {(codeElement.Parent as CodeClass).Description.ToFirstCharacterLowerCase()}", writer); + var currentClass = codeElement.Parent as CodeClass; + conventions.WriteShortDescription($"{className} {currentClass.Description.ToFirstCharacterLowerCase()}", writer); writer.WriteLine($"type {className} struct {{"); writer.IncreaseIndent(); if(codeElement.Inherits?.AllTypes?.Any() ?? false) { - var parentTypeName = conventions.GetTypeString(codeElement.Inherits.AllTypes.First(), codeElement.Parent.Parent, true, false); + var parentTypeName = conventions.GetTypeString(codeElement.Inherits.AllTypes.First(), currentClass, true, false); writer.WriteLine($"{parentTypeName}"); } } From 49c7ff012cf768f8225045ec3db6762235b13393 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 12:51:25 -0500 Subject: [PATCH 26/36] - adds error mapping generation for Go Signed-off-by: Vincent Biret --- .../Writers/Go/CodeMethodWriter.cs | 54 +- .../Writers/Go/GoConventionService.cs | 6 + .../Writers/Go/CodeMethodWriterTests.cs | 1251 +++++++++-------- 3 files changed, 673 insertions(+), 638 deletions(-) diff --git a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs index 8c45066b85..fe9410eb3d 100644 --- a/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/Go/CodeMethodWriter.cs @@ -16,7 +16,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri if(!(codeElement.Parent is CodeClass)) throw new InvalidOperationException("the parent of a method should be a class"); var parentClass = codeElement.Parent as CodeClass; - var inherits = (parentClass.StartBlock as CodeClass.Declaration).Inherits != null; + var inherits = parentClass.StartBlock is CodeClass.Declaration declaration && declaration.Inherits != null && !parentClass.IsErrorDefinition; var returnType = conventions.GetTypeString(codeElement.ReturnType, parentClass); WriteMethodPrototype(codeElement, writer, returnType, parentClass); writer.IncreaseIndent(); @@ -30,10 +30,10 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri var requestParams = new RequestProperties(requestOptionsParam, requestBodyParam, queryStringParam, headersParam, optionsParam); switch(codeElement.MethodKind) { case CodeMethodKind.Serializer: - WriteSerializerBody(parentClass, writer); + WriteSerializerBody(parentClass, writer, inherits); break; case CodeMethodKind.Deserializer: - WriteDeserializerBody(codeElement, parentClass, writer); + WriteDeserializerBody(codeElement, parentClass, writer, inherits); break; case CodeMethodKind.IndexerBackwardCompatibility: WriteIndexerBody(codeElement, parentClass, writer, returnType); @@ -101,14 +101,13 @@ private void WriteRequestBuilderBody(CodeClass parentClass, CodeMethod codeEleme var importSymbol = conventions.GetTypeString(codeElement.ReturnType, parentClass); conventions.AddRequestBuilderBody(parentClass, importSymbol, writer, pathParameters: codeElement.Parameters.Where(x => x.IsOfKind(CodeParameterKind.Path))); } - private void WriteSerializerBody(CodeClass parentClass, LanguageWriter writer) { + private void WriteSerializerBody(CodeClass parentClass, LanguageWriter writer, bool inherits) { var additionalDataProperty = parentClass.GetPropertyOfKind(CodePropertyKind.AdditionalData); - var shouldDeclareErrorVar = true; + var shouldDeclareErrorVar = !inherits; if(parentClass.StartBlock is CodeClass.Declaration declaration && - declaration.Inherits != null) { + inherits) { writer.WriteLine($"err := m.{declaration.Inherits.Name.ToFirstCharacterUpperCase()}.Serialize(writer)"); WriteReturnError(writer); - shouldDeclareErrorVar = false; } foreach(var otherProp in parentClass.GetPropertiesOfKind(CodePropertyKind.Custom)) { var accessorName = otherProp.IsNameEscaped && !string.IsNullOrEmpty(otherProp.SerializationName) ? otherProp.SerializationName : otherProp.Name.ToFirstCharacterLowerCase(); @@ -211,13 +210,11 @@ private void WriteSerializationRegistration(List serializationModules, L } private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer, bool inherits) { writer.WriteLine($"m := &{parentClass.Name.ToFirstCharacterUpperCase()}{{"); - if(inherits && - parentClass.StartBlock is CodeClass.Declaration declaration) { + if(parentClass.StartBlock is CodeClass.Declaration declaration && + (inherits || parentClass.IsErrorDefinition)) { writer.IncreaseIndent(); var parentClassName = declaration.Inherits.Name.ToFirstCharacterUpperCase(); - var parentClassFullSymbol = conventions.GetTypeString(declaration.Inherits, parentClass, false, false); - var moduleName = parentClassFullSymbol.Contains(dot) ? $"{parentClassFullSymbol.Split(dot).First()}." : string.Empty; - writer.WriteLine($"{parentClassName}: *{moduleName}New{parentClassName}(),"); + writer.WriteLine($"{parentClassName}: *{conventions.GetImportedStaticMethodName(declaration.Inherits, parentClass)}(),"); writer.DecreaseIndent(); } writer.CloseBlock(decreaseIndent: false); @@ -276,10 +273,10 @@ private void WriteIndexerBody(CodeMethod codeElement, CodeClass parentClass, Lan (idParameter.Type, codeElement.OriginalIndexer.ParameterName, "id")); conventions.AddRequestBuilderBody(parentClass, returnType, writer, urlTemplateVarName: conventions.TempDictionaryVarName); } - private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer) { + private void WriteDeserializerBody(CodeMethod codeElement, CodeClass parentClass, LanguageWriter writer, bool inherits) { var fieldToSerialize = parentClass.GetPropertiesOfKind(CodePropertyKind.Custom); if(parentClass.StartBlock is CodeClass.Declaration declaration && - declaration.Inherits != null) + inherits) writer.WriteLine($"res := m.{declaration.Inherits.Name.ToFirstCharacterUpperCase()}.{codeElement.Name.ToFirstCharacterUpperCase()}()"); else writer.WriteLine($"res := make({codeElement.ReturnType.Name})"); @@ -340,20 +337,25 @@ _ when string.IsNullOrEmpty(returnType) => "SendNoContentAsync", WriteGeneratorMethodCall(codeElement, requestParams, writer, $"{RequestInfoVarName}, err := "); WriteReturnError(writer, returnType); var parsableImportSymbol = GetConversionHelperMethodImport(codeElement.Parent as CodeClass, "Parsable"); - var typeString = conventions.GetTypeString(codeElement.ReturnType, codeElement.Parent, false, false)?.Split(dot); - var importSymbol = typeString == null || typeString.Length < 2 ? string.Empty : typeString.First() + dot; var constructorFunction = returnType switch { _ when isVoid => string.Empty, _ when isPrimitive || isBinary => $"\"{returnType.TrimCollectionAndPointerSymbols()}\", ", - _ => $"func () {parsableImportSymbol} {{ return {importSymbol}New{typeString.Last()}() }}, ", + _ => $"func () {parsableImportSymbol} {{ return {conventions.GetImportedStaticMethodName(codeElement.ReturnType, codeElement.Parent)}() }}, ", }; + var errorMappingVarName = "nil"; + if(codeElement.ErrorMappings.Any()) { + errorMappingVarName = "errorMapping"; + writer.WriteLine($"{errorMappingVarName} := {conventions.AbstractionsHash}.ErrorMappings {{"); + writer.IncreaseIndent(); + foreach(var errorMapping in codeElement.ErrorMappings) { + writer.WriteLine($"\"{errorMapping.Key.ToUpperInvariant()}\": func() {parsableImportSymbol} {{ return {conventions.GetImportedStaticMethodName(errorMapping.Value, codeElement.Parent)}() }},"); + } + writer.CloseBlock(); + } var assignmentPrefix = isVoid ? "err =" : "res, err :="; - if(responseHandlerParam != null) - writer.WriteLine($"{assignmentPrefix} m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}{responseHandlerParam.Name})"); - else - writer.WriteLine($"{assignmentPrefix} m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}nil)"); + writer.WriteLine($"{assignmentPrefix} m.requestAdapter.{sendMethodName}(*{RequestInfoVarName}, {constructorFunction}{responseHandlerParam?.Name ?? "nil"}, {errorMappingVarName})"); WriteReturnError(writer, returnType); var valueVarName = string.Empty; if(codeElement.ReturnType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None) { @@ -448,16 +450,15 @@ private string GetDeserializationMethodName(CodeTypeBase propType, CodeClass par var propertyTypeName = conventions.GetTypeString(propType, parentClass, false, false); var propertyTypeNameWithoutImportSymbol = conventions.TranslateType(propType, false); if(propType is CodeType currentType) { - var importSymbol = propertyTypeName.Contains(dot) ? $"{propertyTypeName.Split(dot).First().TrimCollectionAndPointerSymbols()}." : string.Empty; if(isCollection) if(currentType.TypeDefinition == null) return $"n.GetCollectionOfPrimitiveValues(\"{propertyTypeName.ToFirstCharacterLowerCase()}\")"; else if (currentType.TypeDefinition is CodeEnum) - return $"n.GetCollectionOfEnumValues({importSymbol}Parse{propertyTypeNameWithoutImportSymbol.ToFirstCharacterUpperCase()})"; + return $"n.GetCollectionOfEnumValues({conventions.GetImportedStaticMethodName(propType, parentClass, "Parse")})"; else return $"n.GetCollectionOfObjectValues({GetTypeFactory(propType, parentClass, propertyTypeNameWithoutImportSymbol)})"; else if (currentType.TypeDefinition is CodeEnum currentEnum) { - return $"n.GetEnum{(currentEnum.Flags ? "Set" : string.Empty)}Value({importSymbol}Parse{propertyTypeNameWithoutImportSymbol.ToFirstCharacterUpperCase()})"; + return $"n.GetEnum{(currentEnum.Flags ? "Set" : string.Empty)}Value({conventions.GetImportedStaticMethodName(propType, parentClass, "Parse")})"; } } return propertyTypeNameWithoutImportSymbol switch { @@ -468,13 +469,10 @@ _ when conventions.StreamTypeName.Equals(propertyTypeNameWithoutImportSymbol, St _ => $"n.GetObjectValue({GetTypeFactory(propType, parentClass, propertyTypeNameWithoutImportSymbol)})", }; } - private static readonly char dot = '.'; private string GetTypeFactory(CodeTypeBase propTypeBase, CodeClass parentClass, string propertyTypeName) { if(propTypeBase is CodeType propType) { - var importSymbol = conventions.GetTypeString(propType, parentClass, false, false); - var importNS = importSymbol.Contains(dot) ? importSymbol.Split(dot).First() + dot : string.Empty; var parsableSymbol = GetConversionHelperMethodImport(parentClass, "Parsable"); - return $"func () {parsableSymbol} {{ return {importNS}New{propertyTypeName.ToFirstCharacterUpperCase()}() }}"; + return $"func () {parsableSymbol} {{ return {conventions.GetImportedStaticMethodName(propType, parentClass)}() }}"; } else return GetTypeFactory(propTypeBase.AllTypes.First(), parentClass, propertyTypeName); } private void WriteSerializationMethodCall(CodeTypeBase propType, CodeClass parentClass, string serializationKey, string valueGet, bool shouldDeclareErrorVar, LanguageWriter writer) { diff --git a/src/Kiota.Builder/Writers/Go/GoConventionService.cs b/src/Kiota.Builder/Writers/Go/GoConventionService.cs index 4ce2a71e93..08296cd79a 100644 --- a/src/Kiota.Builder/Writers/Go/GoConventionService.cs +++ b/src/Kiota.Builder/Writers/Go/GoConventionService.cs @@ -24,6 +24,12 @@ public override string GetParameterSignature(CodeParameter parameter, CodeElemen { return $"{parameter.Name.ToFirstCharacterLowerCase()} {GetTypeString(parameter.Type, targetElement)}"; } + private static readonly char dot = '.'; + public string GetImportedStaticMethodName(CodeTypeBase code, CodeElement targetElement, string methodPrefix = "New") { + var typeString = GetTypeString(code, targetElement, false, false)?.Split(dot); + var importSymbol = typeString == null || typeString.Length < 2 ? string.Empty : typeString.First() + dot; + return $"{importSymbol}{methodPrefix}{typeString.Last().ToFirstCharacterUpperCase()}"; + } public override string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true) => GetTypeString(code, targetElement, includeCollectionInformation, true); public string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation, bool addPointerSymbol) diff --git a/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs index d4a9e63e2b..84a3876445 100644 --- a/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/Go/CodeMethodWriterTests.cs @@ -7,621 +7,652 @@ using Moq; using Xunit; -namespace Kiota.Builder.Writers.Go.Tests { - public class CodeMethodWriterTests : IDisposable { - private const string DefaultPath = "./"; - private const string DefaultName = "name"; - private readonly StringWriter tw; - private readonly LanguageWriter writer; - private readonly CodeMethod method; - private readonly CodeClass parentClass; - private readonly CodeNamespace root; - private const string MethodName = "methodName"; - private const string ReturnTypeName = "Somecustomtype"; - private const string MethodDescription = "some description"; - private const string ParamDescription = "some parameter description"; - private const string ParamName = "paramName"; - public CodeMethodWriterTests() - { - writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Go, DefaultPath, DefaultName); - tw = new StringWriter(); - writer.SetTextWriter(tw); - root = CodeNamespace.InitRootNamespace(); - parentClass = new CodeClass { - Name = "parentClass" - }; - root.AddClass(parentClass); - method = new CodeMethod { - Name = MethodName, - }; - method.ReturnType = new CodeType { - Name = ReturnTypeName - }; - parentClass.AddMethod(method); - } - public void Dispose() - { - tw?.Dispose(); - GC.SuppressFinalize(this); - } - private void AddRequestProperties() { - parentClass.AddProperty(new CodeProperty { - Name = "requestAdapter", - PropertyKind = CodePropertyKind.RequestAdapter, - }); - parentClass.AddProperty(new CodeProperty { - Name = "pathParameters", - PropertyKind = CodePropertyKind.PathParameters, - Type = new CodeType { - Name = "string" - }, - }); - parentClass.AddProperty(new CodeProperty { - Name = "UrlTemplate", - PropertyKind = CodePropertyKind.UrlTemplate, - }); - } - private void AddSerializationProperties() { - var addData = parentClass.AddProperty(new CodeProperty { - Name = "additionalData", - PropertyKind = CodePropertyKind.AdditionalData, - }).First(); - addData.Type = new CodeType { - Name = "string" - }; - var dummyProp = parentClass.AddProperty(new CodeProperty { - Name = "dummyProp", - }).First(); - dummyProp.Type = new CodeType { +namespace Kiota.Builder.Writers.Go.Tests; +public class CodeMethodWriterTests : IDisposable { + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeMethod method; + private readonly CodeClass parentClass; + private readonly CodeNamespace root; + private const string MethodName = "methodName"; + private const string ReturnTypeName = "Somecustomtype"; + private const string MethodDescription = "some description"; + private const string ParamDescription = "some parameter description"; + private const string ParamName = "paramName"; + public CodeMethodWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.Go, DefaultPath, DefaultName); + tw = new StringWriter(); + writer.SetTextWriter(tw); + root = CodeNamespace.InitRootNamespace(); + parentClass = new CodeClass { + Name = "parentClass" + }; + root.AddClass(parentClass); + method = new CodeMethod { + Name = MethodName, + }; + method.ReturnType = new CodeType { + Name = ReturnTypeName + }; + parentClass.AddMethod(method); + } + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + private void AddRequestProperties() { + parentClass.AddProperty(new CodeProperty { + Name = "requestAdapter", + PropertyKind = CodePropertyKind.RequestAdapter, + }); + parentClass.AddProperty(new CodeProperty { + Name = "pathParameters", + PropertyKind = CodePropertyKind.PathParameters, + Type = new CodeType { Name = "string" - }; - var dummyCollectionProp = parentClass.AddProperty(new CodeProperty { - Name = "dummyColl", - }).First(); - dummyCollectionProp.Type = new CodeType { + }, + }); + parentClass.AddProperty(new CodeProperty { + Name = "UrlTemplate", + PropertyKind = CodePropertyKind.UrlTemplate, + }); + } + private void AddSerializationProperties() { + var addData = parentClass.AddProperty(new CodeProperty { + Name = "additionalData", + PropertyKind = CodePropertyKind.AdditionalData, + }).First(); + addData.Type = new CodeType { + Name = "string" + }; + var dummyProp = parentClass.AddProperty(new CodeProperty { + Name = "dummyProp", + }).First(); + dummyProp.Type = new CodeType { + Name = "string" + }; + var dummyCollectionProp = parentClass.AddProperty(new CodeProperty { + Name = "dummyColl", + }).First(); + dummyCollectionProp.Type = new CodeType { + Name = "string", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + }; + var dummyComplexCollection = parentClass.AddProperty(new CodeProperty { + Name = "dummyComplexColl" + }).First(); + dummyComplexCollection.Type = new CodeType { + Name = "Complex", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + TypeDefinition = new CodeClass { + Name = "SomeComplexType" + } + }; + var dummyEnumProp = parentClass.AddProperty(new CodeProperty{ + Name = "dummyEnumCollection", + }).First(); + dummyEnumProp.Type = new CodeType { + Name = "SomeEnum", + TypeDefinition = new CodeEnum { + Name = "EnumType" + } + }; + } + private void AddInheritanceClass() { + (parentClass.StartBlock as CodeClass.Declaration).Inherits = new CodeType { + Name = "someParentClass" + }; + } + private void AddRequestBodyParameters(CodeMethod target = default) { + var stringType = new CodeType { + Name = "string", + }; + target ??= method; + target.AddParameter(new CodeParameter { + Name = "h", + ParameterKind = CodeParameterKind.Headers, + Type = stringType, + }); + target.AddParameter(new CodeParameter{ + Name = "q", + ParameterKind = CodeParameterKind.QueryParameter, + Type = stringType, + }); + target.AddParameter(new CodeParameter{ + Name = "b", + ParameterKind = CodeParameterKind.RequestBody, + Type = stringType, + }); + target.AddParameter(new CodeParameter{ + Name = "r", + ParameterKind = CodeParameterKind.ResponseHandler, + Type = stringType, + }); + target.AddParameter(new CodeParameter { + Name = "o", + ParameterKind = CodeParameterKind.Options, + Type = stringType, + }); + } + [Fact] + public void WritesNullableVoidTypeForExecutor(){ + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + method.ReturnType = new CodeType { + Name = "void", + }; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("(error)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesRequestBuilder() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.RequestBuilderBackwardCompatibility; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.pathParameters", result); + Assert.Contains("m.requestAdapter", result); + Assert.Contains("return", result); + Assert.Contains("func (m", result); + Assert.Contains("New", result); + } + [Fact] + public void WritesRequestBodiesThrowOnNullHttpMethod() { + method.MethodKind = CodeMethodKind.RequestExecutor; + Assert.Throws(() => writer.Write(method)); + method.MethodKind = CodeMethodKind.RequestGenerator; + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void WritesRequestExecutorBody() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + var error4XX = root.AddClass(new CodeClass{ + Name = "Error4XX", + }).First(); + var error5XX = root.AddClass(new CodeClass{ + Name = "Error5XX", + }).First(); + var error401 = root.AddClass(new CodeClass{ + Name = "Error401", + }).First(); + method.ErrorMappings = new () { + {"4XX", new CodeType {Name = "Error4XX", TypeDefinition = error4XX}}, + {"5XX", new CodeType {Name = "Error5XX", TypeDefinition = error5XX}}, + {"403", new CodeType {Name = "Error403", TypeDefinition = error401}}, + }; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("requestInfo, err :=", result); + Assert.Contains($"errorMapping := {AbstractionsPackageHash}.ErrorMappings", result); + Assert.Contains($"\"4XX\": func() Parsable {{ return ", result); + Assert.Contains($"\"5XX\": func() Parsable {{ return ", result); + Assert.Contains($"\"403\": func() Parsable {{ return ", result); + Assert.Contains("NewError5XX", result); + Assert.Contains("NewError4XX", result); + Assert.Contains("NewError403", result); + Assert.Contains("m.requestAdapter.SendAsync", result); + Assert.Contains("return res.(", result); + Assert.Contains("err != nil", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesntCreateDictionaryOnEmptyErrorMapping() { + method.MethodKind = CodeMethodKind.RequestExecutor; + method.HttpMethod = HttpMethod.Get; + AddRequestBodyParameters(); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain($"errorMapping := {AbstractionsPackageHash}.ErrorMappings", result); + Assert.Contains("nil)", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + private const string AbstractionsPackageHash = "ida96af0f171bb75f894a4013a6b3146a4397c58f11adb81a2b7cbea9314783a9"; + [Fact] + public void WritesRequestGeneratorBody() { + var configurationMock = new Mock(); + var refiner = new GoRefiner(configurationMock.Object); + method.MethodKind = CodeMethodKind.RequestGenerator; + method.HttpMethod = HttpMethod.Get; + var executor = parentClass.AddMethod(new CodeMethod { + Name = "executor", + HttpMethod = HttpMethod.Get, + MethodKind = CodeMethodKind.RequestExecutor, + ReturnType = new CodeType { + Name = "string", + IsExternal = true, + } + }).First(); + AddRequestBodyParameters(executor); + AddRequestBodyParameters(); + AddRequestProperties(); + refiner.Refine(parentClass.Parent as CodeNamespace); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"requestInfo := {AbstractionsPackageHash}.NewRequestInformation()", result); + Assert.Contains("requestInfo.UrlTemplate = ", result); + Assert.Contains("requestInfo.PathParameters", result); + Assert.Contains($"Method = {AbstractionsPackageHash}.GET", result); + Assert.Contains("err != nil", result); + Assert.Contains("H != nil", result); + Assert.Contains("requestInfo.Headers =", result); + Assert.Contains("Q != nil", result); + Assert.Contains("requestInfo.AddQueryParameters(*(options.Q))", result); + Assert.Contains("O) != 0", result); + Assert.Contains("requestInfo.AddRequestOptions(", result); + Assert.Contains("requestInfo.SetContentFromParsable(m.requestAdapter", result); + Assert.Contains("return requestInfo, nil", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedDeSerializerBody() { + method.MethodKind = CodeMethodKind.Deserializer; + method.IsAsync = false; + AddSerializationProperties(); + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.SomeParentClass.MethodName()", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesDeSerializerBody() { + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.MethodKind = CodeMethodKind.Deserializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("GetStringValue", result); + Assert.Contains("GetCollectionOfPrimitiveValues", result); + Assert.Contains("GetCollectionOfObjectValues", result); + Assert.Contains("GetEnumValue", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesInheritedSerializerBody() { + method.MethodKind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.SomeParentClass.Serialize", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesSerializerBody() { + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.MethodKind = CodeMethodKind.Serializer; + method.IsAsync = false; + AddSerializationProperties(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("WriteStringValue", result); + Assert.Contains("WriteCollectionOfStringValues", result); + Assert.Contains("WriteCollectionOfObjectValues", result); + Assert.Contains("WriteAdditionalData(m.GetAdditionalData())", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact(Skip = "descriptions are not supported")] + public void WritesMethodSyncDescription() { + method.Description = MethodDescription; + method.IsAsync = false; + var parameter = new CodeParameter{ + Description = ParamDescription, + Name = ParamName + }; + parameter.Type = new CodeType { + Name = "string" + }; + method.AddParameter(parameter); + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain("@return a CompletableFuture of", result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void Defensive() { + var codeMethodWriter = new CodeMethodWriter(new GoConventionService()); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); + var originalParent = method.Parent; + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + method.Parent = originalParent; + method.ReturnType = null; + Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); + } + [Fact] + public void ThrowsIfParentIsNotClass() { + method.Parent = CodeNamespace.InitRootNamespace(); + Assert.Throws(() => writer.Write(method)); + } + [Fact] + public void ThrowsIfReturnTypeIsMissing() { + method.ReturnType = null; + Assert.Throws(() => writer.Write(method)); + } + private const string TaskPrefix = "func() ("; + [Fact] + public void WritesReturnType() { + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"{MethodName.ToFirstCharacterUpperCase()}()(*{ReturnTypeName}, error)", result);// async default + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void DoesNotAddAsyncInformationOnSyncMethods() { + method.IsAsync = false; + writer.Write(method); + var result = tw.ToString(); + Assert.DoesNotContain(TaskPrefix, result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPublicMethodByDefault() { + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(MethodName.ToFirstCharacterUpperCase(), result);// public default + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesPrivateMethod() { + method.Access = AccessModifier.Private; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(MethodName.ToFirstCharacterLowerCase(), result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesProtectedMethod() { + method.Access = AccessModifier.Protected; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(MethodName.ToFirstCharacterLowerCase(), result); + AssertExtensions.CurlyBracesAreClosed(result); + } + [Fact] + public void WritesIndexer() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.IndexerBackwardCompatibility; + method.OriginalIndexer = new () { + Name = "indx", + ParameterName = "id", + IndexType = new CodeType { Name = "string", - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, - }; - var dummyComplexCollection = parentClass.AddProperty(new CodeProperty { - Name = "dummyComplexColl" - }).First(); - dummyComplexCollection.Type = new CodeType { - Name = "Complex", - CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, - TypeDefinition = new CodeClass { - Name = "SomeComplexType" - } - }; - var dummyEnumProp = parentClass.AddProperty(new CodeProperty{ - Name = "dummyEnumCollection", - }).First(); - dummyEnumProp.Type = new CodeType { - Name = "SomeEnum", - TypeDefinition = new CodeEnum { - Name = "EnumType" - } - }; - } - private void AddInheritanceClass() { - (parentClass.StartBlock as CodeClass.Declaration).Inherits = new CodeType { - Name = "someParentClass" - }; - } - private void AddRequestBodyParameters(CodeMethod target = default) { - var stringType = new CodeType { + IsNullable = true, + } + }; + method.AddParameter(new CodeParameter { + Name = "id", + ParameterKind = CodeParameterKind.Custom, + Type = new CodeType { Name = "string", - }; - target ??= method; - target.AddParameter(new CodeParameter { - Name = "h", - ParameterKind = CodeParameterKind.Headers, - Type = stringType, - }); - target.AddParameter(new CodeParameter{ - Name = "q", - ParameterKind = CodeParameterKind.QueryParameter, - Type = stringType, - }); - target.AddParameter(new CodeParameter{ - Name = "b", - ParameterKind = CodeParameterKind.RequestBody, - Type = stringType, - }); - target.AddParameter(new CodeParameter{ - Name = "r", - ParameterKind = CodeParameterKind.ResponseHandler, - Type = stringType, - }); - target.AddParameter(new CodeParameter { - Name = "o", - ParameterKind = CodeParameterKind.Options, - Type = stringType, - }); - } - [Fact] - public void WritesNullableVoidTypeForExecutor(){ - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - method.ReturnType = new CodeType { - Name = "void", - }; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("(error)", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesRequestBuilder() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.RequestBuilderBackwardCompatibility; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.pathParameters", result); - Assert.Contains("m.requestAdapter", result); - Assert.Contains("return", result); - Assert.Contains("func (m", result); - Assert.Contains("New", result); - } - [Fact] - public void WritesRequestBodiesThrowOnNullHttpMethod() { - method.MethodKind = CodeMethodKind.RequestExecutor; - Assert.Throws(() => writer.Write(method)); - method.MethodKind = CodeMethodKind.RequestGenerator; - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void WritesRequestExecutorBody() { - method.MethodKind = CodeMethodKind.RequestExecutor; - method.HttpMethod = HttpMethod.Get; - AddRequestBodyParameters(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("requestInfo, err :=", result); - Assert.Contains("m.requestAdapter.SendAsync", result); - Assert.Contains("return res.(", result); - Assert.Contains("err != nil", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - private const string AbstractionsPackageHash = "ida96af0f171bb75f894a4013a6b3146a4397c58f11adb81a2b7cbea9314783a9"; - [Fact] - public void WritesRequestGeneratorBody() { - var configurationMock = new Mock(); - var refiner = new GoRefiner(configurationMock.Object); - method.MethodKind = CodeMethodKind.RequestGenerator; - method.HttpMethod = HttpMethod.Get; - var executor = parentClass.AddMethod(new CodeMethod { - Name = "executor", - HttpMethod = HttpMethod.Get, - MethodKind = CodeMethodKind.RequestExecutor, - ReturnType = new CodeType { - Name = "string", - IsExternal = true, - } - }).First(); - AddRequestBodyParameters(executor); - AddRequestBodyParameters(); - AddRequestProperties(); - refiner.Refine(parentClass.Parent as CodeNamespace); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains($"requestInfo := {AbstractionsPackageHash}.NewRequestInformation()", result); - Assert.Contains("requestInfo.UrlTemplate = ", result); - Assert.Contains("requestInfo.PathParameters", result); - Assert.Contains($"Method = {AbstractionsPackageHash}.GET", result); - Assert.Contains("err != nil", result); - Assert.Contains("H != nil", result); - Assert.Contains("requestInfo.Headers =", result); - Assert.Contains("Q != nil", result); - Assert.Contains("requestInfo.AddQueryParameters(*(options.Q))", result); - Assert.Contains("O) != 0", result); - Assert.Contains("requestInfo.AddRequestOptions(", result); - Assert.Contains("requestInfo.SetContentFromParsable(m.requestAdapter", result); - Assert.Contains("return requestInfo, nil", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesInheritedDeSerializerBody() { - method.MethodKind = CodeMethodKind.Deserializer; - method.IsAsync = false; - AddSerializationProperties(); - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.SomeParentClass.MethodName()", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesDeSerializerBody() { - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { + IsNullable = true, + }, + Optional = true + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.requestAdapter", result); + Assert.Contains("m.pathParameters", result); + Assert.Contains("= *id", result); + Assert.Contains("return", result); + Assert.Contains("New", result); + } + [Fact] + public void WritesPathParameterRequestBuilder() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; + method.AddParameter(new CodeParameter { + Name = "pathParam", + ParameterKind = CodeParameterKind.Path, + Type = new CodeType { Name = "string" - }; - method.MethodKind = CodeMethodKind.Deserializer; - method.IsAsync = false; - AddSerializationProperties(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("GetStringValue", result); - Assert.Contains("GetCollectionOfPrimitiveValues", result); - Assert.Contains("GetCollectionOfObjectValues", result); - Assert.Contains("GetEnumValue", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesInheritedSerializerBody() { - method.MethodKind = CodeMethodKind.Serializer; - method.IsAsync = false; - AddSerializationProperties(); - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.SomeParentClass.Serialize", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesSerializerBody() { - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.requestAdapter", result); + Assert.Contains("m.pathParameters", result); + Assert.Contains("pathParam", result); + Assert.Contains("return New", result); + } + [Fact] + public void WritesDescription() { + AddRequestProperties(); + method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; + method.AddParameter(new CodeParameter { + Name = "pathParam", + ParameterKind = CodeParameterKind.Path, + Type = new CodeType { Name = "string" - }; - method.MethodKind = CodeMethodKind.Serializer; - method.IsAsync = false; - AddSerializationProperties(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("WriteStringValue", result); - Assert.Contains("WriteCollectionOfStringValues", result); - Assert.Contains("WriteCollectionOfObjectValues", result); - Assert.Contains("WriteAdditionalData(m.GetAdditionalData())", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact(Skip = "descriptions are not supported")] - public void WritesMethodSyncDescription() { - method.Description = MethodDescription; - method.IsAsync = false; - var parameter = new CodeParameter{ - Description = ParamDescription, - Name = ParamName - }; - parameter.Type = new CodeType { + } + }); + method.Description = "Some description"; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains($"// {method.Name.ToFirstCharacterUpperCase()} some description", result); + } + [Fact] + public void WritesGetterToBackingStore() { + parentClass.AddBackingStoreProperty(); + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Getter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.GetBackingStore().Get(\"someProperty\")", result); + } + [Fact] + public void WritesGetterToBackingStoreWithNonnullProperty() { + method.AddAccessedProperty(); + parentClass.AddBackingStoreProperty(); + method.AccessedProperty.Type = new CodeType { + Name = "string", + IsNullable = false, + }; + var defaultValue = "someDefaultValue"; + method.AccessedProperty.DefaultValue = defaultValue; + method.MethodKind = CodeMethodKind.Getter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("if value == nil", result); + Assert.Contains(defaultValue, result); + } + [Fact] + public void WritesSetterToBackingStore() { + parentClass.AddBackingStoreProperty(); + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Setter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.GetBackingStore().Set(\"someProperty\", value)", result); + } + [Fact] + public void WritesGetterToField() { + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Getter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.someProperty", result); + } + [Fact] + public void WritesSetterToField() { + method.AddAccessedProperty(); + method.MethodKind = CodeMethodKind.Setter; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains("m.someProperty = value", result); + } + [Fact] + public void WritesConstructor() { + method.MethodKind = CodeMethodKind.Constructor; + var defaultValue = "someVal"; + var propName = "propWithDefaultValue"; + parentClass.ClassKind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty { + Name = propName, + DefaultValue = defaultValue, + PropertyKind = CodePropertyKind.UrlTemplate, + }); + AddRequestProperties(); + method.AddParameter(new CodeParameter { + Name = "pathParameters", + ParameterKind = CodeParameterKind.PathParameters, + Type = new CodeType { + Name = "map[string]string" + } + }); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains($"m.{propName} = {defaultValue}", result); + Assert.Contains("make(map[string]string)", result); + } + [Fact] + public void WritesRawUrlConstructor() { + method.MethodKind = CodeMethodKind.RawUrlConstructor; + var defaultValue = "someVal"; + var propName = "propWithDefaultValue"; + parentClass.ClassKind = CodeClassKind.RequestBuilder; + parentClass.AddProperty(new CodeProperty { + Name = propName, + DefaultValue = defaultValue, + PropertyKind = CodePropertyKind.UrlTemplate, + }); + AddRequestProperties(); + method.AddParameter(new CodeParameter { + Name = "rawUrl", + ParameterKind = CodeParameterKind.RawUrl, + Type = new CodeType { Name = "string" - }; - method.AddParameter(parameter); - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain("@return a CompletableFuture of", result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void Defensive() { - var codeMethodWriter = new CodeMethodWriter(new GoConventionService()); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(null, writer)); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, null)); - var originalParent = method.Parent; - method.Parent = CodeNamespace.InitRootNamespace(); - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); - method.Parent = originalParent; - method.ReturnType = null; - Assert.Throws(() => codeMethodWriter.WriteCodeElement(method, writer)); - } - [Fact] - public void ThrowsIfParentIsNotClass() { - method.Parent = CodeNamespace.InitRootNamespace(); - Assert.Throws(() => writer.Write(method)); - } - [Fact] - public void ThrowsIfReturnTypeIsMissing() { - method.ReturnType = null; - Assert.Throws(() => writer.Write(method)); - } - private const string TaskPrefix = "func() ("; - [Fact] - public void WritesReturnType() { - writer.Write(method); - var result = tw.ToString(); - Assert.Contains($"{MethodName.ToFirstCharacterUpperCase()}()(*{ReturnTypeName}, error)", result);// async default - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void DoesNotAddAsyncInformationOnSyncMethods() { - method.IsAsync = false; - writer.Write(method); - var result = tw.ToString(); - Assert.DoesNotContain(TaskPrefix, result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesPublicMethodByDefault() { - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(MethodName.ToFirstCharacterUpperCase(), result);// public default - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesPrivateMethod() { - method.Access = AccessModifier.Private; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(MethodName.ToFirstCharacterLowerCase(), result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesProtectedMethod() { - method.Access = AccessModifier.Protected; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(MethodName.ToFirstCharacterLowerCase(), result); - AssertExtensions.CurlyBracesAreClosed(result); - } - [Fact] - public void WritesIndexer() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.IndexerBackwardCompatibility; - method.OriginalIndexer = new () { - Name = "indx", - ParameterName = "id", - IndexType = new CodeType { - Name = "string", - IsNullable = true, - } - }; - method.AddParameter(new CodeParameter { - Name = "id", - ParameterKind = CodeParameterKind.Custom, - Type = new CodeType { - Name = "string", - IsNullable = true, - }, - Optional = true - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.requestAdapter", result); - Assert.Contains("m.pathParameters", result); - Assert.Contains("= *id", result); - Assert.Contains("return", result); - Assert.Contains("New", result); - } - [Fact] - public void WritesPathParameterRequestBuilder() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; - method.AddParameter(new CodeParameter { - Name = "pathParam", - ParameterKind = CodeParameterKind.Path, - Type = new CodeType { - Name = "string" - } - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.requestAdapter", result); - Assert.Contains("m.pathParameters", result); - Assert.Contains("pathParam", result); - Assert.Contains("return New", result); - } - [Fact] - public void WritesDescription() { - AddRequestProperties(); - method.MethodKind = CodeMethodKind.RequestBuilderWithParameters; - method.AddParameter(new CodeParameter { - Name = "pathParam", - ParameterKind = CodeParameterKind.Path, - Type = new CodeType { - Name = "string" - } - }); - method.Description = "Some description"; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains($"// {method.Name.ToFirstCharacterUpperCase()} some description", result); - } - [Fact] - public void WritesGetterToBackingStore() { - parentClass.AddBackingStoreProperty(); - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Getter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.GetBackingStore().Get(\"someProperty\")", result); - } - [Fact] - public void WritesGetterToBackingStoreWithNonnullProperty() { - method.AddAccessedProperty(); - parentClass.AddBackingStoreProperty(); - method.AccessedProperty.Type = new CodeType { - Name = "string", - IsNullable = false, - }; - var defaultValue = "someDefaultValue"; - method.AccessedProperty.DefaultValue = defaultValue; - method.MethodKind = CodeMethodKind.Getter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("if value == nil", result); - Assert.Contains(defaultValue, result); - } - [Fact] - public void WritesSetterToBackingStore() { - parentClass.AddBackingStoreProperty(); - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Setter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.GetBackingStore().Set(\"someProperty\", value)", result); - } - [Fact] - public void WritesGetterToField() { - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Getter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.someProperty", result); - } - [Fact] - public void WritesSetterToField() { - method.AddAccessedProperty(); - method.MethodKind = CodeMethodKind.Setter; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains("m.someProperty = value", result); - } - [Fact] - public void WritesConstructor() { - method.MethodKind = CodeMethodKind.Constructor; - var defaultValue = "someVal"; - var propName = "propWithDefaultValue"; - parentClass.ClassKind = CodeClassKind.RequestBuilder; - parentClass.AddProperty(new CodeProperty { - Name = propName, - DefaultValue = defaultValue, - PropertyKind = CodePropertyKind.UrlTemplate, - }); - AddRequestProperties(); - method.AddParameter(new CodeParameter { - Name = "pathParameters", - ParameterKind = CodeParameterKind.PathParameters, - Type = new CodeType { - Name = "map[string]string" - } - }); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains($"m.{propName} = {defaultValue}", result); - Assert.Contains("make(map[string]string)", result); - } - [Fact] - public void WritesRawUrlConstructor() { - method.MethodKind = CodeMethodKind.RawUrlConstructor; - var defaultValue = "someVal"; - var propName = "propWithDefaultValue"; - parentClass.ClassKind = CodeClassKind.RequestBuilder; - parentClass.AddProperty(new CodeProperty { - Name = propName, - DefaultValue = defaultValue, - PropertyKind = CodePropertyKind.UrlTemplate, - }); - AddRequestProperties(); - method.AddParameter(new CodeParameter { - Name = "rawUrl", - ParameterKind = CodeParameterKind.RawUrl, - Type = new CodeType { - Name = "string" - } - }); - method.AddParameter(new CodeParameter { - Name = "requestAdapter", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = new CodeType { - Name = "string" - } - }); - method.OriginalMethod = new (); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains($"urlParams := make(map[string]string)", result); - Assert.Contains($"urlParams[\"request-raw-url\"] = rawUrl", result); - } - [Fact] - public void WritesInheritedConstructor() { - method.MethodKind = CodeMethodKind.Constructor; - AddInheritanceClass(); - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains("SomeParentClass: *NewSomeParentClass", result); - } - [Fact] - public void WritesApiConstructor() { - method.MethodKind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty { - Name = "core", - PropertyKind = CodePropertyKind.RequestAdapter, - }).First(); - coreProp.Type = new CodeType { - Name = "HttpCore", - IsExternal = true, - }; - method.AddParameter(new CodeParameter { - Name = "core", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, - }); - method.DeserializerModules = new() {"github.com/microsoft/kiota/serialization/go/json.Deserializer"}; - method.SerializerModules = new() {"github.com/microsoft/kiota/serialization/go/json.Serializer"}; - writer.Write(method); - var result = tw.ToString(); - Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); - Assert.Contains("RegisterDefaultSerializer", result); - Assert.Contains("RegisterDefaultDeserializer", result); - } - [Fact] - public void WritesApiConstructorWithBackingStore() { - method.MethodKind = CodeMethodKind.ClientConstructor; - var coreProp = parentClass.AddProperty(new CodeProperty { - Name = "core", - PropertyKind = CodePropertyKind.RequestAdapter, - }).First(); - coreProp.Type = new CodeType { - Name = "HttpCore", - IsExternal = true, - }; - method.AddParameter(new CodeParameter { - Name = "core", - ParameterKind = CodeParameterKind.RequestAdapter, - Type = coreProp.Type, - }); - var backingStoreParam = new CodeParameter { - Name = "backingStore", - ParameterKind = CodeParameterKind.BackingStore, - }; - backingStoreParam.Type = new CodeType { - Name = "IBackingStore", - IsExternal = true, - }; - method.AddParameter(backingStoreParam); - var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Go, DefaultPath, DefaultName); - tempWriter.SetTextWriter(tw); - tempWriter.Write(method); - var result = tw.ToString(); - Assert.Contains("EnableBackingStore", result); - } - [Fact] - public void AccessorsTargetingEscapedPropertiesAreNotEscapedThemselves() { - var model = root.AddClass(new CodeClass { - Name = "SomeClass", - ClassKind = CodeClassKind.Model - }).First(); - model.AddProperty(new CodeProperty { - Name = "select", - Type = new CodeType { Name = "string" }, - Access = AccessModifier.Public, - PropertyKind = CodePropertyKind.Custom, - }); - ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); - var getter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Getter)); - var setter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Setter)); - var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Go, DefaultPath, DefaultName); - tempWriter.SetTextWriter(tw); - tempWriter.Write(getter); - var result = tw.ToString(); - Assert.Contains("GetSelect", result); - Assert.DoesNotContain("GetSelect_escaped", result); - - using var tw2 = new StringWriter(); - tempWriter.SetTextWriter(tw2); - tempWriter.Write(setter); - result = tw2.ToString(); - Assert.Contains("SetSelect", result); - Assert.DoesNotContain("SetSelect_escaped", result); - } + } + }); + method.AddParameter(new CodeParameter { + Name = "requestAdapter", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = new CodeType { + Name = "string" + } + }); + method.OriginalMethod = new (); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains($"urlParams := make(map[string]string)", result); + Assert.Contains($"urlParams[\"request-raw-url\"] = rawUrl", result); + } + [Fact] + public void WritesInheritedConstructor() { + method.MethodKind = CodeMethodKind.Constructor; + AddInheritanceClass(); + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains("SomeParentClass: *NewSomeParentClass", result); + } + [Fact] + public void WritesApiConstructor() { + method.MethodKind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty { + Name = "core", + PropertyKind = CodePropertyKind.RequestAdapter, + }).First(); + coreProp.Type = new CodeType { + Name = "HttpCore", + IsExternal = true, + }; + method.AddParameter(new CodeParameter { + Name = "core", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + method.DeserializerModules = new() {"github.com/microsoft/kiota/serialization/go/json.Deserializer"}; + method.SerializerModules = new() {"github.com/microsoft/kiota/serialization/go/json.Serializer"}; + writer.Write(method); + var result = tw.ToString(); + Assert.Contains(parentClass.Name.ToFirstCharacterUpperCase(), result); + Assert.Contains("RegisterDefaultSerializer", result); + Assert.Contains("RegisterDefaultDeserializer", result); + } + [Fact] + public void WritesApiConstructorWithBackingStore() { + method.MethodKind = CodeMethodKind.ClientConstructor; + var coreProp = parentClass.AddProperty(new CodeProperty { + Name = "core", + PropertyKind = CodePropertyKind.RequestAdapter, + }).First(); + coreProp.Type = new CodeType { + Name = "HttpCore", + IsExternal = true, + }; + method.AddParameter(new CodeParameter { + Name = "core", + ParameterKind = CodeParameterKind.RequestAdapter, + Type = coreProp.Type, + }); + var backingStoreParam = new CodeParameter { + Name = "backingStore", + ParameterKind = CodeParameterKind.BackingStore, + }; + backingStoreParam.Type = new CodeType { + Name = "IBackingStore", + IsExternal = true, + }; + method.AddParameter(backingStoreParam); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Go, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(method); + var result = tw.ToString(); + Assert.Contains("EnableBackingStore", result); + } + [Fact] + public void AccessorsTargetingEscapedPropertiesAreNotEscapedThemselves() { + var model = root.AddClass(new CodeClass { + Name = "SomeClass", + ClassKind = CodeClassKind.Model + }).First(); + model.AddProperty(new CodeProperty { + Name = "select", + Type = new CodeType { Name = "string" }, + Access = AccessModifier.Public, + PropertyKind = CodePropertyKind.Custom, + }); + ILanguageRefiner.Refine(new GenerationConfiguration { Language = GenerationLanguage.Go }, root); + var getter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Getter)); + var setter = model.Methods.First(x => x.IsOfKind(CodeMethodKind.Setter)); + var tempWriter = LanguageWriter.GetLanguageWriter(GenerationLanguage.Go, DefaultPath, DefaultName); + tempWriter.SetTextWriter(tw); + tempWriter.Write(getter); + var result = tw.ToString(); + Assert.Contains("GetSelect", result); + Assert.DoesNotContain("GetSelect_escaped", result); + + using var tw2 = new StringWriter(); + tempWriter.SetTextWriter(tw2); + tempWriter.Write(setter); + result = tw2.ToString(); + Assert.Contains("SetSelect", result); + Assert.DoesNotContain("SetSelect_escaped", result); } } From 11bb61eb6063d675184c20bf566c2139c4f931da Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 12:52:03 -0500 Subject: [PATCH 27/36] - adds missing constructor for go api error Signed-off-by: Vincent Biret --- abstractions/go/api_error.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/abstractions/go/api_error.go b/abstractions/go/api_error.go index 3e2ace3a9d..90c3c0fb30 100644 --- a/abstractions/go/api_error.go +++ b/abstractions/go/api_error.go @@ -14,3 +14,8 @@ func (e *ApiError) Error() string { return "error status code received from the API" } } + +// NewApiError creates a new ApiError instance +func NewApiError() *ApiError { + return &ApiError{} +} From 1210f3e9990f6c86e86b9a0d2300bce6755ef796 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 13:23:05 -0500 Subject: [PATCH 28/36] - fixes go error deserialization - moves parsableFactory to serialization package Signed-off-by: Vincent Biret --- abstractions/go/request_adapter.go | 9 +++---- abstractions/go/serialization/parsable.go | 3 +++ http/go/nethttp/nethttp_request_adapter.go | 28 ++++++++++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/abstractions/go/request_adapter.go b/abstractions/go/request_adapter.go index 6a57c58494..7d75e5b799 100644 --- a/abstractions/go/request_adapter.go +++ b/abstractions/go/request_adapter.go @@ -12,18 +12,15 @@ import ( s "github.com/microsoft/kiota/abstractions/go/serialization" ) -// ParsableFactory is a factory for creating Parsable. -type ParsableFactory func() s.Parsable - // ErrorMappings is a mapping of status codes to error types factories. -type ErrorMappings map[string]ParsableFactory +type ErrorMappings map[string]s.ParsableFactory // RequestAdapter is the service responsible for translating abstract RequestInformation into native HTTP requests. type RequestAdapter interface { // SendAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model. - SendAsync(requestInfo RequestInformation, constructor ParsableFactory, responseHandler ResponseHandler, errorMappings ErrorMappings) (s.Parsable, error) + SendAsync(requestInfo RequestInformation, constructor s.ParsableFactory, responseHandler ResponseHandler, errorMappings ErrorMappings) (s.Parsable, error) // SendCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. - SendCollectionAsync(requestInfo RequestInformation, constructor ParsableFactory, responseHandler ResponseHandler, errorMappings ErrorMappings) ([]s.Parsable, error) + SendCollectionAsync(requestInfo RequestInformation, constructor s.ParsableFactory, responseHandler ResponseHandler, errorMappings ErrorMappings) ([]s.Parsable, error) // SendPrimitiveAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model. SendPrimitiveAsync(requestInfo RequestInformation, typeName string, responseHandler ResponseHandler, errorMappings ErrorMappings) (interface{}, error) // SendPrimitiveCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized primitive response model collection. diff --git a/abstractions/go/serialization/parsable.go b/abstractions/go/serialization/parsable.go index 320873c453..3a4ca7696f 100644 --- a/abstractions/go/serialization/parsable.go +++ b/abstractions/go/serialization/parsable.go @@ -13,3 +13,6 @@ type Parsable interface { // IsNil returns whether the current object is nil or not. IsNil() bool } + +// ParsableFactory is a factory for creating Parsable. +type ParsableFactory func() Parsable diff --git a/http/go/nethttp/nethttp_request_adapter.go b/http/go/nethttp/nethttp_request_adapter.go index 416088bea8..cb6c731258 100644 --- a/http/go/nethttp/nethttp_request_adapter.go +++ b/http/go/nethttp/nethttp_request_adapter.go @@ -139,7 +139,7 @@ func (a *NetHttpRequestAdapter) getRequestFromRequestInformation(requestInfo abs } // SendAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model. -func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, constructor abs.ParsableFactory, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) (absser.Parsable, error) { +func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, constructor absser.ParsableFactory, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) (absser.Parsable, error) { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return nil, err @@ -167,7 +167,7 @@ func (a *NetHttpRequestAdapter) SendAsync(requestInfo abs.RequestInformation, co } // SendCollectionAsync executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection. -func (a *NetHttpRequestAdapter) SendCollectionAsync(requestInfo abs.RequestInformation, constructor abs.ParsableFactory, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) ([]absser.Parsable, error) { +func (a *NetHttpRequestAdapter) SendCollectionAsync(requestInfo abs.RequestInformation, constructor absser.ParsableFactory, responseHandler abs.ResponseHandler, errorMappings abs.ErrorMappings) ([]absser.Parsable, error) { response, err := a.getHttpResponseMessage(requestInfo) if err != nil { return nil, err @@ -300,18 +300,32 @@ func (a *NetHttpRequestAdapter) throwFailedResponses(response *nethttp.Response, } statusAsString := strconv.Itoa(response.StatusCode) + var errorCtor absser.ParsableFactory = nil if len(errorMappings) != 0 { if errorMappings[statusAsString] != nil { - return (errorMappings[statusAsString]()).(error) + errorCtor = errorMappings[statusAsString] } else if response.StatusCode >= 400 && response.StatusCode < 500 && errorMappings["4XX"] != nil { - return (errorMappings["4XX"]()).(error) + errorCtor = errorMappings["4XX"] } else if response.StatusCode >= 500 && response.StatusCode < 600 && errorMappings["5XX"] != nil { - return (errorMappings["5XX"]()).(error) + errorCtor = errorMappings["5XX"] } } - return &abs.ApiError{ - Message: "The server returned an unexpected status code and no error factory is registered for this code: " + statusAsString, + if errorCtor == nil { + return &abs.ApiError{ + Message: "The server returned an unexpected status code and no error factory is registered for this code: " + statusAsString, + } + } + + rootNode, err := a.getRootParseNode(response) + if err != nil { + return err + } + + errValue, err := rootNode.GetObjectValue(errorCtor) + if err != nil { + return err } + return errValue.(error) } From 638b949723c1f58125287728488ea9c7138ded02 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 13:32:19 -0500 Subject: [PATCH 29/36] - bumps version numbers for error handling --- abstractions/dotnet/src/Microsoft.Kiota.Abstractions.csproj | 2 +- abstractions/java/lib/build.gradle | 2 +- abstractions/typescript/package-lock.json | 4 ++-- abstractions/typescript/package.json | 2 +- .../src/Microsoft.Kiota.Http.HttpClientLibrary.csproj | 4 ++-- http/java/okhttp/lib/build.gradle | 4 ++-- http/typescript/fetch/package-lock.json | 4 ++-- http/typescript/fetch/package.json | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/abstractions/dotnet/src/Microsoft.Kiota.Abstractions.csproj b/abstractions/dotnet/src/Microsoft.Kiota.Abstractions.csproj index 104f8a5660..a93b15b929 100644 --- a/abstractions/dotnet/src/Microsoft.Kiota.Abstractions.csproj +++ b/abstractions/dotnet/src/Microsoft.Kiota.Abstractions.csproj @@ -6,7 +6,7 @@ latest true https://github.com/microsoft/kiota - 1.0.28 + 1.0.29 true true diff --git a/abstractions/java/lib/build.gradle b/abstractions/java/lib/build.gradle index 305581f03c..458ed41c1a 100644 --- a/abstractions/java/lib/build.gradle +++ b/abstractions/java/lib/build.gradle @@ -48,7 +48,7 @@ publishing { publications { gpr(MavenPublication) { artifactId 'kiota-abstractions' - version '1.0.25' + version '1.0.26' from(components.java) } } diff --git a/abstractions/typescript/package-lock.json b/abstractions/typescript/package-lock.json index 2081e46ea5..2f1a2633f8 100644 --- a/abstractions/typescript/package-lock.json +++ b/abstractions/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/kiota-abstractions", - "version": "1.0.28", + "version": "1.0.29", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/kiota-abstractions", - "version": "1.0.28", + "version": "1.0.29", "license": "MIT", "dependencies": { "tinyduration": "^3.2.2", diff --git a/abstractions/typescript/package.json b/abstractions/typescript/package.json index df0995dadc..7b16aa4878 100644 --- a/abstractions/typescript/package.json +++ b/abstractions/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/kiota-abstractions", - "version": "1.0.28", + "version": "1.0.29", "description": "Core abstractions for kiota generated libraries in TypeScript and JavaScript", "main": "dist/cjs/index.js", "module": "dist/es/index.js", diff --git a/http/dotnet/httpclient/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj b/http/dotnet/httpclient/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj index d7693d5a5f..1b88fb2621 100644 --- a/http/dotnet/httpclient/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj +++ b/http/dotnet/httpclient/src/Microsoft.Kiota.Http.HttpClientLibrary.csproj @@ -6,7 +6,7 @@ latest true https://github.com/microsoft/kiota - 1.0.19 + 1.0.20 true @@ -14,7 +14,7 @@ - + diff --git a/http/java/okhttp/lib/build.gradle b/http/java/okhttp/lib/build.gradle index 4ce4a9fa50..0a6590d943 100644 --- a/http/java/okhttp/lib/build.gradle +++ b/http/java/okhttp/lib/build.gradle @@ -36,7 +36,7 @@ dependencies { // This dependency is used internally, and not exposed to consumers on their own compile classpath. implementation 'com.google.guava:guava:31.0.1-jre' api 'com.squareup.okhttp3:okhttp:4.9.3' - api 'com.microsoft.kiota:kiota-abstractions:1.0.23' + api 'com.microsoft.kiota:kiota-abstractions:1.0.26' } publishing { @@ -53,7 +53,7 @@ publishing { publications { gpr(MavenPublication) { artifactId 'kiota-http-okhttplibrary' - version '1.0.13' + version '1.0.14' from(components.java) } } diff --git a/http/typescript/fetch/package-lock.json b/http/typescript/fetch/package-lock.json index 395d78efe2..4da4498345 100644 --- a/http/typescript/fetch/package-lock.json +++ b/http/typescript/fetch/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/kiota-http-fetchlibrary", - "version": "1.0.15", + "version": "1.0.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@microsoft/kiota-http-fetchlibrary", - "version": "1.0.15", + "version": "1.0.16", "license": "MIT", "dependencies": { "@microsoft/kiota-abstractions": "^1.0.27", diff --git a/http/typescript/fetch/package.json b/http/typescript/fetch/package.json index 10d786bd7d..fac76880bd 100644 --- a/http/typescript/fetch/package.json +++ b/http/typescript/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft/kiota-http-fetchlibrary", - "version": "1.0.15", + "version": "1.0.16", "description": "Kiota request adapter implementation with fetch", "main": "dist/cjs/index.js", "module": "dist/es/index.js", @@ -34,7 +34,7 @@ "registry": "https://npm.pkg.github.com" }, "dependencies": { - "@microsoft/kiota-abstractions": "^1.0.27", + "@microsoft/kiota-abstractions": "^1.0.29", "cross-fetch": "^3.1.5", "tslib": "^2.3.1" }, From 5532a43787f83b573e60c581a7311e2cfac599f4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 13:50:41 -0500 Subject: [PATCH 30/36] - updates documentation for error handling Signed-off-by: Vincent Biret --- docs/extending/kiotaabstractions.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/extending/kiotaabstractions.md b/docs/extending/kiotaabstractions.md index 187afe0bea..ebd694aa49 100644 --- a/docs/extending/kiotaabstractions.md +++ b/docs/extending/kiotaabstractions.md @@ -23,19 +23,23 @@ public interface IRequestAdapter Task SendAsync( RequestInformation requestInfo, - IResponseHandler responseHandler = default) where ModelType : IParsable; + IResponseHandler responseHandler = default, + Dictionary> errorMappings = default) where ModelType : IParsable; Task> SendCollectionAsync( RequestInformation requestInfo, - IResponseHandler responseHandler = default) where ModelType : IParsable; + IResponseHandler responseHandler = default, + Dictionary> errorMappings = default) where ModelType : IParsable; Task SendPrimitiveAsync( RequestInformation requestInfo, - IResponseHandler responseHandler = default); + IResponseHandler responseHandler = default, + Dictionary> errorMappings = default); Task SendNoContentAsync( RequestInformation requestInfo, - IResponseHandler responseHandler = default); + IResponseHandler responseHandler = default, + Dictionary> errorMappings = default); } ``` @@ -69,10 +73,23 @@ When passed to the execution method from the fluent style API, this allows core ```csharp public interface IResponseHandler { - Task HandleResponseAsync(NativeResponseType response); + Task HandleResponseAsync(NativeResponseType response, Dictionary> errorMappings); } ``` +### Failed responses handling + +A Kiota API client will handle failed http responses (status code ∈ [400, 600[) as an exception/error. If error types are described for the operation, Kiota will generate those and attempt to deserialize a failed response to an instance of the corresponding error type with the following sequence: + +1. If the response is successful, deserialize it to the expected model, otherwise move to the next step. +1. If an error factory is registered for the corresponding code (e.g. 403), deserialize to that type and throw, otherwise move to the next step. +1. If an error factory is registered for the error class (e.g. 4XX or 5XX), deserialize to that type and throw, otherwise move to the next step. +1. Throw the generic **ApiException** type defined in the abstractions. + +Additionally all generated error types inherit from the **ApiException** type defined in the abstractions to enable cross cutting implementations and returning an error when no error types are defined for an operation. This type inherits itself from **Exception** (or the native error type on the platform). + +> Note: if a response handler is passed, the error detection logic is bypassed to allow the caller to implement whichever custom handling they desire. + ## Serialization This section provides information about the types offered in the abstractions library the generated result depends on which are related to serializing and deserializing payloads. From 8ce76e0e1937e043727e721a94668b33e9e4cec0 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 13:53:08 -0500 Subject: [PATCH 31/36] - adds changelog entry for error management Signed-off-by: Vincent Biret --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 871911466e..da44f7d58b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Modified the TypeScript RequestInformation content data type to ArrayBuffer. - Updated PHP abstractions to make property keys and values nullable in `SerializationWriter.php`. - Fixed an issue where enum collections parsing would fail in Go. +- Breaking. Kiota clients generate error types and throw when the target API returns a failed response (dotnet, go, java, typescript). #1100 ## [0.0.15] - 2021-12-17 From c9512ab27eb968477cada7f39dd58247c6dd5dc6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 13:59:01 -0500 Subject: [PATCH 32/36] - moves method invocation for clarity Signed-off-by: Vincent Biret --- src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs index 5e139dc92d..558d5a35d8 100644 --- a/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs +++ b/src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs @@ -180,9 +180,9 @@ protected static void ReplaceReservedNames(CodeElement current, IReservedNamesPr !returnType.IsExternal && provider.ReservedNames.Contains(returnType.Name)) returnType.Name = replacement.Invoke(returnType.Name); - ReplaceReservedParameterNamesTypes(currentMethod, provider, replacement); if(currentMethod.ErrorMappings.Values.Select(x => x.Name).Any(x => provider.ReservedNames.Contains(x))) ReplaceErrorMappingNames(currentMethod, provider, replacement); + ReplaceReservedParameterNamesTypes(currentMethod, provider, replacement); } else if (current is CodeProperty currentProperty && isNotInExceptions && shouldReplace && From 3b5f7a336fec53f49b137caf051ba4033bb8f7c2 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Mon, 7 Feb 2022 14:28:48 -0500 Subject: [PATCH 33/36] - fixes error response implementation in java to avoid bypassing native response handler Signed-off-by: Vincent Biret --- .../kiota/http/OkHttpRequestAdapter.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java index e381ca8e37..c91373631e 100644 --- a/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java +++ b/http/java/okhttp/lib/src/main/java/com/microsoft/kiota/http/OkHttpRequestAdapter.java @@ -97,13 +97,15 @@ public CompletableFuture> sendC Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); return this.getHttpResponseMessage(requestInfo) - .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) .thenCompose(response -> { if(responseHandler == null) { try { + this.throwFailedResponse(response, errorMappings); final ParseNode rootNode = getRootParseNode(response); final Iterable result = rootNode.getCollectionOfObjectValues(targetClass); return CompletableFuture.completedStage(result); + } catch(ApiException ex) { + return CompletableFuture.failedFuture(ex); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { @@ -119,13 +121,15 @@ public CompletableFuture sendAsync(@Nonn Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); return this.getHttpResponseMessage(requestInfo) - .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) .thenCompose(response -> { if(responseHandler == null) { try { + this.throwFailedResponse(response, errorMappings); final ParseNode rootNode = getRootParseNode(response); final ModelType result = rootNode.getObjectValue(targetClass); return CompletableFuture.completedStage(result); + } catch(ApiException ex) { + return CompletableFuture.failedFuture(ex); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { @@ -142,10 +146,10 @@ private String getMediaTypeAndSubType(final MediaType mediaType) { @Nonnull public CompletableFuture sendPrimitiveAsync(@Nonnull final RequestInformation requestInfo, @Nonnull final Class targetClass, @Nullable final ResponseHandler responseHandler, @Nullable final HashMap> errorMappings) { return this.getHttpResponseMessage(requestInfo) - .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) .thenCompose(response -> { if(responseHandler == null) { try { + this.throwFailedResponse(response, errorMappings); if(targetClass == Void.class) { return CompletableFuture.completedStage(null); } else { @@ -175,6 +179,8 @@ public CompletableFuture sendPrimitiveAsync(@Nonnull fina } return CompletableFuture.completedStage((ModelType)result); } + } catch(ApiException ex) { + return CompletableFuture.failedFuture(ex); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { @@ -189,13 +195,15 @@ public CompletableFuture> sendPrimitiveCollectio Objects.requireNonNull(requestInfo, "parameter requestInfo cannot be null"); return this.getHttpResponseMessage(requestInfo) - .thenCompose(r -> this.throwFailedResponse(r, errorMappings)) .thenCompose(response -> { if(responseHandler == null) { try { + this.throwFailedResponse(response, errorMappings); final ParseNode rootNode = getRootParseNode(response); final Iterable result = rootNode.getCollectionOfPrimitiveValues(targetClass); return CompletableFuture.completedStage(result); + } catch(ApiException ex) { + return CompletableFuture.failedFuture(ex); } catch(IOException ex) { return CompletableFuture.failedFuture(new RuntimeException("failed to read the response body", ex)); } finally { @@ -213,8 +221,8 @@ private ParseNode getRootParseNode(final Response response) throws IOException { return rootNode; } } - private CompletableFuture throwFailedResponse(final Response response, final HashMap> errorMappings) { - if (response.isSuccessful()) return CompletableFuture.completedFuture(response); + private Response throwFailedResponse(final Response response, final HashMap> errorMappings) throws IOException, ApiException { + if (response.isSuccessful()) return response; final String statusCodeAsString = Integer.toString(response.code()); final Integer statusCode = response.code(); @@ -222,7 +230,7 @@ private CompletableFuture throwFailedResponse(final Response response, !errorMappings.containsKey(statusCodeAsString) && !(statusCode >= 400 && statusCode < 500 && errorMappings.containsKey("4XX")) && !(statusCode >= 500 && statusCode < 600 && errorMappings.containsKey("5XX"))) { - return CompletableFuture.failedFuture(new ApiException("the server returned an unexpected status code and no error class is registered for this code " + statusCode)); + throw new ApiException("the server returned an unexpected status code and no error class is registered for this code " + statusCode); } final Class errorClass = errorMappings.containsKey(statusCodeAsString) ? errorMappings.get(statusCodeAsString) : @@ -232,13 +240,11 @@ private CompletableFuture throwFailedResponse(final Response response, try { final ParseNode rootNode = getRootParseNode(response); final Parsable error = rootNode.getObjectValue(errorClass); - if (error instanceof Exception) { - return CompletableFuture.failedFuture((Exception)error); + if (error instanceof ApiException) { + throw (ApiException)error; } else { - return CompletableFuture.failedFuture(new ApiException("unexpected error type " + error.getClass().getName())); + throw new ApiException("unexpected error type " + error.getClass().getName()); } - } catch (IOException ex) { - return CompletableFuture.failedFuture(ex); } finally { response.close(); } From 5a74e8c27366465cb217b6f67cd60ef72635a1b4 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Wed, 9 Feb 2022 08:19:12 -0500 Subject: [PATCH 34/36] - changes ts api error to inheerit from the base error class Signed-off-by: Vincent Biret --- abstractions/typescript/src/apiError.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/abstractions/typescript/src/apiError.ts b/abstractions/typescript/src/apiError.ts index 5a990f1c83..96ae787ffa 100644 --- a/abstractions/typescript/src/apiError.ts +++ b/abstractions/typescript/src/apiError.ts @@ -1,10 +1,7 @@ /** Parent interface for errors thrown by the client when receiving failed responses to its requests. */ -export class ApiError implements Error { - public name: string; - public message: string; - public stack?: string; +export class ApiError extends Error { public constructor(message?: string) { - this.message = message || ""; - this.name = "ApiError"; + super(message); + Object.setPrototypeOf(this, ApiError.prototype); } } \ No newline at end of file From 9713b9cea6ddbf60ce823425a2146a386a072694 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 10 Feb 2022 13:34:39 -0500 Subject: [PATCH 35/36] - adds error prototype inheritance for typescript Signed-off-by: Vincent Biret --- src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs index 831bc48091..ae8b16d73b 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs @@ -107,6 +107,8 @@ private static void WriteSerializationRegistration(List serializationMod private void WriteConstructorBody(CodeClass parentClass, CodeMethod currentMethod, LanguageWriter writer, bool inherits) { if(inherits || parentClass.IsErrorDefinition) writer.WriteLine("super();"); + if(parentClass.IsErrorDefinition && parentClass.StartBlock is CodeClass.Declaration declaration) + writer.WriteLine($"Object.setPrototypeOf(this, {declaration.Inherits.Name.ToFirstCharacterUpperCase()}.prototype);"); var propertiesWithDefaultValues = new List { CodePropertyKind.AdditionalData, CodePropertyKind.BackingStore, From 992d206f09c90abc91c021aee0d2c48ea58e2e1e Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Thu, 10 Feb 2022 10:35:38 -0800 Subject: [PATCH 36/36] Update abstractions/typescript/src/nativeResponseHandler.ts Co-authored-by: Nikitha Chettiar --- abstractions/typescript/src/nativeResponseHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abstractions/typescript/src/nativeResponseHandler.ts b/abstractions/typescript/src/nativeResponseHandler.ts index 631b044fd6..171a3d1fee 100644 --- a/abstractions/typescript/src/nativeResponseHandler.ts +++ b/abstractions/typescript/src/nativeResponseHandler.ts @@ -5,7 +5,7 @@ import { Parsable } from "./serialization"; export class NativeResponseHandler implements ResponseHandler { /** Native response object as returned by the core service */ public value?: any; - /** The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if an the specific error code is not present. */ + /** The error mappings for the response to use when deserializing failed responses bodies. Where an error code like 401 applies specifically to that status code, a class code like 4XX applies to all status codes within the range if a specific error code is not present. */ public errorMappings: Record Parsable> | undefined public handleResponseAsync( response: NativeResponseType,