diff --git a/it/config.json b/it/config.json index f6e1a2ae70..91f811dbf9 100644 --- a/it/config.json +++ b/it/config.json @@ -229,10 +229,6 @@ }, "apisguru::stripe.com": { "Suppressions": [ - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/5256" - }, { "Language": "java", "Rationale": "https://github.com/microsoft/kiota/issues/2842" diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs index 825c94a1c3..ce50121a46 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs @@ -169,31 +169,10 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken GroupReusableModelsInSingleFile(modelsNamespace); RemoveSelfReferencingUsings(generatedCode); AddAliasToCodeFileUsings(generatedCode); - CorrectSerializerParameters(generatedCode); cancellationToken.ThrowIfCancellationRequested(); }, cancellationToken); } - private static void CorrectSerializerParameters(CodeElement currentElement) - { - if (currentElement is CodeFunction currentFunction && - currentFunction.OriginalLocalMethod.Kind is CodeMethodKind.Serializer) - { - foreach (var parameter in currentFunction.OriginalLocalMethod.Parameters - .Where(p => GetOriginalComposedType(p.Type) is CodeComposedTypeBase composedType && - composedType.IsComposedOfObjectsAndPrimitives(IsPrimitiveType))) - { - var composedType = GetOriginalComposedType(parameter.Type)!; - var newType = (CodeComposedTypeBase)composedType.Clone(); - var nonPrimitiveTypes = composedType.Types.Where(x => !IsPrimitiveType(x, composedType)).ToArray(); - newType.SetTypes(nonPrimitiveTypes); - parameter.Type = newType; - } - } - - CrawlTree(currentElement, CorrectSerializerParameters); - } - private static void AddAliasToCodeFileUsings(CodeElement currentElement) { if (currentElement is CodeFile codeFile) @@ -1483,7 +1462,7 @@ private static void AddDeserializerUsingToDiscriminatorFactoryForComposedTypePar function.AddUsing(new CodeUsing { - Name = modelDeserializerFunction.Parent.Name, + Name = modelDeserializerFunction.Name, Declaration = new CodeType { Name = modelDeserializerFunction.Name, diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs index 5f156ba4df..27e5d4dc08 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs @@ -70,6 +70,11 @@ private void WriteComposedTypeDeserializer(CodeFunction codeElement, LanguageWri if (GetOriginalComposedType(composedParam) is not { } composedType) return; writer.StartBlock("return {"); + if (composedType.Types.Any(x => IsPrimitiveType(x, composedType, false))) + { + var expression = string.Join(" ?? ", composedType.Types.Where(x => IsPrimitiveType(x, composedType, false)).Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeElement.OriginalLocalMethod, composedType.IsCollection)}")); + writer.WriteLine($"\"\" : n => {{ {composedParam.Name.ToFirstCharacterLowerCase()} = {expression}}},"); + } foreach (var mappedType in composedType.Types.Where(x => !IsPrimitiveType(x, composedType, false))) { var functionName = GetDeserializerFunctionName(codeElement, mappedType); @@ -90,10 +95,10 @@ private void WriteComposedTypeSerializer(CodeFunction codeElement, LanguageWrite { var paramName = composedParam.Name.ToFirstCharacterLowerCase(); writer.WriteLine($"if ({paramName} === undefined || {paramName} === null) return;"); - writer.StartBlock($"switch (typeof {paramName}) {{"); + writer.StartBlock($"switch (true) {{"); foreach (var type in composedType.Types.Where(x => IsPrimitiveType(x, composedType, false))) { - WriteCaseStatementForPrimitiveTypeSerialization(type, "key", paramName, codeElement, writer); + WriteCaseStatementForPrimitiveTypeSerialization(type, "undefined", paramName, codeElement, writer); } writer.CloseBlock(); return; @@ -110,7 +115,6 @@ private void WriteComposedTypeSerializer(CodeFunction codeElement, LanguageWrite private void WriteSerializationFunctionForCodeIntersectionType(CodeComposedTypeBase composedType, CodeParameter composedParam, CodeFunction method, LanguageWriter writer) { - // Serialization/Deserialization functions can be called for object types only foreach (var mappedType in composedType.Types.Where(x => !IsPrimitiveType(x, composedType))) { var functionName = GetSerializerFunctionName(method, mappedType); @@ -173,15 +177,18 @@ private void WriteDiscriminatorSwitchBlock(DiscriminatorInformation discriminato writer.CloseBlock(); } - private void WriteCaseStatementForPrimitiveTypeSerialization(CodeTypeBase type, string key, string value, CodeFunction method, LanguageWriter writer) + private void WriteCaseStatementForPrimitiveTypeSerialization(CodeTypeBase type, string key, string modelParamName, CodeFunction method, LanguageWriter writer) { var nodeType = conventions.GetTypeString(type, method, false); var serializationName = GetSerializationMethodName(type, method.OriginalLocalMethod); if (string.IsNullOrEmpty(serializationName) || string.IsNullOrEmpty(nodeType)) return; - writer.StartBlock($"case \"{nodeType}\":"); - writer.WriteLine($"writer.{serializationName}({key}, {value});"); - writer.WriteLine($"break;"); + writer.StartBlock(type.IsCollection + ? $"case Array.isArray({modelParamName}) && ({modelParamName}).every(item => typeof item === '{nodeType}') :" + : $"case typeof {modelParamName} === \"{nodeType}\":"); + + writer.WriteLine($"writer.{serializationName}({key}, {modelParamName} as {conventions.GetTypeString(type, method)});"); + writer.WriteLine("break;"); writer.DecreaseIndent(); } @@ -329,7 +336,10 @@ private string FindFunctionInNameSpace(string functionName, CodeElement codeElem var codeFunction = Array.Find(codeFunctions, func => func.GetImmediateParentOfType().Name == myNamespace.Name) ?? throw new InvalidOperationException($"Function {functionName} not found in namespace {myNamespace.Name}"); - return conventions.GetTypeString(new CodeType { TypeDefinition = codeFunction }, codeElement, false); + + var targetElement = codeElement.GetImmediateParentOfType(); + + return GetTypescriptTypeString(new CodeType { TypeDefinition = codeFunction }, targetElement, includeCollectionInformation: false); } private void WriteSerializerFunction(CodeFunction codeElement, LanguageWriter writer) @@ -432,7 +442,7 @@ private void WriteComposedTypeSwitchClause(CodeComposedTypeBase composedType, Co if (string.IsNullOrEmpty(serializationName) || string.IsNullOrEmpty(nodeType)) return; writer.StartBlock(type.IsCollection - ? $"Array.isArray({modelParamName}.{codePropertyName}) && ({modelParamName}.{codePropertyName}).every(item => typeof item === '{nodeType}') :" + ? $"case Array.isArray({modelParamName}.{codePropertyName}) && ({modelParamName}.{codePropertyName}).every(item => typeof item === '{nodeType}') :" : $"case typeof {modelParamName}.{codePropertyName} === \"{nodeType}\":"); writer.WriteLine($"writer.{serializationName}(\"{codeProperty.WireName}\", {spreadOperator}{modelParamName}.{codePropertyName}{defaultValueSuffix} as {nodeType});"); diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs index ce8ae9769b..c72aae643b 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs @@ -16,7 +16,8 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri ArgumentNullException.ThrowIfNull(writer); if (codeElement.Parent is CodeFunction) return; - var returnType = GetTypescriptTypeString(codeElement.ReturnType, codeElement, inlineComposedTypeString: true); + var codeFile = codeElement.GetImmediateParentOfType(); + var returnType = GetTypescriptTypeString(codeElement.ReturnType, codeFile, inlineComposedTypeString: true); var isVoid = "void".EqualsIgnoreCase(returnType); WriteMethodDocumentation(codeElement, writer, isVoid); WriteMethodPrototype(codeElement, writer, returnType, isVoid); @@ -30,6 +31,7 @@ private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer, bo } internal static void WriteMethodDocumentationInternal(CodeMethod code, LanguageWriter writer, bool isVoid, TypeScriptConventionService typeScriptConventionService) { + var codeFile = code.GetImmediateParentOfType(); var returnRemark = (isVoid, code.IsAsync) switch { (true, _) => string.Empty, @@ -41,7 +43,7 @@ internal static void WriteMethodDocumentationInternal(CodeMethod code, LanguageW code.Parameters .Where(static x => x.Documentation.DescriptionAvailable) .OrderBy(static x => x.Name) - .Select(x => $"@param {x.Name} {x.Documentation.GetDescription(type => GetTypescriptTypeString(type, code, inlineComposedTypeString: true), ReferenceTypePrefix, ReferenceTypeSuffix, RemoveInvalidDescriptionCharacters)}") + .Select(x => $"@param {x.Name} {x.Documentation.GetDescription(type => GetTypescriptTypeString(type, codeFile, inlineComposedTypeString: true), ReferenceTypePrefix, ReferenceTypeSuffix, RemoveInvalidDescriptionCharacters)}") .Union([returnRemark]) .Union(GetThrownExceptionsRemarks(code))); } diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs index ebb98ab309..7d5da7fd9e 100644 --- a/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs @@ -81,19 +81,32 @@ public override string GetAccessModifier(AccessModifier access) }; } - private static bool ShouldIncludeCollectionInformationForParameter(CodeParameter parameter) - { - return !(GetOriginalComposedType(parameter) is not null - && parameter.Parent is CodeMethod codeMethod - && (codeMethod.IsOfKind(CodeMethodKind.Serializer) || codeMethod.IsOfKind(CodeMethodKind.Deserializer))); - } - public override string GetParameterSignature(CodeParameter parameter, CodeElement targetElement, LanguageWriter? writer = null) { ArgumentNullException.ThrowIfNull(parameter); - var includeCollectionInformation = ShouldIncludeCollectionInformationForParameter(parameter); - var paramType = GetTypescriptTypeString(parameter.Type, targetElement, includeCollectionInformation: includeCollectionInformation, inlineComposedTypeString: true); - var isComposedOfPrimitives = GetOriginalComposedType(parameter.Type) is CodeComposedTypeBase composedType && composedType.IsComposedOfPrimitives(IsPrimitiveType); + var composedType = GetOriginalComposedType(parameter.Type); + var paramType = GetTypescriptTypeString(parameter.Type, targetElement, includeCollectionInformation: true, inlineComposedTypeString: true); + + if (composedType != null && parameter.Parent is CodeMethod cm && cm.IsOfKind(CodeMethodKind.Serializer)) + { + // eliminate primitive types from serializers with composed type signature + var newType = (CodeComposedTypeBase)composedType.Clone(); + var nonPrimitiveTypes = composedType.Types.Where(x => !IsPrimitiveTypeOrPrimitiveCollection(x, composedType)).ToArray(); + newType.SetTypes(nonPrimitiveTypes); + paramType = GetTypescriptTypeString(newType, targetElement, includeCollectionInformation: true, inlineComposedTypeString: true); + } + var isComposedOfPrimitives = composedType != null && composedType.IsComposedOfPrimitives(IsPrimitiveType); + + // add a 'Parsable' type to the parameter if it is composed of non-Parsable types + var parsableTypes = ( + composedType != null, + parameter.Parent is CodeMethod method && (method.IsOfKind(CodeMethodKind.Deserializer) || method.IsOfKind(CodeMethodKind.Serializer)) + ) switch + { + (true, true) => "Parsable | ", + _ => string.Empty, + }; + var defaultValueSuffix = (string.IsNullOrEmpty(parameter.DefaultValue), parameter.Kind, isComposedOfPrimitives) switch { (false, CodeParameterKind.DeserializationTarget, false) when parameter.Parent is CodeMethod codeMethod && codeMethod.Kind is CodeMethodKind.Serializer @@ -107,7 +120,7 @@ public override string GetParameterSignature(CodeParameter parameter, CodeElemen (false, CodeParameterKind.DeserializationTarget) => ("Partial<", ">"), _ => (string.Empty, string.Empty), }; - return $"{parameter.Name.ToFirstCharacterLowerCase()}{(parameter.Optional && parameter.Type.IsNullable ? "?" : string.Empty)}: {partialPrefix}{paramType}{partialSuffix}{(parameter.Type.IsNullable ? " | undefined" : string.Empty)}{defaultValueSuffix}"; + return $"{parameter.Name.ToFirstCharacterLowerCase()}{(parameter.Optional && parameter.Type.IsNullable ? "?" : string.Empty)}: {partialPrefix}{parsableTypes}{paramType}{partialSuffix}{(parameter.Type.IsNullable ? " | undefined" : string.Empty)}{defaultValueSuffix}"; } public override string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true, LanguageWriter? writer = null) @@ -227,6 +240,8 @@ TYPE_LOWERCASE_BOOLEAN or public static bool IsPrimitiveType(CodeType codeType, CodeComposedTypeBase codeComposedTypeBase) => IsPrimitiveType(codeType, codeComposedTypeBase, true); + public static bool IsPrimitiveTypeOrPrimitiveCollection(CodeType codeType, CodeComposedTypeBase codeComposedTypeBase) => IsPrimitiveType(codeType, codeComposedTypeBase, false); + public static bool IsPrimitiveType(CodeType codeType, CodeComposedTypeBase codeComposedTypeBase, bool includeCollectionInformation) => IsPrimitiveType(GetTypescriptTypeString(codeType, codeComposedTypeBase, includeCollectionInformation)); internal static string RemoveInvalidDescriptionCharacters(string originalDescription) => originalDescription?.Replace("\\", "/", StringComparison.OrdinalIgnoreCase) ?? string.Empty; diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs index b2894a186a..bc5b83d515 100644 --- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs @@ -1252,9 +1252,9 @@ public async Task Writes_UnionOfPrimitiveValues_SerializerFunctionAsync() writer.Write(serializerFunction); var serializerFunctionStr = tw.ToString(); Assert.Contains("return", serializerFunctionStr); - Assert.Contains("switch", serializerFunctionStr); - Assert.Contains("case \"number\":", serializerFunctionStr); - Assert.Contains("case \"string\":", serializerFunctionStr); + Assert.Contains("switch (true)", serializerFunctionStr); + Assert.Contains("case typeof primitives === \"number\":", serializerFunctionStr); + Assert.Contains("case typeof primitives === \"string\":", serializerFunctionStr); Assert.Contains("break", serializerFunctionStr); AssertExtensions.CurlyBracesAreClosed(serializerFunctionStr, 1); }