Skip to content

Commit

Permalink
- implements error mapping in typescript code-generation
Browse files Browse the repository at this point in the history
Signed-off-by: Vincent Biret <vibiret@microsoft.com>
  • Loading branch information
baywet committed Feb 4, 2022
1 parent 296965f commit 238338f
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 20 deletions.
18 changes: 8 additions & 10 deletions abstractions/typescript/src/apiError.ts
Original file line number Diff line number Diff line change
@@ -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;
export class ApiError implements Error {
public name: string;
public message: string;
public stack?: string;
public constructor(message?: string) {
this.message = message || "";
}
}
10 changes: 9 additions & 1 deletion src/Kiota.Builder/Refiners/CommonLanguageRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down Expand Up @@ -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<string, string> 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<string, string> replacement)
{
currentMethod.Parameters.Where(x => x.Type is CodeType parameterType &&
Expand Down
5 changes: 5 additions & 0 deletions src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class TypeScriptReservedNamesProvider : IReservedNamesProvider {
"do",
"else",
"enum",
"error",
"export",
"extends",
"false",
Expand All @@ -24,7 +25,7 @@ public class TypeScriptReservedNamesProvider : IReservedNamesProvider {
"If",
"import",
"in",
"istanceOf",
"instanceOf",
"new",
"null",
"return",
Expand Down
23 changes: 16 additions & 7 deletions src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -105,7 +105,7 @@ private static void WriteSerializationRegistration(List<string> 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> {
CodePropertyKind.AdditionalData,
Expand Down Expand Up @@ -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<string, (item: T, node: {localConventions.ParseNodeInterfaceName}) => void>([{(inherits ? $"...super.{codeElement.Name.ToFirstCharacterLowerCase()}()," : string.Empty)}");
writer.IncreaseIndent();
var parentClassName = parentClass.Name.ToFirstCharacterUpperCase();
Expand Down Expand Up @@ -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<string, new () => 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;
Expand All @@ -228,7 +237,7 @@ private void WriteRequestGeneratorBody(CodeMethod codeElement, RequestParams req
$"{RequestInfoVarName}.pathParameters = {GetPropertyCall(urlTemplateParamsProperty, "''")};",
$"{RequestInfoVarName}.httpMethod = HttpMethod.{codeElement.HttpMethod.ToString().ToUpperInvariant()};");
if(requestParams.headers != null)
writer.WriteLine($"{RequestInfoVarName}.headers = h;");
writer.WriteLine($"if(h) {RequestInfoVarName}.headers = h;");
if(requestParams.queryString != null)
writer.WriteLines($"{requestParams.queryString.Name} && {RequestInfoVarName}.setQueryStringParametersFromRawObject(q);");
if(requestParams.requestBody != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Linq;
using Xunit;

Expand All @@ -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<InvalidOperationException>(() => 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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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"
};
Expand Down Expand Up @@ -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<string, new () => 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<string, new () => Parsable> =", result);
Assert.Contains("undefined", result);
AssertExtensions.CurlyBracesAreClosed(result);
}
[Fact]
public void WritesRequestExecutorBodyForCollections() {
method.MethodKind = CodeMethodKind.RequestExecutor;
method.HttpMethod = HttpMethod.Get;
Expand Down

0 comments on commit 238338f

Please sign in to comment.