diff --git a/it/config.json b/it/config.json index 13468a0cde..588285507a 100644 --- a/it/config.json +++ b/it/config.json @@ -43,10 +43,6 @@ } ], "Suppressions": [ - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" - }, { "Language": "ruby", "Rationale": "https://github.com/microsoft/kiota/issues/1816" @@ -156,10 +152,6 @@ "Language": "go", "Rationale": "https://github.com/microsoft/kiota/issues/3436" }, - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" - }, { "Language": "ruby", "Rationale": "https://github.com/microsoft/kiota/issues/2484" @@ -170,10 +162,6 @@ } ], "IdempotencySuppressions": [ - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" - } ] }, "apisguru::twilio.com:api": { @@ -248,7 +236,7 @@ "Suppressions": [ { "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" + "Rationale": "https://github.com/microsoft/kiota/issues/5256" }, { "Language": "go", @@ -280,10 +268,6 @@ "Language": "python", "Rationale": "https://github.com/microsoft/kiota/issues/2842" }, - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" - }, { "Language": "ruby", "Rationale": "https://github.com/microsoft/kiota/issues/1816" @@ -312,20 +296,12 @@ }, "apisguru::twitter.com:current": { "Suppressions": [ - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" - }, { "Language": "ruby", "Rationale": "https://github.com/microsoft/kiota/issues/1816" } ], "IdempotencySuppressions": [ - { - "Language": "typescript", - "Rationale": "https://github.com/microsoft/kiota/issues/1812" - } ] }, "apisguru::apis.guru": { diff --git a/src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs b/src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs index aa600049a0..d2b5241d37 100644 --- a/src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs +++ b/src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using static Kiota.Builder.Writers.TypeScript.TypeScriptConventionService; namespace Kiota.Builder.CodeDOM; /// @@ -25,6 +26,13 @@ public bool ContainsType(CodeType codeType) ArgumentNullException.ThrowIfNull(codeType); return types.ContainsKey(NormalizeKey(codeType)); } + public void SetTypes(params CodeType[] codeTypes) + { + ArgumentNullException.ThrowIfNull(codeTypes); + types.Clear(); + foreach (var codeType in codeTypes) + AddType(codeType); + } private readonly ConcurrentDictionary types = new(StringComparer.OrdinalIgnoreCase); public IEnumerable Types { @@ -66,9 +74,18 @@ public CodeNamespace? TargetNamespace { get; set; } + public DeprecationInformation? Deprecation { get; set; } + + public bool IsComposedOfPrimitives(Func checkIfPrimitive) => Types.All(x => checkIfPrimitive(x, this)); + public bool IsComposedOfObjectsAndPrimitives(Func checkIfPrimitive) + { + // Count the number of primitives in Types + return Types.Any(x => checkIfPrimitive(x, this)) && Types.Any(x => !checkIfPrimitive(x, this)); + } + } diff --git a/src/Kiota.Builder/CodeDOM/CodeFile.cs b/src/Kiota.Builder/CodeDOM/CodeFile.cs index f823c8e8cd..6652799d6e 100644 --- a/src/Kiota.Builder/CodeDOM/CodeFile.cs +++ b/src/Kiota.Builder/CodeDOM/CodeFile.cs @@ -24,7 +24,9 @@ public IEnumerable AddElements(params T[] elements) where T : CodeElement public IEnumerable AllUsingsFromChildElements => GetChildElements(true) .SelectMany(static x => x.GetChildElements(false)) .OfType() - .SelectMany(static x => x.Usings); + .SelectMany(static x => x.Usings) + .Union(GetChildElements(true).Where(static x => x is CodeConstant).Cast() + .SelectMany(static x => x.StartBlock.Usings)); } public class CodeFileDeclaration : ProprietableBlockDeclaration { diff --git a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs index 647dd84f2d..ebf8e8a5b8 100644 --- a/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs +++ b/src/Kiota.Builder/Refiners/TypeScriptRefiner.cs @@ -6,11 +6,13 @@ using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; +using static Kiota.Builder.Writers.TypeScript.TypeScriptConventionService; namespace Kiota.Builder.Refiners; public class TypeScriptRefiner : CommonLanguageRefiner, ILanguageRefiner { public static readonly string BackingStoreEnabledKey = "backingStoreEnabled"; + public TypeScriptRefiner(GenerationConfiguration configuration) : base(configuration) { } public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken cancellationToken) { @@ -19,6 +21,16 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken cancellationToken.ThrowIfCancellationRequested(); DeduplicateErrorMappings(generatedCode); RemoveMethodByKind(generatedCode, CodeMethodKind.RawUrlConstructor, CodeMethodKind.RawUrlBuilder); + // Invoke the ConvertUnionTypesToWrapper method to maintain a consistent CodeDOM structure. + // Note that in the later stages, specifically within the GenerateModelCodeFile() function, the introduced wrapper interface is disregarded. + // Instead, a ComposedType is created, which has its own writer, along with the associated Factory, Serializer, and Deserializer functions + // that are incorporated into the CodeFile. + ConvertUnionTypesToWrapper( + generatedCode, + _configuration.UsesBackingStore, + s => s.ToFirstCharacterLowerCase(), + false + ); ReplaceReservedNames(generatedCode, new TypeScriptReservedNamesProvider(), static x => $"{x}Escaped"); ReplaceReservedExceptionPropertyNames(generatedCode, new TypeScriptExceptionsReservedNamesProvider(), static x => $"{x}Escaped"); MoveRequestBuilderPropertiesToBaseType(generatedCode, @@ -156,9 +168,63 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken GenerateRequestBuilderCodeFiles(modelsNamespace); 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) + { + var enumeratedUsings = codeFile.GetChildElements(true).SelectMany(GetUsingsFromCodeElement).ToArray(); + var duplicatedUsings = enumeratedUsings.Where(static x => !x.IsExternal) + .Where(static x => x.Declaration != null && x.Declaration.TypeDefinition != null) + .Where(static x => string.IsNullOrEmpty(x.Alias)) + .GroupBy(static x => x.Declaration!.Name, StringComparer.OrdinalIgnoreCase) + .Where(static x => x.Count() > 1) + .Where(static x => x.DistinctBy(static y => y.Declaration!.TypeDefinition!.GetImmediateParentOfType()) + .Count() > 1) + .SelectMany(static x => x) + .ToArray(); + + if (duplicatedUsings.Length > 0) + foreach (var usingElement in duplicatedUsings) + usingElement.Alias = (usingElement.Declaration + ?.TypeDefinition + ?.GetImmediateParentOfType() + .Name + + usingElement.Declaration + ?.TypeDefinition + ?.Name.ToFirstCharacterUpperCase()) + .GetNamespaceImportSymbol() + .ToFirstCharacterUpperCase(); + } + + CrawlTree(currentElement, AddAliasToCodeFileUsings); + } + private static void GenerateEnumObjects(CodeElement currentElement) { AddEnumObject(currentElement); @@ -201,13 +267,10 @@ private static void GenerateRequestBuilderCodeFiles(CodeNamespace modelsNamespac if (modelsNamespace.Parent is not CodeNamespace mainNamespace) return; var elementsToConsider = mainNamespace.Namespaces.Except([modelsNamespace]).OfType().Union(mainNamespace.Classes).ToArray(); foreach (var element in elementsToConsider) - { GenerateRequestBuilderCodeFilesForElement(element); - } + foreach (var element in elementsToConsider) - {// in two separate loops to ensure all the constants are added before the usings are added AddDownwardsConstantsImports(element); - } } private static void GenerateRequestBuilderCodeFilesForElement(CodeElement currentElement) { @@ -240,20 +303,165 @@ parentNamespace.Parent is CodeNamespace parentLevelNamespace && private static CodeFile? GenerateModelCodeFile(CodeInterface codeInterface, CodeNamespace codeNamespace) { - var functions = codeNamespace.GetChildElements(true).OfType().Where(codeFunction => - codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Deserializer or CodeMethodKind.Serializer && - codeFunction.OriginalLocalMethod.Parameters - .Any(x => x.Type.Name.Equals(codeInterface.Name, StringComparison.OrdinalIgnoreCase)) || - - codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Factory && - codeInterface.Name.EqualsIgnoreCase(codeFunction.OriginalMethodParentClass.Name) && - codeFunction.OriginalMethodParentClass.IsChildOf(codeNamespace) - ).ToArray(); + var functions = GetSerializationAndFactoryFunctions(codeInterface, codeNamespace).ToArray(); if (functions.Length == 0) return null; - return codeNamespace.TryAddCodeFile(codeInterface.Name, [codeInterface, .. functions]); + + var composedType = GetOriginalComposedType(codeInterface); + var elements = composedType is null ? new List { codeInterface }.Concat(functions) : GetCodeFileElementsForComposedType(codeInterface, codeNamespace, composedType, functions); + + return codeNamespace.TryAddCodeFile(codeInterface.Name, elements.ToArray()); + } + + private static IEnumerable GetSerializationAndFactoryFunctions(CodeInterface codeInterface, CodeNamespace codeNamespace) + { + return codeNamespace.GetChildElements(true) + .OfType() + .Where(codeFunction => + IsDeserializerOrSerializerFunction(codeFunction, codeInterface) || + IsFactoryFunction(codeFunction, codeInterface, codeNamespace)); + } + + private static bool IsDeserializerOrSerializerFunction(CodeFunction codeFunction, CodeInterface codeInterface) + { + return codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Deserializer or CodeMethodKind.Serializer && + codeFunction.OriginalLocalMethod.Parameters.Any(x => x.Type is CodeType codeType && codeType.TypeDefinition == codeInterface); + } + + private static bool IsFactoryFunction(CodeFunction codeFunction, CodeInterface codeInterface, CodeNamespace codeNamespace) + { + return codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Factory && + codeInterface.Name.EqualsIgnoreCase(codeFunction.OriginalMethodParentClass.Name) && + codeFunction.OriginalMethodParentClass.IsChildOf(codeNamespace); + } + + private static List GetCodeFileElementsForComposedType(CodeInterface codeInterface, CodeNamespace codeNamespace, CodeComposedTypeBase composedType, CodeFunction[] functions) + { + var children = new List(functions) + { + // Add the composed type, The writer will output the composed type as a type definition e.g export type Pet = Cat | Dog + composedType + }; + + ReplaceFactoryMethodForComposedType(composedType, children); + ReplaceSerializerMethodForComposedType(composedType, children); + ReplaceDeserializerMethodForComposedType(codeInterface, codeNamespace, composedType, children); + + return children; + } + + private static CodeFunction? FindFunctionOfKind(List elements, CodeMethodKind kind) + { + return elements.OfType().FirstOrDefault(function => function.OriginalLocalMethod.IsOfKind(kind)); + } + + private static void RemoveUnusedDeserializerImport(List children, CodeFunction factoryFunction) + { + if (FindFunctionOfKind(children, CodeMethodKind.Deserializer) is { } deserializerMethod) + factoryFunction.RemoveUsingsByDeclarationName(deserializerMethod.Name); + } + + private static void ReplaceFactoryMethodForComposedType(CodeComposedTypeBase composedType, List children) + { + if (composedType is null || FindFunctionOfKind(children, CodeMethodKind.Factory) is not { } function) return; + + if (composedType.IsComposedOfPrimitives(IsPrimitiveType)) + { + function.OriginalLocalMethod.ReturnType = composedType; + // Remove the deserializer import statement if its not being used + RemoveUnusedDeserializerImport(children, function); + } + } + + private static void ReplaceSerializerMethodForComposedType(CodeComposedTypeBase composedType, List children) + { + if (FindFunctionOfKind(children, CodeMethodKind.Serializer) is not { } function) return; + + // Add the key parameter if the composed type is a union of primitive values + if (composedType.IsComposedOfPrimitives(IsPrimitiveType)) + function.OriginalLocalMethod.AddParameter(CreateKeyParameter()); + + // Add code usings for each individual item since the functions can be invoked to serialize/deserialize the contained classes/interfaces + AddSerializationUsingsForCodeComposed(composedType, function, CodeMethodKind.Serializer); + } + + private static void AddSerializationUsingsForCodeComposed(CodeComposedTypeBase composedType, CodeFunction function, CodeMethodKind kind) + { + // Add code usings for each individual item since the functions can be invoked to serialize/deserialize the contained classes/interfaces + foreach (var codeClass in composedType.Types.Where(x => !IsPrimitiveType(x, composedType)) + .Select(static x => x.TypeDefinition) + .OfType() + .Select(static x => x.OriginalClass) + .OfType()) + { + var (serializer, deserializer) = GetSerializationFunctionsForNamespace(codeClass); + if (kind == CodeMethodKind.Serializer) + AddSerializationUsingsToFunction(function, serializer); + if (kind == CodeMethodKind.Deserializer) + AddSerializationUsingsToFunction(function, deserializer); + } + } + + private static void AddSerializationUsingsToFunction(CodeFunction function, CodeFunction serializationFunction) + { + if (serializationFunction.Parent is not null) + { + function.AddUsing(new CodeUsing + { + Name = serializationFunction.Parent.Name, + Declaration = new CodeType + { + Name = serializationFunction.Name, + TypeDefinition = serializationFunction + } + }); + } + } + + private static void ReplaceDeserializerMethodForComposedType(CodeInterface codeInterface, CodeNamespace codeNamespace, CodeComposedTypeBase composedType, List children) + { + if (FindFunctionOfKind(children, CodeMethodKind.Deserializer) is not { } deserializerMethod) return; + + // Deserializer function is not required for primitive values + if (composedType.IsComposedOfPrimitives(IsPrimitiveType)) + { + children.Remove(deserializerMethod); + codeInterface.RemoveChildElement(deserializerMethod); + codeNamespace.RemoveChildElement(deserializerMethod); + } + + // Add code usings for each individual item since the functions can be invoked to serialize/deserialize the contained classes/interfaces + AddSerializationUsingsForCodeComposed(composedType, deserializerMethod, CodeMethodKind.Deserializer); + } + + private static CodeParameter CreateKeyParameter() + { + return new CodeParameter + { + Name = "key", + Type = new CodeType { Name = "string", IsExternal = true, IsNullable = false }, + Optional = false, + Documentation = new() + { + DescriptionTemplate = "The name of the property to write in the serialization.", + }, + }; + } + + public static CodeComposedTypeBase? GetOriginalComposedType(CodeElement element) + { + return element switch + { + CodeParameter param => GetOriginalComposedType(param.Type), + CodeType codeType when codeType.TypeDefinition is not null => GetOriginalComposedType(codeType.TypeDefinition), + CodeClass codeClass => codeClass.OriginalComposedType, + CodeInterface codeInterface => codeInterface.OriginalClass.OriginalComposedType, + CodeComposedTypeBase composedType => composedType, + _ => null, + }; } + private static readonly CodeUsing[] navigationMetadataUsings = [ new CodeUsing { @@ -379,7 +587,7 @@ private static void GenerateRequestBuilderCodeFile(CodeInterface codeInterface, codeNamespace.TryAddCodeFile(codeInterface.Name, elements); } - private static IEnumerable GetUsingsFromCodeElement(CodeElement codeElement) + public static IEnumerable GetUsingsFromCodeElement(CodeElement codeElement) { return codeElement switch { @@ -729,9 +937,9 @@ private static void AddInterfaceParamToSerializer(CodeInterface modelInterface, method.AddParameter(new CodeParameter { - Name = ReturnFinalInterfaceName(modelInterface), + Name = GetFinalInterfaceName(modelInterface), DefaultValue = "{}", - Type = new CodeType { Name = ReturnFinalInterfaceName(modelInterface), TypeDefinition = modelInterface }, + Type = new CodeType { Name = GetFinalInterfaceName(modelInterface), TypeDefinition = modelInterface }, Kind = CodeParameterKind.DeserializationTarget, }); @@ -742,7 +950,7 @@ private static void AddInterfaceParamToSerializer(CodeInterface modelInterface, Name = modelInterface.Parent.Name, Declaration = new CodeType { - Name = ReturnFinalInterfaceName(modelInterface), + Name = GetFinalInterfaceName(modelInterface), TypeDefinition = modelInterface } }); @@ -773,7 +981,7 @@ private static void RenameModelInterfacesAndRemoveClasses(CodeElement currentEle { if (currentElement is CodeInterface modelInterface && modelInterface.IsOfKind(CodeInterfaceKind.Model) && modelInterface.Parent is CodeNamespace parentNS) { - var finalName = ReturnFinalInterfaceName(modelInterface); + var finalName = GetFinalInterfaceName(modelInterface); if (!finalName.Equals(modelInterface.Name, StringComparison.Ordinal)) { if (parentNS.FindChildByName(finalName, false) is CodeClass existingClassToRemove) @@ -793,11 +1001,11 @@ private static void RenameCodeInterfaceParamsInSerializers(CodeFunction codeFunc { if (codeFunction.OriginalLocalMethod.Parameters.FirstOrDefault(static x => x.Type is CodeType codeType && codeType.TypeDefinition is CodeInterface) is CodeParameter param && param.Type is CodeType codeType && codeType.TypeDefinition is CodeInterface paramInterface) { - param.Name = ReturnFinalInterfaceName(paramInterface); + param.Name = GetFinalInterfaceName(paramInterface); } } - private static string ReturnFinalInterfaceName(CodeInterface codeInterface) + private static string GetFinalInterfaceName(CodeInterface codeInterface) { return codeInterface.OriginalClass.Name.ToFirstCharacterUpperCase(); } @@ -917,7 +1125,7 @@ private static void SetTypeAsModelInterface(CodeInterface interfaceElement, Code Name = interfaceElement.Name, TypeDefinition = interfaceElement, }; - requestBuilder.RemoveUsingsByDeclarationName(ReturnFinalInterfaceName(interfaceElement)); + requestBuilder.RemoveUsingsByDeclarationName(GetFinalInterfaceName(interfaceElement)); if (!requestBuilder.Usings.Any(x => x.Declaration?.TypeDefinition == elemType.TypeDefinition)) { requestBuilder.AddUsing(new CodeUsing @@ -961,7 +1169,7 @@ private static CodeInterface CreateModelInterface(CodeClass modelClass, Func interfaceNamingCallback) + { + // If the property is a composed type then add code using for each of the classes contained in the composed type + if (GetOriginalComposedType(propertyType) is not { } composedTypeProperty) return; + foreach (var composedType in composedTypeProperty.AllTypes) + { + if (composedType.TypeDefinition is CodeClass composedTypePropertyClass) + { + var composedTypePropertyInterfaceTypeAndUsing = GetUpdatedModelInterfaceAndCodeUsing(composedTypePropertyClass, composedType, interfaceNamingCallback); + SetUsingInModelInterface(modelInterface, composedTypePropertyInterfaceTypeAndUsing); + } + } + } + private static void ProcessModelClassProperties(CodeClass modelClass, CodeInterface modelInterface, IEnumerable properties, Func interfaceNamingCallback) { /* @@ -1161,7 +1383,9 @@ private static void ProcessModelClassProperties(CodeClass modelClass, CodeInterf } else if (mProp.Type is CodeType propertyType && propertyType.TypeDefinition is CodeClass propertyClass) { - var interfaceTypeAndUsing = ReturnUpdatedModelInterfaceTypeAndUsing(propertyClass, propertyType, interfaceNamingCallback); + AddCodeUsingForComposedTypeProperty(propertyType, modelInterface, interfaceNamingCallback); + + var interfaceTypeAndUsing = GetUpdatedModelInterfaceAndCodeUsing(propertyClass, propertyType, interfaceNamingCallback); SetUsingInModelInterface(modelInterface, interfaceTypeAndUsing); // In case of a serializer function, the object serializer function will hold reference to serializer function of the property type. @@ -1180,7 +1404,7 @@ private static void ProcessModelClassProperties(CodeClass modelClass, CodeInterf private const string FactorySuffix = "FromDiscriminatorValue"; private static string GetFactoryFunctionNameFromTypeName(string? typeName) => string.IsNullOrEmpty(typeName) ? string.Empty : $"{FactoryPrefix}{typeName.ToFirstCharacterUpperCase()}{FactorySuffix}"; - private static (CodeInterface?, CodeUsing?) ReturnUpdatedModelInterfaceTypeAndUsing(CodeClass sourceClass, CodeType originalType, Func interfaceNamingCallback) + private static (CodeInterface?, CodeUsing?) GetUpdatedModelInterfaceAndCodeUsing(CodeClass sourceClass, CodeType originalType, Func interfaceNamingCallback) { var propertyInterfaceType = CreateModelInterface(sourceClass, interfaceNamingCallback); if (propertyInterfaceType.Parent is null) @@ -1230,8 +1454,45 @@ private static void AddPropertyFactoryUsingToDeserializer(CodeFunction codeFunct } } + /// + /// Adds all the required import statements (CodeUsings) to the deserialization function which have a dependency on ComposedTypes. + /// Composed types can be comprised of other interfaces/classes. + /// + /// The code element to process. + private static void AddDeserializerUsingToDiscriminatorFactoryForComposedTypeParameters(CodeElement codeElement) + { + if (codeElement is not CodeFunction function) return; + + var composedTypeParam = function.OriginalLocalMethod.Parameters + .FirstOrDefault(x => GetOriginalComposedType(x) is not null); + + if (composedTypeParam is null) return; + + var composedType = GetOriginalComposedType(composedTypeParam); + if (composedType is null) return; + + foreach (var type in composedType.AllTypes) + { + if (type.TypeDefinition is not CodeInterface codeInterface) continue; + + var modelDeserializerFunction = GetSerializationFunctionsForNamespace(codeInterface.OriginalClass).Item2; + if (modelDeserializerFunction.Parent is null) continue; + + function.AddUsing(new CodeUsing + { + Name = modelDeserializerFunction.Parent.Name, + Declaration = new CodeType + { + Name = modelDeserializerFunction.Name, + TypeDefinition = modelDeserializerFunction + }, + }); + } + } + private static void AddDeserializerUsingToDiscriminatorFactory(CodeElement codeElement) { + AddDeserializerUsingToDiscriminatorFactoryForComposedTypeParameters(codeElement); if (codeElement is CodeFunction parsableFactoryFunction && parsableFactoryFunction.OriginalLocalMethod.IsOfKind(CodeMethodKind.Factory) && parsableFactoryFunction.OriginalLocalMethod?.ReturnType is CodeType codeType && codeType.TypeDefinition is CodeClass modelReturnClass) { @@ -1269,7 +1530,6 @@ private static void AddDeserializerUsingToDiscriminatorFactory(CodeElement codeE } } } - } CrawlTree(codeElement, AddDeserializerUsingToDiscriminatorFactory); } diff --git a/src/Kiota.Builder/Writers/LanguageWriter.cs b/src/Kiota.Builder/Writers/LanguageWriter.cs index 54ece0b814..3a4a8886e0 100644 --- a/src/Kiota.Builder/Writers/LanguageWriter.cs +++ b/src/Kiota.Builder/Writers/LanguageWriter.cs @@ -158,6 +158,12 @@ public void Write(T code) where T : CodeElement case CodeConstant codeConstant: ((ICodeElementWriter)elementWriter).WriteCodeElement(codeConstant, this); break; + case CodeUnionType codeUnionType: + ((ICodeElementWriter)elementWriter).WriteCodeElement(codeUnionType, this); + break; + case CodeIntersectionType codeIntersectionType: + ((ICodeElementWriter)elementWriter).WriteCodeElement(codeIntersectionType, this); + break; } else if (code is not CodeClass && code is not BlockDeclaration && diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeComposedTypeBaseWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeComposedTypeBaseWriter.cs new file mode 100644 index 0000000000..5dc9ea85f0 --- /dev/null +++ b/src/Kiota.Builder/Writers/TypeScript/CodeComposedTypeBaseWriter.cs @@ -0,0 +1,27 @@ +using System; +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Extensions; + +namespace Kiota.Builder.Writers.TypeScript; + +public abstract class CodeComposedTypeBaseWriter(TypeScriptConventionService conventionService) : BaseElementWriter(conventionService) where TCodeComposedTypeBase : CodeComposedTypeBase +{ + public abstract string TypesDelimiter + { + get; + } + + public override void WriteCodeElement(TCodeComposedTypeBase codeElement, LanguageWriter writer) + { + ArgumentNullException.ThrowIfNull(codeElement); + ArgumentNullException.ThrowIfNull(writer); + + if (!codeElement.Types.Any()) + throw new InvalidOperationException("CodeComposedTypeBase should be comprised of one or more types."); + + var codeUnionString = string.Join($" {TypesDelimiter} ", codeElement.Types.Select(x => conventions.GetTypeString(x, codeElement))); + + writer.WriteLine($"export type {codeElement.Name.ToFirstCharacterUpperCase()} = {codeUnionString};"); + } +} diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs index 9a51721846..9e4cda57f8 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs @@ -2,6 +2,7 @@ using System.Linq; using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; +using static Kiota.Builder.Writers.TypeScript.TypeScriptConventionService; namespace Kiota.Builder.Writers.TypeScript; public class CodeConstantWriter : BaseElementWriter @@ -101,7 +102,7 @@ private void WriteRequestsMetadataConstant(CodeConstant codeElement, LanguageWri var isStream = conventions.StreamTypeName.Equals(returnType, StringComparison.OrdinalIgnoreCase); var isEnum = executorMethod.ReturnType is CodeType codeType && codeType.TypeDefinition is CodeEnum; var returnTypeWithoutCollectionSymbol = GetReturnTypeWithoutCollectionSymbol(executorMethod, returnType); - var isPrimitive = conventions.IsPrimitiveType(returnTypeWithoutCollectionSymbol); + var isPrimitive = IsPrimitiveType(returnTypeWithoutCollectionSymbol); writer.StartBlock($"{executorMethod.Name.ToFirstCharacterLowerCase()}: {{"); var urlTemplateValue = executorMethod.HasUrlTemplateOverride ? $"\"{executorMethod.UrlTemplateOverride}\"" : uriTemplateConstant.Name.ToFirstCharacterUpperCase(); writer.WriteLine($"uriTemplate: {urlTemplateValue},"); @@ -167,7 +168,7 @@ private string GetTypeFactory(bool isVoid, bool isStream, CodeMethod codeElement { if (isVoid) return string.Empty; var typeName = conventions.TranslateType(codeElement.ReturnType); - if (isStream || conventions.IsPrimitiveType(typeName)) return $" \"{typeName}\""; + if (isStream || IsPrimitiveType(typeName)) return $" \"{typeName}\""; return $" {GetFactoryMethodName(codeElement.ReturnType, codeElement, writer)}"; } private string GetReturnTypeWithoutCollectionSymbol(CodeMethod codeElement, string fullTypeName) @@ -177,46 +178,19 @@ private string GetReturnTypeWithoutCollectionSymbol(CodeMethod codeElement, stri clone.CollectionKind = CodeTypeBase.CodeTypeCollectionKind.None; return conventions.GetTypeString(clone, codeElement); } - private string GetFactoryMethodName(CodeTypeBase targetClassType, CodeElement currentElement, LanguageWriter writer) - { - var returnType = conventions.GetTypeString(targetClassType, currentElement, false, writer); - var targetClassName = conventions.TranslateType(targetClassType); - var resultName = $"create{targetClassName.ToFirstCharacterUpperCase()}FromDiscriminatorValue"; - if (targetClassName.Equals(returnType, StringComparison.OrdinalIgnoreCase)) - return resultName; - if (targetClassType is CodeType currentType && - currentType.TypeDefinition is CodeClass definitionClass && - definitionClass.GetImmediateParentOfType() is CodeNamespace parentNamespace && - parentNamespace.FindChildByName(resultName) is CodeFunction factoryMethod) - { - var methodName = conventions.GetTypeString(new CodeType - { - Name = resultName, - TypeDefinition = factoryMethod - }, currentElement, false, writer); - return methodName.ToFirstCharacterUpperCase();// static function is aliased - } - throw new InvalidOperationException($"Unable to find factory method for {targetClassName}"); - } private string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCollection, bool isPrimitive, bool isEnum) { - if (isVoid) - { - return "sendNoResponseContent"; - } - else if (isEnum) - { - return isCollection ? "sendCollectionOfEnum" : "sendEnum"; - } - else if (isPrimitive || isStream) + return (isVoid, isEnum, isPrimitive || isStream, isCollection) switch { - return isCollection ? "sendCollectionOfPrimitive" : "sendPrimitive"; - } - else - { - return isCollection ? "sendCollection" : "send"; - } + (true, _, _, _) => "sendNoResponseContent", + (_, true, _, true) => "sendCollectionOfEnum", + (_, true, _, _) => "sendEnum", + (_, _, true, true) => "sendCollectionOfPrimitive", + (_, _, true, _) => "sendPrimitive", + (_, _, _, true) => "sendCollection", + _ => "send" + }; } private void WriteUriTemplateConstant(CodeConstant codeElement, LanguageWriter writer) diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs index c1694db626..5a80b74ad8 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs @@ -1,20 +1,17 @@ - - -using System; +using System; using System.Collections.Generic; using System.Linq; using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; using static Kiota.Builder.Refiners.TypeScriptRefiner; +using static Kiota.Builder.Writers.TypeScript.TypeScriptConventionService; namespace Kiota.Builder.Writers.TypeScript; -public class CodeFunctionWriter : BaseElementWriter +public class CodeFunctionWriter(TypeScriptConventionService conventionService) : BaseElementWriter(conventionService) { - public CodeFunctionWriter(TypeScriptConventionService conventionService) : base(conventionService) - { - } private static readonly HashSet customSerializationWriters = new(StringComparer.OrdinalIgnoreCase) { "writeObjectValue", "writeCollectionOfObjectValues" }; + private const string FactoryMethodReturnType = "((instance?: Parsable) => Record void>)"; public override void WriteCodeElement(CodeFunction codeElement, LanguageWriter writer) { @@ -25,10 +22,12 @@ public override void WriteCodeElement(CodeFunction codeElement, LanguageWriter w if (codeElement.Parent is not CodeFile parentFile) throw new InvalidOperationException("the parent of a function should be a file"); var codeMethod = codeElement.OriginalLocalMethod; + var composedType = GetOriginalComposedType(codeMethod.ReturnType); + var isComposedOfPrimitives = composedType is not null && composedType.IsComposedOfPrimitives(IsPrimitiveType); - var returnType = codeMethod.Kind is CodeMethodKind.Factory ? - "((instance?: Parsable) => Record void>)" : - conventions.GetTypeString(codeMethod.ReturnType, codeElement); + var returnType = codeMethod.Kind is CodeMethodKind.Factory && !isComposedOfPrimitives ? + FactoryMethodReturnType : + GetTypescriptTypeString(codeMethod.ReturnType, codeElement, inlineComposedTypeString: true); var isVoid = "void".EqualsIgnoreCase(returnType); CodeMethodWriter.WriteMethodDocumentationInternal(codeElement.OriginalLocalMethod, writer, isVoid, conventions); CodeMethodWriter.WriteMethodTypecheckIgnoreInternal(codeElement.OriginalLocalMethod, writer); @@ -45,7 +44,7 @@ public override void WriteCodeElement(CodeFunction codeElement, LanguageWriter w WriteSerializerFunction(codeElement, writer); break; case CodeMethodKind.Factory: - WriteDiscriminatorFunction(codeElement, writer); + WriteFactoryMethod(codeElement, writer); break; case CodeMethodKind.ClientConstructor: WriteApiConstructorBody(parentFile, codeMethod, writer); @@ -54,6 +53,138 @@ public override void WriteCodeElement(CodeFunction codeElement, LanguageWriter w } } + private string GetSerializationMethodsForPrimitiveUnionTypes(CodeComposedTypeBase composedType, string parseNodeParameterName, CodeFunction codeElement, bool nodeParameterCanBeNull = true) + { + var optionalChainingSymbol = nodeParameterCanBeNull ? "?" : string.Empty; + return string.Join(" ?? ", composedType.Types.Where(x => IsPrimitiveType(x, composedType)).Select(x => $"{parseNodeParameterName}{optionalChainingSymbol}." + conventions.GetDeserializationMethodName(x, codeElement.OriginalLocalMethod))); + } + + private static CodeParameter? GetComposedTypeParameter(CodeFunction codeElement) + { + return codeElement.OriginalLocalMethod.Parameters.FirstOrDefault(x => GetOriginalComposedType(x) is not null); + } + + private void WriteComposedTypeDeserializer(CodeFunction codeElement, LanguageWriter writer, CodeParameter composedParam) + { + + if (GetOriginalComposedType(composedParam) is not { } composedType) return; + + writer.StartBlock("return {"); + foreach (var mappedType in composedType.Types.Where(x => !IsPrimitiveType(x, composedType))) + { + var functionName = GetDeserializerFunctionName(codeElement, mappedType); + var variableName = composedParam.Name.ToFirstCharacterLowerCase(); + var variableType = GetTypescriptTypeString(mappedType, codeElement, includeCollectionInformation: false, + inlineComposedTypeString: true); + + writer.WriteLine($"...{functionName}({variableName} as {variableType}),"); + } + writer.CloseBlock(); + } + + private void WriteComposedTypeSerializer(CodeFunction codeElement, LanguageWriter writer, CodeParameter composedParam) + { + if (GetOriginalComposedType(composedParam) is not { } composedType) return; + + if (composedType.IsComposedOfPrimitives(IsPrimitiveType)) + { + var paramName = composedParam.Name.ToFirstCharacterLowerCase(); + writer.WriteLine($"if ({paramName} === undefined || {paramName} === null) return;"); + writer.StartBlock($"switch (typeof {paramName}) {{"); + foreach (var type in composedType.Types.Where(x => IsPrimitiveType(x, composedType))) + { + WriteCaseStatementForPrimitiveTypeSerialization(type, "key", paramName, codeElement, writer); + } + writer.CloseBlock(); + return; + } + + if (composedType is CodeIntersectionType) + { + WriteSerializationFunctionForCodeIntersectionType(composedType, composedParam, codeElement, writer); + return; + } + + WriteSerializationFunctionForCodeUnionTypes(composedType, composedParam, codeElement, writer); + } + + 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); + var variableName = composedParam.Name.ToFirstCharacterLowerCase(); + var variableType = GetTypescriptTypeString(mappedType, method, includeCollectionInformation: false, inlineComposedTypeString: true); + + writer.WriteLine($"{functionName}(writer, {variableName} as {variableType});"); + } + } + + private void WriteSerializationFunctionForCodeUnionTypes(CodeComposedTypeBase composedType, CodeParameter composedParam, CodeFunction codeElement, LanguageWriter writer) + { + var discriminatorInfo = codeElement.OriginalMethodParentClass.DiscriminatorInformation; + var discriminatorPropertyName = discriminatorInfo.DiscriminatorPropertyName; + + if (string.IsNullOrEmpty(discriminatorPropertyName)) + { + WriteBruteForceSerializationFunctionForCodeUnionType(composedType, composedParam, codeElement, writer); + return; + } + + var paramName = composedParam.Name.ToFirstCharacterLowerCase(); + writer.WriteLine($"if ({paramName} === undefined || {paramName} === null) return;"); + WriteDiscriminatorSwitchBlock(discriminatorInfo, paramName, codeElement, writer); + } + + /// + /// Writes the brute-force serialization function for a union type. + /// + /// The composed type representing the union. + /// The parameter associated with the composed type. + /// The function code element where serialization is performed. + /// The language writer used to generate the code. + /// + /// This method handles serialization for union types when the discriminator property is missing. + /// In the absence of a discriminator, all possible types in the union are serialized. For example, + /// a Pet union defined as Cat | Dog would result in the serialization of both Cat and Dog types. + /// It delegates the task to the method responsible for intersection types, treating the union + /// similarly to an intersection in this context. + /// + private void WriteBruteForceSerializationFunctionForCodeUnionType(CodeComposedTypeBase composedType, CodeParameter composedParam, CodeFunction codeElement, LanguageWriter writer) + { + // Delegate the serialization logic to the method handling intersection types, + // as both require serializing all possible type variations. + WriteSerializationFunctionForCodeIntersectionType(composedType, composedParam, codeElement, writer); + } + + private void WriteDiscriminatorSwitchBlock(DiscriminatorInformation discriminatorInfo, string paramName, CodeFunction codeElement, LanguageWriter writer) + { + writer.StartBlock($"switch ({paramName}.{discriminatorInfo.DiscriminatorPropertyName}) {{"); + + foreach (var mappedType in discriminatorInfo.DiscriminatorMappings) + { + writer.StartBlock($"case \"{mappedType.Key}\":"); + writer.WriteLine($"{GetSerializerFunctionName(codeElement, mappedType.Value)}(writer, {paramName} as {mappedType.Value.AllTypes.First().Name.ToFirstCharacterUpperCase()});"); + writer.WriteLine("break;"); + writer.DecreaseIndent(); + } + + writer.CloseBlock(); + } + + private void WriteCaseStatementForPrimitiveTypeSerialization(CodeTypeBase type, string key, string value, 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.DecreaseIndent(); + } + private static void WriteApiConstructorBody(CodeFile parentFile, CodeMethod method, LanguageWriter writer) { WriteSerializationRegistration(method.SerializerModules, writer, "registerDefaultSerializer"); @@ -61,7 +192,7 @@ private static void WriteApiConstructorBody(CodeFile parentFile, CodeMethod meth if (method.Parameters.OfKind(CodeParameterKind.RequestAdapter)?.Name.ToFirstCharacterLowerCase() is not string requestAdapterArgumentName) return; if (!string.IsNullOrEmpty(method.BaseUrl)) { - writer.StartBlock($"if ({requestAdapterArgumentName}.baseUrl === undefined || {requestAdapterArgumentName}.baseUrl === \"\") {{"); + writer.StartBlock($"if ({requestAdapterArgumentName}.baseUrl === undefined || {requestAdapterArgumentName}.baseUrl === null || {requestAdapterArgumentName}.baseUrl === \"\") {{"); writer.WriteLine($"{requestAdapterArgumentName}.baseUrl = \"{method.BaseUrl}\";"); writer.CloseBlock(); } @@ -84,7 +215,7 @@ private static void WriteSerializationRegistration(HashSet serialization writer.WriteLine($"{methodName}({module});"); } - private void WriteDiscriminatorFunction(CodeFunction codeElement, LanguageWriter writer) + private void WriteFactoryMethod(CodeFunction codeElement, LanguageWriter writer) { var returnType = conventions.GetTypeString(codeElement.OriginalLocalMethod.ReturnType, codeElement); @@ -96,9 +227,46 @@ private void WriteDiscriminatorFunction(CodeFunction codeElement, LanguageWriter private void WriteFactoryMethodBody(CodeFunction codeElement, string returnType, LanguageWriter writer) { var parseNodeParameter = codeElement.OriginalLocalMethod.Parameters.OfKind(CodeParameterKind.ParseNode); - if (codeElement.OriginalMethodParentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForInheritedType && parseNodeParameter != null) + var composedType = GetOriginalComposedType(codeElement.OriginalLocalMethod.ReturnType); + + switch (composedType) + { + case CodeComposedTypeBase type when type.IsComposedOfPrimitives(IsPrimitiveType): + string primitiveValuesUnionString = GetSerializationMethodsForPrimitiveUnionTypes(composedType, parseNodeParameter!.Name.ToFirstCharacterLowerCase(), codeElement); + writer.WriteLine($"return {primitiveValuesUnionString};"); + break; + case CodeUnionType _ when parseNodeParameter != null: + WriteDiscriminatorInformation(codeElement, parseNodeParameter, writer); + // The default discriminator is useful when the discriminator information is not provided. + WriteDefaultDiscriminator(codeElement, returnType, writer); + break; + case CodeIntersectionType _ when parseNodeParameter != null: + WriteDefaultDiscriminator(codeElement, returnType, writer); + break; + default: + if (codeElement.OriginalMethodParentClass.DiscriminatorInformation.ShouldWriteDiscriminatorForInheritedType && parseNodeParameter != null) + WriteDiscriminatorInformation(codeElement, parseNodeParameter, writer); + + WriteDefaultDiscriminator(codeElement, returnType, writer); + break; + } + } + + private void WriteDefaultDiscriminator(CodeFunction codeElement, string returnType, LanguageWriter writer) + { + var nameSpace = codeElement.GetImmediateParentOfType(); + var deserializationFunction = GetFunctionName(codeElement, returnType, CodeMethodKind.Deserializer, nameSpace); + writer.WriteLine($"return {deserializationFunction.ToFirstCharacterLowerCase()};"); + } + + private void WriteDiscriminatorInformation(CodeFunction codeElement, CodeParameter parseNodeParameter, LanguageWriter writer) + { + var discriminatorInfo = codeElement.OriginalMethodParentClass.DiscriminatorInformation; + var discriminatorPropertyName = discriminatorInfo.DiscriminatorPropertyName; + + if (!string.IsNullOrEmpty(discriminatorPropertyName)) { - writer.WriteLines($"const mappingValueNode = {parseNodeParameter.Name.ToFirstCharacterLowerCase()}.getChildNode(\"{codeElement.OriginalMethodParentClass.DiscriminatorInformation.DiscriminatorPropertyName}\");", + writer.WriteLines($"const mappingValueNode = {parseNodeParameter.Name.ToFirstCharacterLowerCase()}?.getChildNode(\"{discriminatorPropertyName}\");", "if (mappingValueNode) {"); writer.IncreaseIndent(); writer.WriteLines("const mappingValue = mappingValueNode.getStringValue();", @@ -106,30 +274,77 @@ private void WriteFactoryMethodBody(CodeFunction codeElement, string returnType, writer.IncreaseIndent(); writer.StartBlock("switch (mappingValue) {"); - foreach (var mappedType in codeElement.OriginalMethodParentClass.DiscriminatorInformation.DiscriminatorMappings) + foreach (var mappedType in discriminatorInfo.DiscriminatorMappings) { writer.StartBlock($"case \"{mappedType.Key}\":"); - writer.WriteLine($"return {getDeserializationFunction(codeElement, mappedType.Value.Name.ToFirstCharacterUpperCase())};"); + writer.WriteLine($"return {GetDeserializerFunctionName(codeElement, mappedType.Value)};"); writer.DecreaseIndent(); } writer.CloseBlock(); writer.CloseBlock(); writer.CloseBlock(); } - var s = getDeserializationFunction(codeElement, returnType); - writer.WriteLine($"return {s.ToFirstCharacterLowerCase()};"); } - private string getDeserializationFunction(CodeElement codeElement, string returnType) + private string GetFunctionName(CodeElement codeElement, string returnType, CodeMethodKind kind, CodeNamespace targetNamespace) { - var codeNamespace = codeElement.GetImmediateParentOfType(); - var parent = codeNamespace.FindChildByName($"deserializeInto{returnType}")!; + var functionName = kind switch + { + CodeMethodKind.Serializer => $"serialize{returnType}", + CodeMethodKind.Deserializer => $"deserializeInto{returnType}", + _ => throw new InvalidOperationException($"Unsupported function kind :: {kind}") + }; + + var codeFunction = FindCodeFunctionInParentNamespaces(functionName, targetNamespace); - return conventions.GetTypeString(new CodeType { TypeDefinition = parent }, codeElement, false); + return conventions.GetTypeString(new CodeType { TypeDefinition = codeFunction }, codeElement, false); + } + + private static CodeFunction? FindCodeFunctionInParentNamespaces(string functionName, CodeNamespace? parentNamespace) + { + CodeFunction? codeFunction = null; + + for (var currentNamespace = parentNamespace; + currentNamespace is not null && !functionName.Equals(codeFunction?.Name, StringComparison.Ordinal); + currentNamespace = currentNamespace.Parent?.GetImmediateParentOfType()) + { + codeFunction = currentNamespace.FindChildByName(functionName); + } + + return codeFunction; + } + + private string GetDeserializerFunctionName(CodeElement codeElement, CodeType returnType) => + FindFunctionInNameSpace($"deserializeInto{returnType.Name.ToFirstCharacterUpperCase()}", codeElement, returnType); + + private string GetSerializerFunctionName(CodeElement codeElement, CodeType returnType) => + FindFunctionInNameSpace($"serialize{returnType.Name.ToFirstCharacterUpperCase()}", codeElement, returnType); + + private string FindFunctionInNameSpace(string functionName, CodeElement codeElement, CodeType returnType) + { + var myNamespace = returnType.TypeDefinition!.GetImmediateParentOfType(); + + CodeFunction[] codeFunctions = myNamespace.FindChildrenByName(functionName).ToArray(); + + var codeFunction = Array.Find(codeFunctions, + func => func.GetImmediateParentOfType().Name == myNamespace.Name); + + if (codeFunction == null) + throw new InvalidOperationException($"Function {functionName} not found in namespace {myNamespace.Name}"); + + return conventions.GetTypeString(new CodeType { TypeDefinition = codeFunction }, codeElement, false); } private void WriteSerializerFunction(CodeFunction codeElement, LanguageWriter writer) { + // Determine if the function serializes a composed type + var composedParam = GetComposedTypeParameter(codeElement); + if (composedParam is not null) + { + WriteComposedTypeSerializer(codeElement, writer, composedParam); + return; + } + if (codeElement.OriginalLocalMethod.Parameters.FirstOrDefault(static x => x.Type is CodeType type && type.TypeDefinition is CodeInterface) is not { Type: CodeType @@ -155,105 +370,235 @@ private void WriteSerializerFunction(CodeFunction codeElement, LanguageWriter wr writer.CloseBlock(); } - private static bool IsCodePropertyCollectionOfEnum(CodeProperty property) + private static bool IsCollectionOfEnum(CodeProperty property) { - return property.Type is CodeType cType && cType.IsCollection && cType.TypeDefinition is CodeEnum; + return property.Type is CodeType codeType && codeType.IsCollection && codeType.TypeDefinition is CodeEnum; } private void WritePropertySerializer(string modelParamName, CodeProperty codeProperty, LanguageWriter writer, CodeFunction codeFunction) { - var isCollectionOfEnum = IsCodePropertyCollectionOfEnum(codeProperty); - var spreadOperator = isCollectionOfEnum ? "..." : string.Empty; var codePropertyName = codeProperty.Name.ToFirstCharacterLowerCase(); - var propTypeName = conventions.GetTypeString(codeProperty.Type, codeProperty.Parent!, false); + var propTypeName = GetTypescriptTypeString(codeProperty.Type, codeProperty.Parent!, false, inlineComposedTypeString: true); + + var serializationName = GetSerializationMethodName(codeProperty.Type, codeFunction.OriginalLocalMethod); + var defaultValueSuffix = GetDefaultValueLiteralForProperty(codeProperty) is string dft && !string.IsNullOrEmpty(dft) && !dft.EqualsIgnoreCase("\"null\"") ? $" ?? {dft}" : string.Empty; - var serializationName = GetSerializationMethodName(codeProperty.Type); - var defaultValueSuffix = GetDefaultValueLiteralForProperty(codeProperty) is string dft && !string.IsNullOrEmpty(dft) ? $" ?? {dft}" : string.Empty; if (customSerializationWriters.Contains(serializationName) && codeProperty.Type is CodeType propType && propType.TypeDefinition is not null) { - var serializeName = getSerializerAlias(propType, codeFunction, $"serialize{propType.TypeDefinition.Name}"); - writer.WriteLine($"writer.{serializationName}<{propTypeName}>(\"{codeProperty.WireName}\", {modelParamName}.{codePropertyName}{defaultValueSuffix}, {serializeName});"); + var serializeName = GetSerializerAlias(propType, codeFunction, $"serialize{propType.TypeDefinition.Name}"); + if (GetOriginalComposedType(propType.TypeDefinition) is { } ct && (ct.IsComposedOfPrimitives(IsPrimitiveType) || ct.IsComposedOfObjectsAndPrimitives(IsPrimitiveType))) + WriteSerializationStatementForComposedTypeProperty(ct, modelParamName, codeFunction, writer, codeProperty, serializeName); + else + writer.WriteLine($"writer.{serializationName}<{propTypeName}>(\"{codeProperty.WireName}\", {modelParamName}.{codePropertyName}{defaultValueSuffix}, {serializeName});"); } else { - if (!string.IsNullOrWhiteSpace(spreadOperator)) + WritePropertySerializationStatement(codeProperty, modelParamName, serializationName, defaultValueSuffix, codeFunction, writer); + } + } + + private void WritePropertySerializationStatement(CodeProperty codeProperty, string modelParamName, string? serializationName, string? defaultValueSuffix, CodeFunction codeFunction, LanguageWriter writer) + { + var isCollectionOfEnum = IsCollectionOfEnum(codeProperty); + var spreadOperator = isCollectionOfEnum ? "..." : string.Empty; + var codePropertyName = codeProperty.Name.ToFirstCharacterLowerCase(); + var composedType = GetOriginalComposedType(codeProperty.Type); + + if (!string.IsNullOrWhiteSpace(spreadOperator)) + writer.WriteLine($"if({modelParamName}.{codePropertyName})"); + if (composedType is not null && (composedType.IsComposedOfPrimitives(IsPrimitiveType) || composedType.IsComposedOfObjectsAndPrimitives(IsPrimitiveType))) + WriteSerializationStatementForComposedTypeProperty(composedType, modelParamName, codeFunction, writer, codeProperty, string.Empty); + else + writer.WriteLine($"writer.{serializationName}(\"{codeProperty.WireName}\", {spreadOperator}{modelParamName}.{codePropertyName}{defaultValueSuffix});"); + } + + private void WriteSerializationStatementForComposedTypeProperty(CodeComposedTypeBase composedType, string modelParamName, CodeFunction method, LanguageWriter writer, CodeProperty codeProperty, string? serializeName) + { + var defaultValueSuffix = GetDefaultValueLiteralForProperty(codeProperty) is string dft && !string.IsNullOrEmpty(dft) && !dft.EqualsIgnoreCase("\"null\"") ? $" ?? {dft}" : string.Empty; + writer.StartBlock("switch (true) {"); + WriteComposedTypeSwitchClause(composedType, method, writer, codeProperty, modelParamName, defaultValueSuffix); + WriteComposedTypeDefaultClause(composedType, writer, codeProperty, modelParamName, defaultValueSuffix, serializeName); + writer.CloseBlock(); + } + + private void WriteComposedTypeSwitchClause(CodeComposedTypeBase composedType, CodeFunction method, LanguageWriter writer, CodeProperty codeProperty, string modelParamName, string defaultValueSuffix) + { + var codePropertyName = codeProperty.Name.ToFirstCharacterLowerCase(); + var isCollectionOfEnum = IsCollectionOfEnum(codeProperty); + var spreadOperator = isCollectionOfEnum ? "..." : string.Empty; + + foreach (var type in composedType.Types.Where(x => IsPrimitiveType(x, composedType))) + { + var nodeType = conventions.GetTypeString(type, method, false); + var serializationName = GetSerializationMethodName(type, method.OriginalLocalMethod); + if (string.IsNullOrEmpty(serializationName) || string.IsNullOrEmpty(nodeType)) return; + + writer.StartBlock(type.IsCollection + ? $"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});"); + writer.CloseBlock("break;"); + } + } + + private static void WriteComposedTypeDefaultClause(CodeComposedTypeBase composedType, LanguageWriter writer, CodeProperty codeProperty, string modelParamName, string defaultValueSuffix, string? serializeName) + { + var codePropertyName = codeProperty.Name.ToFirstCharacterLowerCase(); + var nonPrimitiveTypes = composedType.Types.Where(x => !IsPrimitiveType(x, composedType)).ToArray(); + if (nonPrimitiveTypes.Length > 0) + { + writer.StartBlock("default:"); + foreach (var groupedTypes in nonPrimitiveTypes.GroupBy(static x => x.IsCollection)) { - writer.WriteLine($"if({modelParamName}.{codePropertyName})"); + var collectionCodeType = (composedType.Clone() as CodeComposedTypeBase)!; + collectionCodeType.SetTypes(groupedTypes.ToArray()); + var propTypeName = GetTypescriptTypeString(collectionCodeType!, codeProperty.Parent!, false, inlineComposedTypeString: true); + + var writerFunction = groupedTypes.Key ? "writeCollectionOfObjectValues" : "writeObjectValue"; + var propertyTypes = collectionCodeType.IsNullable ? " | undefined | null" : string.Empty; + var groupSymbol = groupedTypes.Key ? "[]" : string.Empty; + + writer.WriteLine($"writer.{writerFunction}<{propTypeName}>(\"{codeProperty.WireName}\", {modelParamName}.{codePropertyName}{defaultValueSuffix} as {propTypeName}{groupSymbol}{propertyTypes}, {serializeName});"); } - writer.WriteLine($"writer.{serializationName}(\"{codeProperty.WireName}\", {spreadOperator}{modelParamName}.{codePropertyName}{defaultValueSuffix});"); + writer.CloseBlock("break;"); } } - private string GetSerializationMethodName(CodeTypeBase propType) + + private string GetSerializationMethodName(CodeTypeBase propertyType, CodeMethod method) { - var propertyType = conventions.TranslateType(propType); - if (!string.IsNullOrEmpty(propertyType) && propType is CodeType currentType && GetSerializationMethodNameForCodeType(currentType, propertyType) is string result && !string.IsNullOrWhiteSpace(result)) + ArgumentNullException.ThrowIfNull(propertyType); + ArgumentNullException.ThrowIfNull(method); + + var composedType = GetOriginalComposedType(propertyType); + if (composedType is not null && composedType.IsComposedOfPrimitives(IsPrimitiveType)) + return $"serialize{composedType.Name.ToFirstCharacterUpperCase()}"; + + var propertyTypeName = TranslateTypescriptType(propertyType); + CodeType? currentType = composedType is not null ? GetCodeTypeForComposedType(composedType) : propertyType as CodeType; + + if (currentType != null && !string.IsNullOrEmpty(propertyTypeName)) { - return result; + var result = GetSerializationMethodNameForCodeType(currentType, propertyTypeName); + if (!string.IsNullOrWhiteSpace(result)) + { + return result; + } } - return propertyType switch + + if (propertyTypeName is TYPE_LOWERCASE_STRING or TYPE_LOWERCASE_BOOLEAN or TYPE_NUMBER or TYPE_GUID or TYPE_DATE or TYPE_DATE_ONLY or TYPE_TIME_ONLY or TYPE_DURATION) + return $"write{propertyTypeName.ToFirstCharacterUpperCase()}Value"; + + return "writeObjectValue"; + } + + private static CodeType GetCodeTypeForComposedType(CodeComposedTypeBase composedType) + { + ArgumentNullException.ThrowIfNull(composedType); + return new CodeType { - "string" or "boolean" or "number" or "Guid" or "Date" or "DateOnly" or "TimeOnly" or "Duration" => $"write{propertyType.ToFirstCharacterUpperCase()}Value", - _ => $"writeObjectValue", + Name = composedType.Name, + TypeDefinition = composedType, + CollectionKind = composedType.CollectionKind }; } private string? GetSerializationMethodNameForCodeType(CodeType propType, string propertyType) { - if (propType.TypeDefinition is CodeEnum currentEnum) - return $"writeEnumValue<{currentEnum.Name.ToFirstCharacterUpperCase()}{(currentEnum.Flags && !propType.IsCollection ? "[]" : string.Empty)}>"; - else if (conventions.StreamTypeName.Equals(propertyType, StringComparison.OrdinalIgnoreCase)) - return "writeByteArrayValue"; - else if (propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None) + return propType switch { - if (propType.TypeDefinition == null) - return $"writeCollectionOfPrimitiveValues<{propertyType}>"; - else - return "writeCollectionOfObjectValues"; - } - return null; + _ when propType.TypeDefinition is CodeEnum currentEnum => $"writeEnumValue<{currentEnum.Name.ToFirstCharacterUpperCase()}{(currentEnum.Flags && !propType.IsCollection ? "[]" : string.Empty)}>", + _ when conventions.StreamTypeName.Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "writeByteArrayValue", + _ when propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None => propType.TypeDefinition == null ? $"writeCollectionOfPrimitiveValues<{propertyType}>" : "writeCollectionOfObjectValues", + _ => null + }; } private void WriteDeserializerFunction(CodeFunction codeFunction, LanguageWriter writer) { - if (codeFunction.OriginalLocalMethod.Parameters.FirstOrDefault() is CodeParameter param && param.Type is CodeType codeType && codeType.TypeDefinition is CodeInterface codeInterface) + var composedParam = GetComposedTypeParameter(codeFunction); + if (composedParam is not null) { - var properties = codeInterface.Properties.Where(static x => x.IsOfKind(CodePropertyKind.Custom, CodePropertyKind.BackingStore) && !x.ExistsInBaseType); + WriteComposedTypeDeserializer(codeFunction, writer, composedParam); + return; + } - writer.StartBlock("return {"); - if (codeInterface.StartBlock.Implements.FirstOrDefault(static x => x.TypeDefinition is CodeInterface) is CodeType type && type.TypeDefinition is CodeInterface inherits) - { - writer.WriteLine($"...deserializeInto{inherits.Name.ToFirstCharacterUpperCase()}({param.Name.ToFirstCharacterLowerCase()}),"); - } + var param = codeFunction.OriginalLocalMethod.Parameters.FirstOrDefault(); + if (param?.Type is CodeType codeType && codeType.TypeDefinition is CodeInterface codeInterface) + { + WriteDeserializerFunctionProperties(param, codeInterface, codeFunction, writer); + } + else + { + throw new InvalidOperationException($"Model interface for deserializer function {codeFunction.Name} is not available"); + } + } - var primaryErrorMapping = string.Empty; - var primaryErrorMappingKey = string.Empty; - var parentClass = codeFunction.OriginalMethodParentClass; + private void WriteDeserializerFunctionProperties(CodeParameter param, CodeInterface codeInterface, CodeFunction codeFunction, LanguageWriter writer) + { + var properties = codeInterface.Properties.Where(static x => x.IsOfKind(CodePropertyKind.Custom, CodePropertyKind.BackingStore) && !x.ExistsInBaseType); - if (parentClass.IsErrorDefinition && parentClass.AssociatedInterface is not null && parentClass.AssociatedInterface.GetPrimaryMessageCodePath(static x => x.Name.ToFirstCharacterLowerCase(), static x => x.Name.ToFirstCharacterLowerCase(), "?.") is string primaryMessageCodePath && !string.IsNullOrEmpty(primaryMessageCodePath)) - { - primaryErrorMapping = $" {param.Name.ToFirstCharacterLowerCase()}.message = {param.Name.ToFirstCharacterLowerCase()}.{primaryMessageCodePath} ?? \"\";"; - primaryErrorMappingKey = primaryMessageCodePath.Split("?.", StringSplitOptions.RemoveEmptyEntries)[0]; - } + writer.StartBlock("return {"); + if (codeInterface.StartBlock.Implements.FirstOrDefault(static x => x.TypeDefinition is CodeInterface) is CodeType type && type.TypeDefinition is CodeInterface inherits) + { + writer.WriteLine($"...deserializeInto{inherits.Name.ToFirstCharacterUpperCase()}({param.Name.ToFirstCharacterLowerCase()}),"); + } + var (primaryErrorMapping, primaryErrorMappingKey) = GetPrimaryErrorMapping(codeFunction, param); - foreach (var otherProp in properties) - { - var suffix = otherProp.Name.Equals(primaryErrorMappingKey, StringComparison.Ordinal) ? primaryErrorMapping : string.Empty; - if (otherProp.Kind is CodePropertyKind.BackingStore) - writer.WriteLine($"\"{BackingStoreEnabledKey}\": n => {{ {param.Name.ToFirstCharacterLowerCase()}.{otherProp.Name.ToFirstCharacterLowerCase()} = true;{suffix} }},"); - else - { - var defaultValueSuffix = GetDefaultValueLiteralForProperty(otherProp) is string dft && !string.IsNullOrEmpty(dft) ? $" ?? {dft}" : string.Empty; - writer.WriteLine($"\"{otherProp.WireName}\": n => {{ {param.Name.ToFirstCharacterLowerCase()}.{otherProp.Name.ToFirstCharacterLowerCase()} = n.{GetDeserializationMethodName(otherProp.Type, codeFunction)}{defaultValueSuffix};{suffix} }},"); - } - } + foreach (var otherProp in properties) + { + WritePropertyDeserializationBlock(otherProp, param, primaryErrorMapping, primaryErrorMappingKey, codeFunction, writer); + } - writer.CloseBlock(); + writer.CloseBlock(); + } + + private static (string, string) GetPrimaryErrorMapping(CodeFunction codeFunction, CodeParameter param) + { + var primaryErrorMapping = string.Empty; + var primaryErrorMappingKey = string.Empty; + var parentClass = codeFunction.OriginalMethodParentClass; + + if (parentClass.IsErrorDefinition && parentClass.AssociatedInterface is not null && parentClass.AssociatedInterface.GetPrimaryMessageCodePath(static x => x.Name.ToFirstCharacterLowerCase(), static x => x.Name.ToFirstCharacterLowerCase(), "?.") is string primaryMessageCodePath && !string.IsNullOrEmpty(primaryMessageCodePath)) + { + primaryErrorMapping = $" {param.Name.ToFirstCharacterLowerCase()}.message = {param.Name.ToFirstCharacterLowerCase()}.{primaryMessageCodePath} ?? \"\";"; + primaryErrorMappingKey = primaryMessageCodePath.Split("?.", StringSplitOptions.RemoveEmptyEntries)[0]; + } + + return (primaryErrorMapping, primaryErrorMappingKey); + } + + private void WritePropertyDeserializationBlock(CodeProperty otherProp, CodeParameter param, string primaryErrorMapping, string primaryErrorMappingKey, CodeFunction codeFunction, LanguageWriter writer) + { + var suffix = otherProp.Name.Equals(primaryErrorMappingKey, StringComparison.Ordinal) ? primaryErrorMapping : string.Empty; + var paramName = param.Name.ToFirstCharacterLowerCase(); + var propName = otherProp.Name.ToFirstCharacterLowerCase(); + + if (otherProp.Kind is CodePropertyKind.BackingStore) + { + writer.WriteLine($"\"{BackingStoreEnabledKey}\": n => {{ {paramName}.{propName} = true;{suffix} }},"); + } + else if (GetOriginalComposedType(otherProp.Type) is { } composedType) + { + var expression = string.Join(" ?? ", composedType.Types.Select(codeType => $"n.{conventions.GetDeserializationMethodName(codeType, codeFunction.OriginalLocalMethod, composedType.IsCollection)}")); + writer.WriteLine($"\"{otherProp.WireName}\": n => {{ {paramName}.{propName} = {expression};{suffix} }},"); } else - throw new InvalidOperationException($"Model interface for deserializer function {codeFunction.Name} is not available"); + { + var objectSerializationMethodName = conventions.GetDeserializationMethodName(otherProp.Type, codeFunction.OriginalLocalMethod); + var defaultValueSuffix = GetDefaultValueSuffix(otherProp); + writer.WriteLine($"\"{otherProp.WireName}\": n => {{ {paramName}.{propName} = n.{objectSerializationMethodName}{defaultValueSuffix};{suffix} }},"); + } } + + private static string GetDefaultValueSuffix(CodeProperty otherProp) + { + var defaultValue = GetDefaultValueLiteralForProperty(otherProp); + return !string.IsNullOrEmpty(defaultValue) && !defaultValue.EqualsIgnoreCase("\"null\"") ? $" ?? {defaultValue}" : string.Empty; + } + private static string GetDefaultValueLiteralForProperty(CodeProperty codeProperty) { if (string.IsNullOrEmpty(codeProperty.DefaultValue)) return string.Empty; @@ -277,56 +622,30 @@ private void WriteDefensiveStatements(CodeMethod codeElement, LanguageWriter wri writer.WriteLine($"if(!{parameterName}) throw new Error(\"{parameterName} cannot be undefined\");"); } } - private string GetDeserializationMethodName(CodeTypeBase propType, CodeFunction codeFunction) + + private string? GetSerializerAlias(CodeType propType, CodeFunction codeFunction, string propertySerializerName) { - var isCollection = propType.CollectionKind != CodeTypeBase.CodeTypeCollectionKind.None; - var propertyType = conventions.GetTypeString(propType, codeFunction, false); - if (!string.IsNullOrEmpty(propertyType) && propType is CodeType currentType) + CodeFunction serializationFunction; + + if (GetOriginalComposedType(propType) is not null) { - if (currentType.TypeDefinition is CodeEnum currentEnum && currentEnum.CodeEnumObject is not null) - return $"{(currentEnum.Flags || isCollection ? "getCollectionOfEnumValues" : "getEnumValue")}<{currentEnum.Name.ToFirstCharacterUpperCase()}>({currentEnum.CodeEnumObject.Name.ToFirstCharacterUpperCase()})"; - else if (conventions.StreamTypeName.Equals(propertyType, StringComparison.OrdinalIgnoreCase)) - return "getByteArrayValue"; - else if (isCollection) - if (currentType.TypeDefinition == null) - return $"getCollectionOfPrimitiveValues<{propertyType}>()"; - else - { - return $"getCollectionOfObjectValues<{propertyType.ToFirstCharacterUpperCase()}>({GetFactoryMethodName(propType, codeFunction.OriginalLocalMethod)})"; - } + if (codeFunction.GetImmediateParentOfType() is not CodeFile functionParentFile || + functionParentFile.FindChildByName(propertySerializerName, false) is not CodeFunction composedTypeSerializationFunction) + { + return string.Empty; + } + serializationFunction = composedTypeSerializationFunction; } - return propertyType switch - { - "string" or "boolean" or "number" or "Guid" or "Date" or "DateOnly" or "TimeOnly" or "Duration" => $"get{propertyType.ToFirstCharacterUpperCase()}Value()", - _ => $"getObjectValue<{propertyType.ToFirstCharacterUpperCase()}>({GetFactoryMethodName(propType, codeFunction.OriginalLocalMethod)})" - }; - } - - private string GetFactoryMethodName(CodeTypeBase targetClassType, CodeMethod currentElement) - { - if (conventions.TranslateType(targetClassType) is string targetClassName) + else { - var resultName = $"create{targetClassName.ToFirstCharacterUpperCase()}FromDiscriminatorValue"; - if (conventions.GetTypeString(targetClassType, currentElement, false) is string returnType && targetClassName.EqualsIgnoreCase(returnType)) return resultName; - if (targetClassType is CodeType currentType && currentType.TypeDefinition is CodeInterface definitionClass) + if (propType.TypeDefinition?.GetImmediateParentOfType() is not CodeFile parentFile || + parentFile.FindChildByName(propertySerializerName, false) is not CodeFunction foundSerializationFunction) { - var factoryMethod = definitionClass.GetImmediateParentOfType()?.FindChildByName(resultName) ?? - definitionClass.GetImmediateParentOfType()?.FindChildByName(resultName); - if (factoryMethod != null) - { - var methodName = conventions.GetTypeString(new CodeType { Name = resultName, TypeDefinition = factoryMethod }, currentElement, false); - return methodName.ToFirstCharacterUpperCase();// static function is aliased - } + return string.Empty; } + serializationFunction = foundSerializationFunction; } - throw new InvalidOperationException($"Unable to find factory method for {targetClassType}"); - } - private string? getSerializerAlias(CodeType propType, CodeFunction codeFunction, string propertySerializerName) - { - if (propType.TypeDefinition?.GetImmediateParentOfType() is not CodeFile parentFile || - parentFile.FindChildByName(propertySerializerName, false) is not CodeFunction serializationFunction) - return string.Empty; return conventions.GetTypeString(new CodeType { TypeDefinition = serializationFunction }, codeFunction, false); } } diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeIntersectionTypeWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeIntersectionTypeWriter.cs new file mode 100644 index 0000000000..69edee087f --- /dev/null +++ b/src/Kiota.Builder/Writers/TypeScript/CodeIntersectionTypeWriter.cs @@ -0,0 +1,15 @@ +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.TypeScript; + +public class CodeIntersectionTypeWriter(TypeScriptConventionService conventionService) : CodeComposedTypeBaseWriter(conventionService) +{ + // The `CodeIntersectionType` will utilize the same union symbol `|`, but the methods for serialization and deserialization + // will differ slightly. This is because the `CodeIntersectionType` for `Foo` and `Bar` can encompass both `Foo` and `Bar` + // simultaneously, whereas the `CodeUnion` can only include either `Foo` or `Bar`, but not both at the same time. + + public override string TypesDelimiter + { + get => "|"; + } +} diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs index 7ef1f50958..ce8ae9769b 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs @@ -4,13 +4,11 @@ using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; using Kiota.Builder.OrderComparers; +using static Kiota.Builder.Writers.TypeScript.TypeScriptConventionService; namespace Kiota.Builder.Writers.TypeScript; -public class CodeMethodWriter : BaseElementWriter +public class CodeMethodWriter(TypeScriptConventionService conventionService) : BaseElementWriter(conventionService) { - public CodeMethodWriter(TypeScriptConventionService conventionService) : base(conventionService) - { - } public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter writer) { ArgumentNullException.ThrowIfNull(codeElement); @@ -18,7 +16,7 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri ArgumentNullException.ThrowIfNull(writer); if (codeElement.Parent is CodeFunction) return; - var returnType = conventions.GetTypeString(codeElement.ReturnType, codeElement); + var returnType = GetTypescriptTypeString(codeElement.ReturnType, codeElement, inlineComposedTypeString: true); var isVoid = "void".EqualsIgnoreCase(returnType); WriteMethodDocumentation(codeElement, writer, isVoid); WriteMethodPrototype(codeElement, writer, returnType, isVoid); @@ -35,19 +33,19 @@ internal static void WriteMethodDocumentationInternal(CodeMethod code, LanguageW var returnRemark = (isVoid, code.IsAsync) switch { (true, _) => string.Empty, - (false, true) => $"@returns {{Promise<{typeScriptConventionService.GetTypeString(code.ReturnType, code)}>}}", - (false, false) => $"@returns {{{typeScriptConventionService.GetTypeString(code.ReturnType, code)}}}", + (false, true) => $"@returns {{Promise<{GetTypescriptTypeString(code.ReturnType, code, inlineComposedTypeString: true)}>}}", + (false, false) => $"@returns {{{GetTypescriptTypeString(code.ReturnType, code, inlineComposedTypeString: true)}}}", }; typeScriptConventionService.WriteLongDescription(code, writer, code.Parameters .Where(static x => x.Documentation.DescriptionAvailable) .OrderBy(static x => x.Name) - .Select(x => $"@param {x.Name} {x.Documentation.GetDescription(type => typeScriptConventionService.GetTypeString(type, code), TypeScriptConventionService.ReferenceTypePrefix, TypeScriptConventionService.ReferenceTypeSuffix, TypeScriptConventionService.RemoveInvalidDescriptionCharacters)}") + .Select(x => $"@param {x.Name} {x.Documentation.GetDescription(type => GetTypescriptTypeString(type, code, inlineComposedTypeString: true), ReferenceTypePrefix, ReferenceTypeSuffix, RemoveInvalidDescriptionCharacters)}") .Union([returnRemark]) - .Union(GetThrownExceptionsRemarks(code, typeScriptConventionService))); + .Union(GetThrownExceptionsRemarks(code))); } - private static IEnumerable GetThrownExceptionsRemarks(CodeMethod code, TypeScriptConventionService typeScriptConventionService) + private static IEnumerable GetThrownExceptionsRemarks(CodeMethod code) { if (code.Kind is not CodeMethodKind.RequestExecutor) yield break; foreach (var errorMapping in code.ErrorMappings) @@ -57,7 +55,7 @@ private static IEnumerable GetThrownExceptionsRemarks(CodeMethod code, T "XXX" => "4XX or 5XX", _ => errorMapping.Key, }; - var errorTypeString = typeScriptConventionService.GetTypeString(errorMapping.Value, code, false); + var errorTypeString = GetTypescriptTypeString(errorMapping.Value, code, false, inlineComposedTypeString: true); yield return $"@throws {{{errorTypeString}}} error when the service returns a {statusCode} status code"; } } diff --git a/src/Kiota.Builder/Writers/TypeScript/CodePropertyWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodePropertyWriter.cs index fb1aaf201b..557925f7d8 100644 --- a/src/Kiota.Builder/Writers/TypeScript/CodePropertyWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/CodePropertyWriter.cs @@ -1,6 +1,7 @@ using System; using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; +using static Kiota.Builder.Writers.TypeScript.TypeScriptConventionService; namespace Kiota.Builder.Writers.TypeScript; public class CodePropertyWriter : BaseElementWriter @@ -12,7 +13,7 @@ public override void WriteCodeElement(CodeProperty codeElement, LanguageWriter w ArgumentNullException.ThrowIfNull(writer); if (codeElement.ExistsInExternalBaseType) return; - var returnType = conventions.GetTypeString(codeElement.Type, codeElement); + var returnType = GetTypescriptTypeString(codeElement.Type, codeElement, inlineComposedTypeString: true); var isFlagEnum = codeElement.Type is CodeType { TypeDefinition: CodeEnum { Flags: true } } && !codeElement.Type.IsCollection;//collection of flagged enums are not supported/don't make sense diff --git a/src/Kiota.Builder/Writers/TypeScript/CodeUnionTypeWriter.cs b/src/Kiota.Builder/Writers/TypeScript/CodeUnionTypeWriter.cs new file mode 100644 index 0000000000..ce51f20892 --- /dev/null +++ b/src/Kiota.Builder/Writers/TypeScript/CodeUnionTypeWriter.cs @@ -0,0 +1,11 @@ +using Kiota.Builder.CodeDOM; + +namespace Kiota.Builder.Writers.TypeScript; + +public class CodeUnionTypeWriter(TypeScriptConventionService conventionService) : CodeComposedTypeBaseWriter(conventionService) +{ + public override string TypesDelimiter + { + get => "|"; + } +} diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs index 0b5a88ad37..a635ef6cd8 100644 --- a/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptConventionService.cs @@ -5,12 +5,41 @@ using Kiota.Builder.CodeDOM; using Kiota.Builder.Extensions; - using static Kiota.Builder.CodeDOM.CodeTypeBase; +using static Kiota.Builder.Refiners.TypeScriptRefiner; namespace Kiota.Builder.Writers.TypeScript; public class TypeScriptConventionService : CommonLanguageConventionService { +#pragma warning disable CA1707 // Remove the underscores + public const string TYPE_INTEGER = "integer"; + public const string TYPE_INT64 = "int64"; + public const string TYPE_INT = "int"; + public const string TYPE_FLOAT = "float"; + public const string TYPE_DOUBLE = "double"; + public const string TYPE_BYTE = "byte"; + public const string TYPE_SBYTE = "sbyte"; + public const string TYPE_DECIMAL = "decimal"; + public const string TYPE_BINARY = "binary"; + public const string TYPE_BASE64 = "base64"; + public const string TYPE_BASE64URL = "base64url"; + public const string TYPE_GUID = "Guid"; + public const string TYPE_STRING = "String"; + public const string TYPE_OBJECT = "Object"; + public const string TYPE_BOOLEAN = "Boolean"; + public const string TYPE_VOID = "Void"; + public const string TYPE_LOWERCASE_STRING = "string"; + public const string TYPE_LOWERCASE_OBJECT = "object"; + public const string TYPE_LOWERCASE_BOOLEAN = "boolean"; + public const string TYPE_LOWERCASE_VOID = "void"; + public const string TYPE_BYTE_ARRAY = "byte[]"; + public const string TYPE_NUMBER = "number"; + public const string TYPE_DATE = "Date"; + public const string TYPE_DATE_ONLY = "DateOnly"; + public const string TYPE_TIME_ONLY = "TimeOnly"; + public const string TYPE_DURATION = "Duration"; +#pragma warning restore CA1707 // Remove the underscores + internal void WriteAutoGeneratedStart(LanguageWriter writer) { writer.WriteLine("/* tslint:disable */"); @@ -52,65 +81,124 @@ 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 paramType = GetTypeString(parameter.Type, targetElement); - var defaultValueSuffix = (string.IsNullOrEmpty(parameter.DefaultValue), parameter.Kind) switch + 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 defaultValueSuffix = (string.IsNullOrEmpty(parameter.DefaultValue), parameter.Kind, isComposedOfPrimitives) switch { - (false, CodeParameterKind.DeserializationTarget) when parameter.Parent is CodeMethod codeMethod && codeMethod.Kind is CodeMethodKind.Serializer + (false, CodeParameterKind.DeserializationTarget, false) when parameter.Parent is CodeMethod codeMethod && codeMethod.Kind is CodeMethodKind.Serializer => $" | null = {parameter.DefaultValue}", - (false, CodeParameterKind.DeserializationTarget) => $" = {parameter.DefaultValue}", - (false, _) => $" = {parameter.DefaultValue} as {paramType}", - (true, _) => string.Empty, + (false, CodeParameterKind.DeserializationTarget, false) => $" = {parameter.DefaultValue}", + (false, _, false) => $" = {parameter.DefaultValue} as {paramType}", + _ => string.Empty, }; - var (partialPrefix, partialSuffix) = parameter.Kind switch + var (partialPrefix, partialSuffix) = (isComposedOfPrimitives, parameter.Kind) switch { - CodeParameterKind.DeserializationTarget => ("Partial<", ">"), + (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}"; } + public override string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true, LanguageWriter? writer = null) + { + return GetTypescriptTypeString(code, targetElement, includeCollectionInformation); + } + + public static string GetTypescriptTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true, bool inlineComposedTypeString = false) { ArgumentNullException.ThrowIfNull(code); ArgumentNullException.ThrowIfNull(targetElement); + var collectionSuffix = code.CollectionKind == CodeTypeCollectionKind.None || !includeCollectionInformation ? string.Empty : "[]"; - if (code is CodeComposedTypeBase currentUnion && currentUnion.Types.Any()) - return string.Join(" | ", currentUnion.Types.Select(x => GetTypeString(x, targetElement))) + collectionSuffix; - if (code is CodeType currentType) + + var composedType = GetOriginalComposedType(code); + + if (inlineComposedTypeString && composedType?.Types.Any() == true) + { + return GetComposedTypeTypeString(composedType, targetElement, collectionSuffix, includeCollectionInformation: includeCollectionInformation); + } + + CodeTypeBase codeType = composedType is not null ? new CodeType() { TypeDefinition = composedType } : code; + + if (codeType is not CodeType currentType) { - var typeName = GetTypeAlias(currentType, targetElement) is string alias && !string.IsNullOrEmpty(alias) ? alias : TranslateType(currentType); - var genericParameters = currentType.GenericTypeParameterValues.Any() ? - $"<{string.Join(", ", currentType.GenericTypeParameterValues.Select(x => GetTypeString(x, targetElement, includeCollectionInformation)))}>" : - string.Empty; - return $"{typeName}{collectionSuffix}{genericParameters}"; + throw new InvalidOperationException($"type of type {code.GetType()} is unknown"); } - throw new InvalidOperationException($"type of type {code.GetType()} is unknown"); + var typeName = GetTypeAlias(currentType, targetElement) is string alias && !string.IsNullOrEmpty(alias) + ? alias + : TranslateTypescriptType(currentType); + + var genericParameters = currentType.GenericTypeParameterValues.Any() + ? $"<{string.Join(", ", currentType.GenericTypeParameterValues.Select(x => GetTypescriptTypeString(x, targetElement, includeCollectionInformation)))}>" + : string.Empty; + + return $"{typeName}{collectionSuffix}{genericParameters}"; + } + + /** + * Gets the composed type string representation e.g `type1 | type2 | type3[]` or `(type1 & type2 & type3)[]` + * @param composedType The composed type to get the string representation for + * @param targetElement The target element + * @returns The composed type string representation + */ + private static string GetComposedTypeTypeString(CodeComposedTypeBase composedType, CodeElement targetElement, string collectionSuffix, bool includeCollectionInformation = true) + { + if (!composedType.Types.Any()) + throw new InvalidOperationException("Composed type should be comprised of at least one type"); + + var typesDelimiter = composedType is CodeUnionType or CodeIntersectionType ? " | " : + throw new InvalidOperationException("Unknown composed type"); + + var returnTypeString = string.Join(typesDelimiter, composedType.Types.Select(x => GetTypescriptTypeString(x, targetElement, includeCollectionInformation: includeCollectionInformation))); + return collectionSuffix.Length > 0 ? $"({returnTypeString}){collectionSuffix}" : returnTypeString; } + private static string GetTypeAlias(CodeType targetType, CodeElement targetElement) { - if (targetElement.GetImmediateParentOfType() is IBlock parentBlock && - parentBlock.Usings - .FirstOrDefault(x => !x.IsExternal && - x.Declaration?.TypeDefinition != null && - x.Declaration.TypeDefinition == targetType.TypeDefinition && - !string.IsNullOrEmpty(x.Alias)) is CodeUsing aliasedUsing) - return aliasedUsing.Alias; - return string.Empty; + var block = targetElement.GetImmediateParentOfType(); + var usings = block is CodeFile cf ? cf.GetChildElements(true).SelectMany(GetUsingsFromCodeElement) : block?.Usings ?? Array.Empty(); + return GetTypeAlias(targetType, usings); + } + + private static string GetTypeAlias(CodeType targetType, IEnumerable usings) + { + var aliasedUsing = usings.FirstOrDefault(x => !x.IsExternal && + x.Declaration?.TypeDefinition != null && + x.Declaration.TypeDefinition == targetType.TypeDefinition && + !string.IsNullOrEmpty(x.Alias)); + + return aliasedUsing != null ? aliasedUsing.Alias : string.Empty; } public override string TranslateType(CodeType type) + { + return TranslateTypescriptType(type); + } + + public static string TranslateTypescriptType(CodeTypeBase type) { return type?.Name switch { - "integer" or "int64" or "float" or "double" or "byte" or "sbyte" or "decimal" => "number", - "binary" or "base64" or "base64url" => "string", - "Guid" => "Guid", - "String" or "Object" or "Boolean" or "Void" or "string" or "object" or "boolean" or "void" => type.Name.ToFirstCharacterLowerCase(), // little casing hack - null => "object", - _ => GetCodeTypeName(type) is string typeName && !string.IsNullOrEmpty(typeName) ? typeName : "object", + TYPE_INTEGER or TYPE_INT or TYPE_INT64 or TYPE_FLOAT or TYPE_DOUBLE or TYPE_BYTE or TYPE_SBYTE or TYPE_DECIMAL => TYPE_NUMBER, + TYPE_BINARY or TYPE_BASE64 or TYPE_BASE64URL => TYPE_STRING, + TYPE_GUID => TYPE_GUID, + TYPE_STRING or TYPE_OBJECT or TYPE_BOOLEAN or TYPE_VOID or TYPE_LOWERCASE_STRING or TYPE_LOWERCASE_OBJECT or TYPE_LOWERCASE_BOOLEAN or TYPE_LOWERCASE_VOID => type.Name.ToFirstCharacterLowerCase(), + null => TYPE_OBJECT, + _ when type is CodeComposedTypeBase composedType => composedType.Name.ToFirstCharacterUpperCase(), + _ when type is CodeType codeType => GetCodeTypeName(codeType) is string typeName && !string.IsNullOrEmpty(typeName) ? typeName : TYPE_OBJECT, + _ => throw new InvalidOperationException($"Unable to translate type {type.Name}") }; } @@ -123,16 +211,22 @@ private static string GetCodeTypeName(CodeType codeType) return (!string.IsNullOrEmpty(codeType.TypeDefinition?.Name) ? codeType.TypeDefinition.Name : codeType.Name).ToFirstCharacterUpperCase(); } -#pragma warning disable CA1822 // Method should be static - public bool IsPrimitiveType(string typeName) + + public static bool IsPrimitiveType(string typeName) { return typeName switch { - "number" or "string" or "byte[]" or "boolean" or "void" => true, + TYPE_NUMBER or + TYPE_LOWERCASE_STRING or + TYPE_BYTE_ARRAY or + TYPE_LOWERCASE_BOOLEAN or + TYPE_LOWERCASE_VOID => true, _ => false, }; } -#pragma warning restore CA1822 // Method should be static + + public static bool IsPrimitiveType(CodeType codeType, CodeComposedTypeBase codeComposedTypeBase) => IsPrimitiveType(GetTypescriptTypeString(codeType, codeComposedTypeBase)); + internal static string RemoveInvalidDescriptionCharacters(string originalDescription) => originalDescription?.Replace("\\", "/", StringComparison.OrdinalIgnoreCase) ?? string.Empty; public override bool WriteShortDescription(IDocumentedElement element, LanguageWriter writer, string prefix = "", string suffix = "") { @@ -184,4 +278,55 @@ private string GetDeprecationComment(IDeprecableElement element) var removalComment = element.Deprecation.RemovalDate is null ? string.Empty : $" and will be removed {element.Deprecation.RemovalDate.Value.Date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}"; return $"@deprecated {element.Deprecation.GetDescription(type => GetTypeString(type, (element as CodeElement)!))}{versionComment}{dateComment}{removalComment}"; } + + public static string GetFactoryMethodName(CodeTypeBase targetClassType, CodeElement currentElement, LanguageWriter? writer = null) + { + var composedType = GetOriginalComposedType(targetClassType); + string targetClassName = TranslateTypescriptType(composedType ?? targetClassType); + var resultName = $"create{targetClassName.ToFirstCharacterUpperCase()}FromDiscriminatorValue"; + if (GetTypescriptTypeString(targetClassType, currentElement, false) is string returnType && targetClassName.EqualsIgnoreCase(returnType)) return resultName; + if (targetClassType is CodeType currentType && currentType.TypeDefinition is CodeInterface definitionClass && GetFactoryMethod(definitionClass, resultName) is { } factoryMethod) + { + var methodName = GetTypescriptTypeString(new CodeType { Name = resultName, TypeDefinition = factoryMethod }, currentElement, false); + return methodName.ToFirstCharacterUpperCase();// static function is aliased + } + throw new InvalidOperationException($"Unable to find factory method for {targetClassType}"); + } + + private static CodeFunction? GetFactoryMethod(CodeInterface definitionClass, string factoryMethodName) + { + return definitionClass.GetImmediateParentOfType(definitionClass)?.FindChildByName(factoryMethodName); + } + + public string GetDeserializationMethodName(CodeTypeBase codeType, CodeMethod method, bool? IsCollection = null) + { + ArgumentNullException.ThrowIfNull(codeType); + ArgumentNullException.ThrowIfNull(method); + var isCollection = IsCollection == true || codeType.IsCollection; + var propertyType = GetTypescriptTypeString(codeType, method, false); + + CodeTypeBase _codeType = GetOriginalComposedType(codeType) is CodeComposedTypeBase composedType ? new CodeType() { Name = composedType.Name, TypeDefinition = composedType } : codeType; + + if (_codeType is CodeType currentType && !string.IsNullOrEmpty(propertyType)) + { + return (currentType.TypeDefinition, isCollection, propertyType) switch + { + (CodeEnum currentEnum, _, _) when currentEnum.CodeEnumObject is not null => $"{(currentEnum.Flags || isCollection ? "getCollectionOfEnumValues" : "getEnumValue")}<{currentEnum.Name.ToFirstCharacterUpperCase()}>({currentEnum.CodeEnumObject.Name.ToFirstCharacterUpperCase()})", + (_, _, _) when StreamTypeName.Equals(propertyType, StringComparison.OrdinalIgnoreCase) => "getByteArrayValue", + (_, true, _) when currentType.TypeDefinition is null => $"getCollectionOfPrimitiveValues<{propertyType}>()", + (_, true, _) => $"getCollectionOfObjectValues<{propertyType.ToFirstCharacterUpperCase()}>({GetFactoryMethodName(_codeType, method)})", + _ => GetDeserializationMethodNameForPrimitiveOrObject(_codeType, propertyType, method) + }; + } + return GetDeserializationMethodNameForPrimitiveOrObject(_codeType, propertyType, method); + } + + private static string GetDeserializationMethodNameForPrimitiveOrObject(CodeTypeBase propType, string propertyTypeName, CodeMethod method) + { + return propertyTypeName switch + { + TYPE_LOWERCASE_STRING or TYPE_STRING or TYPE_LOWERCASE_BOOLEAN or TYPE_BOOLEAN or TYPE_NUMBER or TYPE_GUID or TYPE_DATE or TYPE_DATE_ONLY or TYPE_TIME_ONLY or TYPE_DURATION => $"get{propertyTypeName.ToFirstCharacterUpperCase()}Value()", + _ => $"getObjectValue<{propertyTypeName.ToFirstCharacterUpperCase()}>({GetFactoryMethodName(propType, method)})" + }; + } } diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs index 98d310169d..8006f55698 100644 --- a/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptRelativeImportManager.cs @@ -4,6 +4,7 @@ namespace Kiota.Builder.Writers.TypeScript; public class TypescriptRelativeImportManager(string namespacePrefix, char namespaceSeparator) : RelativeImportManager(namespacePrefix, namespaceSeparator) { + private const string IndexFileName = "index.js"; /// /// Returns the relative import path for the given using and import context namespace. /// @@ -31,5 +32,4 @@ public override (string, string, string) GetRelativeImportPathForUsing(CodeUsing importPath += ".js"; return (importSymbol, codeUsing.Alias, importPath); } - private const string IndexFileName = "index.js"; } diff --git a/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs b/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs index 33d3eb6c45..45f8ccbd72 100644 --- a/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs +++ b/src/Kiota.Builder/Writers/TypeScript/TypeScriptWriter.cs @@ -19,5 +19,7 @@ public TypeScriptWriter(string rootPath, string clientNamespaceName) AddOrReplaceCodeElementWriter(new CodeFileBlockEndWriter(conventionService)); AddOrReplaceCodeElementWriter(new CodeFileDeclarationWriter(conventionService, clientNamespaceName)); AddOrReplaceCodeElementWriter(new CodeConstantWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeUnionTypeWriter(conventionService)); + AddOrReplaceCodeElementWriter(new CodeIntersectionTypeWriter(conventionService)); } } diff --git a/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs b/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs index a3232b9d73..51b4306f49 100644 --- a/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs +++ b/tests/Kiota.Builder.Tests/Export/PublicAPIExportServiceTests.cs @@ -200,7 +200,7 @@ private static void ValidateExportTypeScript(string[] exportContents) Assert.Contains("exportNamespace.models.microsoft.graph.User~~>AdditionalDataHolder; Parsable", exportContents);// captures implemented interfaces Assert.Contains("exportNamespace.models.microsoft.graph.User::|public|id:string", exportContents);// captures property location,type and access inheritance. No getter/setter in TS // NOTE: No constructors in TS - Assert.Contains("exportNamespace.me.meRequestBuilder::|public|ToGetRequestInformation(requestConfiguration?:RequestConfiguration):RequestInformation", exportContents);// captures methods, their parameters(name and types), return and access + Assert.Contains("exportNamespace.me.meRequestBuilder::|public|toGetRequestInformation(requestConfiguration?:RequestConfiguration):RequestInformation", exportContents);// captures methods, their parameters(name and types), return and access Assert.Contains("exportNamespace.models.microsoft.graph::createUserFromDiscriminatorValue(parseNode:ParseNode):User", exportContents);// captures code functions Assert.Contains("exportNamespace.models.microsoft.graph::deserializeIntoUser(User:User={}):Record void>", exportContents);// captures code functions and default params Assert.Contains("exportNamespace.models.microsoft.graph.importance::0000-low", exportContents);// captures enum members diff --git a/tests/Kiota.Builder.Tests/OpenApiSampleFiles/CodeIntersectionTypeSampleYml.cs b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/CodeIntersectionTypeSampleYml.cs new file mode 100644 index 0000000000..218e0ca0e4 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/CodeIntersectionTypeSampleYml.cs @@ -0,0 +1,48 @@ +namespace Kiota.Builder.Tests.OpenApiSampleFiles; + +public static class CodeIntersectionTypeSampleYml +{ + /** + * An OpenAPI 3.0.1 sample document with intersection object types, comprising an intersection of Foo and Bar. + */ + public static readonly string OpenApiYaml = @" +openapi: 3.0.3 +info: + title: FooBar API + description: A sample API that returns an object FooBar which is an intersection of Foo and Bar. + version: 1.0.0 +servers: + - url: https://api.example.com/v1 +paths: + /foobar: + get: + summary: Get a FooBar object + description: Returns an object that is an intersection of Foo and Bar. + responses: + '200': + description: A FooBar object + content: + application/json: + schema: + $ref: '#/components/schemas/FooBar' +components: + schemas: + Foo: + type: object + properties: + foo: + type: string + required: + - foo + Bar: + type: object + properties: + bar: + type: string + required: + - bar + FooBar: + anyOf: + - $ref: '#/components/schemas/Foo' + - $ref: '#/components/schemas/Bar'"; +} diff --git a/tests/Kiota.Builder.Tests/OpenApiSampleFiles/PetsUnion.cs b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/PetsUnion.cs new file mode 100644 index 0000000000..944ac4e776 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/PetsUnion.cs @@ -0,0 +1,69 @@ +namespace Kiota.Builder.Tests.OpenApiSampleFiles; + + +public static class PetsUnion +{ + /** + * An OpenAPI 3.0.1 sample document with a union of objects, comprising a union of Cats and Dogs. + */ + public static readonly string OpenApiYaml = @" +openapi: 3.0.0 +info: + title: Pet API + version: 1.0.0 +paths: + /pets: + patch: + summary: Update a pet + requestBody: + required: true + content: + application/json: + schema: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + discriminator: + propertyName: pet_type + responses: + '200': + description: Updated +components: + schemas: + Pet: + type: object + required: + - pet_type + properties: + pet_type: + type: string + discriminator: + propertyName: pet_type + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + bark: + type: boolean + breed: + type: string + enum: [Dingo, Husky, Retriever, Shepherd] + required: + - pet_type + - bark + - breed + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + hunts: + type: boolean + age: + type: integer + required: + - pet_type + - hunts + - age"; +} diff --git a/tests/Kiota.Builder.Tests/OpenApiSampleFiles/UnionOfPrimitiveAndObjects.cs b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/UnionOfPrimitiveAndObjects.cs new file mode 100644 index 0000000000..0f1f428583 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/UnionOfPrimitiveAndObjects.cs @@ -0,0 +1,86 @@ +namespace Kiota.Builder.Tests.OpenApiSampleFiles; + +public static class UnionOfPrimitiveAndObjects +{ + /** + * An OpenAPI 3.0.1 sample document with union between objects and primitive types + */ + public static readonly string openApiSpec = @" +openapi: 3.0.3 +info: + title: Pet API + description: An API to return pet information. + version: 1.0.0 +servers: + - url: http://localhost:8080 + description: Local server + +paths: + /pet: + get: + summary: Get pet information + operationId: getPet + responses: + '200': + description: Successful response + content: + application/json: + schema: + type: object + properties: + request_id: + type: string + example: ""123e4567-e89b-12d3-a456-426614174000"" + data: + oneOf: + - $ref: '#/components/schemas/Cat' + - $ref: '#/components/schemas/Dog' + - type: array + items: + $ref: '#/components/schemas/Dog' + - type: string + description: Error status + example: ""An error occurred while processing the request."" + - type: integer + description: Error code + example: 409 + '400': + description: Bad Request + '500': + description: Internal Server Error + +components: + schemas: + Pet: + type: object + required: + - name + - age + properties: + name: + type: string + example: ""Fluffy"" + age: + type: integer + example: 4 + + Cat: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + favoriteToy: + type: string + example: ""Mouse"" + + Dog: + allOf: + - $ref: '#/components/schemas/Pet' + - type: object + properties: + breed: + type: string + example: ""Labrador"" +"; + +} diff --git a/tests/Kiota.Builder.Tests/OpenApiSampleFiles/UnionOfPrimitiveValuesSample.cs b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/UnionOfPrimitiveValuesSample.cs new file mode 100644 index 0000000000..e92e444b21 --- /dev/null +++ b/tests/Kiota.Builder.Tests/OpenApiSampleFiles/UnionOfPrimitiveValuesSample.cs @@ -0,0 +1,31 @@ +namespace Kiota.Builder.Tests.OpenApiSampleFiles; + + +public static class UnionOfPrimitiveValuesSample +{ + /** + * An OpenAPI 3.0.1 sample document with a union of primitive values, comprising a union of stings and numbers. + */ + public static readonly string Yaml = @" +openapi: 3.0.1 +info: + title: Example of UnionTypes + version: 1.0.0 +paths: + /primitives: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/primitives' +components: + schemas: + primitives: + oneOf: + - type: string + - type: number"; + +} diff --git a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs index 77b1b957f1..8cbd26de8b 100644 --- a/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs +++ b/tests/Kiota.Builder.Tests/Refiners/TypeScriptLanguageRefinerTests.cs @@ -1,16 +1,32 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.Refiners; - +using Kiota.Builder.Tests.OpenApiSampleFiles; +using Microsoft.Extensions.Logging; +using Moq; using Xunit; namespace Kiota.Builder.Tests.Refiners; -public class TypeScriptLanguageRefinerTests +public sealed class TypeScriptLanguageRefinerTests : IDisposable { + private readonly HttpClient _httpClient = new(); + + private readonly List _tempFiles = new(); + public void Dispose() + { + foreach (var file in _tempFiles) + File.Delete(file); + _httpClient.Dispose(); + GC.SuppressFinalize(this); + } + private readonly CodeNamespace root; private readonly CodeNamespace graphNS; public TypeScriptLanguageRefinerTests() @@ -700,7 +716,6 @@ public async Task ReplaceRequestConfigsQueryParamsAsync() Assert.Single(testNS.Constants.Where(static x => x.IsOfKind(CodeConstantKind.QueryParametersMapper))); } - [Fact] public async Task GeneratesCodeFilesAsync() { @@ -841,6 +856,125 @@ public async Task AddsUsingForUntypedNodeAsync() Assert.Equal("@microsoft/kiota-abstractions", nodeUsing[0].Declaration.Name); } [Fact] + public async Task ParsesAndRefinesUnionOfPrimitiveValuesAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, UnionOfPrimitiveValuesSample.Yaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Primitives", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("Primitives", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.primitives"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("primitivesRequestBuilder", false); + Assert.NotNull(modelCodeFile); + var unionType = modelCodeFile.GetChildElements().Where(x => x is CodeFunction function && TypeScriptRefiner.GetOriginalComposedType(function.OriginalLocalMethod.ReturnType) is not null).ToList(); + Assert.True(unionType.Count > 0); + } + + [Fact] + public void GetOriginalComposedType_ReturnsNull_WhenElementIsNull() + { + var codeElement = new Mock(); + var result = TypeScriptRefiner.GetOriginalComposedType(codeElement.Object); + Assert.Null(result); + } + + [Fact] + public void GetOriginalComposedType_ReturnsComposedType_WhenElementIsComposedType() + { + var composedType = new Mock(); + var result = TypeScriptRefiner.GetOriginalComposedType(composedType.Object); + Assert.Equal(composedType.Object, result); + } + + [Fact] + public void GetOriginalComposedType_ReturnsComposedType_WhenElementIsParameter() + { + var composedType = new Mock(); + + var codeClass = new CodeClass + { + OriginalComposedType = composedType.Object + }; + + var codeType = new CodeType() + { + TypeDefinition = codeClass, + }; + + var parameter = new CodeParameter() { Type = codeType }; + + var result = TypeScriptRefiner.GetOriginalComposedType(parameter); + Assert.Equal(composedType.Object, result); + } + + [Fact] + public void GetOriginalComposedType_ReturnsComposedType_WhenElementIsCodeType() + { + var composedType = new Mock(); + + var codeClass = new CodeClass + { + OriginalComposedType = composedType.Object + }; + + var codeType = new CodeType() + { + TypeDefinition = codeClass, + }; + + var result = TypeScriptRefiner.GetOriginalComposedType(codeType); + Assert.Equal(composedType.Object, result); + } + + [Fact] + public void GetOriginalComposedType_ReturnsComposedType_WhenElementIsCodeClass() + { + var composedType = new Mock(); + + CodeElement codeClass = new CodeClass + { + OriginalComposedType = composedType.Object + }; + + var result = TypeScriptRefiner.GetOriginalComposedType(codeClass); + Assert.Equal(composedType.Object, result); + } + + [Fact] + public void GetOriginalComposedType_ReturnsComposedType_WhenElementIsCodeInterface() + { + var composedType = new Mock(); + + var codeClass = new CodeClass + { + OriginalComposedType = composedType.Object + }; + + CodeElement codeInterface = new CodeInterface() + { + OriginalClass = codeClass, + }; + + var result = TypeScriptRefiner.GetOriginalComposedType(codeInterface); + Assert.Equal(composedType.Object, result); + } + [Fact] public async Task AddsUsingForUntypedNodeInReturnTypeAsync() { var requestBuilderClass = root.AddClass(new CodeClass() { Name = "NodeRequestBuilder" }).First(); diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs index 1eeca309cf..b2894a186a 100644 --- a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeFunctionWriterTests.cs @@ -1,14 +1,20 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading.Tasks; using Kiota.Builder.CodeDOM; using Kiota.Builder.Configuration; using Kiota.Builder.Extensions; using Kiota.Builder.Refiners; +using Kiota.Builder.Tests.OpenApiSampleFiles; using Kiota.Builder.Writers; +using Microsoft.Extensions.Logging; +using Moq; using Xunit; +using static Kiota.Builder.Refiners.TypeScriptRefiner; namespace Kiota.Builder.Tests.Writers.TypeScript; public sealed class CodeFunctionWriterTests : IDisposable @@ -20,6 +26,9 @@ public sealed class CodeFunctionWriterTests : IDisposable private readonly CodeNamespace root; private const string MethodName = "methodName"; private const string ReturnTypeName = "Somecustomtype"; + private readonly HttpClient _httpClient = new(); + private readonly List _tempFiles = new(); + private const string IndexFileName = "index"; public CodeFunctionWriterTests() { @@ -30,6 +39,9 @@ public CodeFunctionWriterTests() } public void Dispose() { + foreach (var file in _tempFiles) + File.Delete(file); + _httpClient.Dispose(); tw?.Dispose(); GC.SuppressFinalize(this); } @@ -112,7 +124,7 @@ public async Task WritesModelFactoryBodyAsync() parentNS.TryAddCodeFile("foo", factoryFunction); writer.Write(factoryFunction); var result = tw.ToString(); - Assert.Contains("const mappingValueNode = parseNode.getChildNode(\"@odata.type\")", result); + Assert.Contains("const mappingValueNode = parseNode?.getChildNode(\"@odata.type\")", result); Assert.Contains("if (mappingValueNode) {", result); Assert.Contains("const mappingValue = mappingValueNode.getStringValue()", result); Assert.Contains("if (mappingValue) {", result); @@ -1118,4 +1130,389 @@ public async Task WritesConstructorWithEnumValueAsync() var result = tw.ToString(); Assert.Contains($" ?? {codeEnum.CodeEnumObject.Name.ToFirstCharacterUpperCase()}.{defaultValue.CleanupSymbolName()}", result);//ensure symbol is cleaned up } + [Fact] + public async Task Writes_UnionOfPrimitiveValues_FactoryFunctionAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, UnionOfPrimitiveValuesSample.Yaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Primitives", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("Primitives", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.primitives"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("primitivesRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + /* + \/** + * Creates a new instance of the appropriate class based on discriminator value + * @returns {ValidationError_errors_value} + *\/ + export function createPrimitivesFromDiscriminatorValue(parseNode: ParseNode | undefined) : Primitives | undefined { + return parseNode?.getNumberValue() ?? parseNode?.getStringValue(); + } + */ + + // Test Factory function + var factoryFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && GetOriginalComposedType(function.OriginalLocalMethod.ReturnType) is not null); + Assert.True(factoryFunction is not null); + writer.Write(factoryFunction); + var result = tw.ToString(); + Assert.Contains("return parseNode?.getNumberValue() ?? parseNode?.getStringValue();", result); + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + + [Fact] + public async Task Writes_UnionOfObjects_FactoryMethodAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, PetsUnion.OpenApiYaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Pets", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("Pets", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.pets"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("petsRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + // Test Serializer function + var factoryFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && function.OriginalLocalMethod.Kind == CodeMethodKind.Factory); + Assert.True(factoryFunction is not null); + writer.Write(factoryFunction); + var result = tw.ToString(); + Assert.Contains("if (mappingValue)", result); + Assert.Contains("case \"Cat\":", result); + Assert.Contains("return deserializeIntoCat;", result); + Assert.Contains("case \"Dog\":", result); + Assert.Contains("return deserializeIntoDog;", result); + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + + [Fact] + public async Task Writes_UnionOfPrimitiveValues_SerializerFunctionAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, UnionOfPrimitiveValuesSample.Yaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Primitives", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("Primitives", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.primitives"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("primitivesRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + // Test Serializer function + var serializerFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && GetOriginalComposedType(function.OriginalLocalMethod.Parameters.FirstOrDefault(x => GetOriginalComposedType(x) is not null)) is not null); + Assert.True(serializerFunction is not null); + 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("break", serializerFunctionStr); + AssertExtensions.CurlyBracesAreClosed(serializerFunctionStr, 1); + } + + [Fact] + public async Task Writes_UnionOfObjects_SerializerFunctionsAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, PetsUnion.OpenApiYaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "Pets", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("Pets", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.pets"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("petsRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + // Test Serializer function + var serializerFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && function.OriginalLocalMethod.Kind == CodeMethodKind.Serializer); + Assert.True(serializerFunction is not null); + writer.Write(serializerFunction); + var serializerFunctionStr = tw.ToString(); + Assert.Contains("return", serializerFunctionStr); + Assert.Contains("switch", serializerFunctionStr); + Assert.Contains("case \"Cat\":", serializerFunctionStr); + Assert.Contains("case \"Dog\":", serializerFunctionStr); + Assert.Contains("break", serializerFunctionStr); + AssertExtensions.CurlyBracesAreClosed(serializerFunctionStr, 1); + } + + [Fact] + public async Task Writes_CodeIntersectionType_FactoryMethodAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, CodeIntersectionTypeSampleYml.OpenApiYaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "FooBar", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("FooBar", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.foobar"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("foobarRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + // Test Factory Function + var factoryFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && function.OriginalLocalMethod.Kind == CodeMethodKind.Factory); + Assert.True(factoryFunction is not null); + writer.Write(factoryFunction); + var result = tw.ToString(); + Assert.Contains("export function createFooBarFromDiscriminatorValue(", result); + Assert.Contains("return deserializeIntoFooBar;", result); + AssertExtensions.CurlyBracesAreClosed(result, 1); + } + + [Fact] + public async Task Writes_CodeIntersectionType_DeserializerFunctionsAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, CodeIntersectionTypeSampleYml.OpenApiYaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "FooBar", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("FooBar", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.foobar"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("foobarRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + // Test Deserializer function + var deserializerFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && function.OriginalLocalMethod.Kind == CodeMethodKind.Deserializer); + Assert.True(deserializerFunction is not null); + writer.Write(deserializerFunction); + var serializerFunctionStr = tw.ToString(); + Assert.Contains("...deserializeIntoBar(fooBar as Bar),", serializerFunctionStr); + Assert.Contains("...deserializeIntoFoo(fooBar as Foo),", serializerFunctionStr); + AssertExtensions.CurlyBracesAreClosed(serializerFunctionStr, 1); + } + + [Fact] + public async Task Writes_CodeIntersectionType_SerializerFunctionsAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var tempFilePath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); + await File.WriteAllTextAsync(tempFilePath, CodeIntersectionTypeSampleYml.OpenApiYaml); + var mockLogger = new Mock>(); + var builder = new KiotaBuilder(mockLogger.Object, new GenerationConfiguration { ClientClassName = "FooBar", Serializers = ["none"], Deserializers = ["none"] }, _httpClient); + await using var fs = new FileStream(tempFilePath, FileMode.Open); + var document = await builder.CreateOpenApiDocumentAsync(fs); + var node = builder.CreateUriSpace(document); + builder.SetApiRootUrl(); + var codeModel = builder.CreateSourceModel(node); + var rootNS = codeModel.FindNamespaceByName("ApiSdk"); + Assert.NotNull(rootNS); + var clientBuilder = rootNS.FindChildByName("FooBar", false); + Assert.NotNull(clientBuilder); + var constructor = clientBuilder.Methods.FirstOrDefault(static x => x.IsOfKind(CodeMethodKind.ClientConstructor)); + Assert.NotNull(constructor); + Assert.Empty(constructor.SerializerModules); + Assert.Empty(constructor.DeserializerModules); + await ILanguageRefiner.RefineAsync(generationConfiguration, rootNS); + Assert.NotNull(rootNS); + var modelsNS = rootNS.FindNamespaceByName("ApiSdk.foobar"); + Assert.NotNull(modelsNS); + var modelCodeFile = modelsNS.FindChildByName("foobarRequestBuilder", false); + Assert.NotNull(modelCodeFile); + + // Test Serializer function + var serializerFunction = modelCodeFile.GetChildElements().FirstOrDefault(x => x is CodeFunction function && function.OriginalLocalMethod.Kind == CodeMethodKind.Serializer); + Assert.True(serializerFunction is not null); + writer.Write(serializerFunction); + var serializerFunctionStr = tw.ToString(); + Assert.Contains("serializeBar(writer, fooBar as Bar);", serializerFunctionStr); + Assert.Contains("serializeFoo(writer, fooBar as Foo);", serializerFunctionStr); + AssertExtensions.CurlyBracesAreClosed(serializerFunctionStr, 1); + } + + [Fact] + public async Task Writes_CodeUnionBetweenObjectsAndPrimitiveTypes_SerializerAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var parentClass = TestHelper.CreateModelClassInModelsNamespace(generationConfiguration, root, "parentClass"); + var method = TestHelper.CreateMethod(parentClass, MethodName, ReturnTypeName); + method.Kind = CodeMethodKind.Serializer; + method.IsAsync = false; + + var modelNameSpace = root.AddNamespace($"{root.Name}.models"); + var composedType = new CodeUnionType { Name = "Union" }; + composedType.AddType(new CodeType { Name = "string" }, new CodeType { Name = "int" }, + new CodeType + { + Name = "ArrayOfObjects", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + TypeDefinition = TestHelper.CreateModelClass(modelNameSpace, "ArrayOfObjects") + }, + new CodeType + { + Name = "SingleObject", + TypeDefinition = TestHelper.CreateModelClass(modelNameSpace, "SingleObject") + }); + parentClass.AddProperty(new CodeProperty + { + Name = "property", + Type = composedType + }); + + + TestHelper.AddSerializationPropertiesToModelClass(parentClass); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.TypeScript }, root); + var serializeFunction = root.FindChildByName($"Serialize{parentClass.Name.ToFirstCharacterUpperCase()}"); + Assert.NotNull(serializeFunction); + var parentNS = serializeFunction.GetImmediateParentOfType(); + Assert.NotNull(parentNS); + parentNS.TryAddCodeFile("foo", serializeFunction); + writer.Write(serializeFunction); + var result = tw.ToString(); + + Assert.Contains("case typeof parentClass.property === \"string\"", result); + Assert.Contains("writer.writeStringValue(\"property\", parentClass.property as string);", result); + Assert.Contains("case typeof parentClass.property === \"number\"", result); + Assert.Contains("writer.writeNumberValue(\"property\", parentClass.property as number);", result); + Assert.Contains( + "writer.writeCollectionOfObjectValues(\"property\", parentClass.property as ArrayOfObjects[] | undefined | null", + result); + Assert.Contains( + "writer.writeObjectValue(\"property\", parentClass.property as SingleObject | undefined | null", + result); + Assert.Contains("writeStringValue", result); + Assert.Contains("writeCollectionOfPrimitiveValues", result); + Assert.Contains("writeCollectionOfObjectValues", result); + Assert.Contains("serializeSomeComplexType", result); + Assert.Contains("writeEnumValue", result); + Assert.Contains("writer.writeAdditionalData", result); + Assert.Contains("definedInParent", result, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task Writes_CodeUnionBetweenObjectsAndPrimitiveTypes_DeserializerAsync() + { + var generationConfiguration = new GenerationConfiguration { Language = GenerationLanguage.TypeScript }; + var parentClass = TestHelper.CreateModelClassInModelsNamespace(generationConfiguration, root, "parentClass"); + var method = TestHelper.CreateMethod(parentClass, MethodName, ReturnTypeName); + method.Kind = CodeMethodKind.Deserializer; + method.IsAsync = false; + + var modelNameSpace = root.AddNamespace($"{root.Name}.models"); + var composedType = new CodeUnionType { Name = "Union" }; + composedType.AddType(new CodeType { Name = "string" }, new CodeType { Name = "int" }, + new CodeType + { + Name = "ArrayOfObjects", + CollectionKind = CodeTypeBase.CodeTypeCollectionKind.Array, + TypeDefinition = TestHelper.CreateModelClass(modelNameSpace, "ArrayOfObjects") + }, + new CodeType + { + Name = "SingleObject", + TypeDefinition = TestHelper.CreateModelClass(modelNameSpace, "SingleObject") + }); + parentClass.AddProperty(new CodeProperty + { + Name = "property", + Type = composedType + }); + + TestHelper.AddSerializationPropertiesToModelClass(parentClass); + await ILanguageRefiner.RefineAsync(new GenerationConfiguration { Language = GenerationLanguage.TypeScript }, root); + var serializeFunction = root.FindChildByName($"DeserializeInto{parentClass.Name.ToFirstCharacterUpperCase()}"); + Assert.NotNull(serializeFunction); + var parentNS = serializeFunction.GetImmediateParentOfType(); + Assert.NotNull(parentNS); + parentNS.TryAddCodeFile("foo", serializeFunction); + writer.Write(serializeFunction); + var result = tw.ToString(); + + Assert.Contains("\"property\": n => { parentClass.property = n.getCollectionOfObjectValues(createArrayOfObjectsFromDiscriminatorValue) ?? n.getNumberValue() ?? n.getObjectValue(createSingleObjectFromDiscriminatorValue) ?? n.getStringValue(); }", result); + } } + diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeIntersectionTypeWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeIntersectionTypeWriterTests.cs new file mode 100644 index 0000000000..1b6468a1c2 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeIntersectionTypeWriterTests.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using Kiota.Builder.CodeDOM; +using Moq; +using Xunit; + +namespace Kiota.Builder.Writers.TypeScript.Tests; +public sealed class CodeIntersectionTypeWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeIntersectionTypeWriter codeElementWriter; + + public CodeIntersectionTypeWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName); + codeElementWriter = new CodeIntersectionTypeWriter(new TypeScriptConventionService()); + tw = new StringWriter(); + writer.SetTextWriter(tw); + } + + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void WriteCodeElement_ShouldThrowArgumentNullException_WhenCodeElementIsNull() + { + Assert.Throws(() => codeElementWriter.WriteCodeElement(null, writer)); + } + + [Fact] + public void WriteCodeElement_ShouldThrowArgumentNullException_WhenWriterIsNull() + { + var composedType = new Mock(); + Assert.Throws(() => codeElementWriter.WriteCodeElement(composedType.Object, null)); + } + + [Fact] + public void WriteCodeElement_ShouldThrowInvalidOperationException_WhenTypesIsEmpty() + { + var composedType = new Mock(); + Assert.Throws(() => codeElementWriter.WriteCodeElement(composedType.Object, writer)); + } + + [Fact] + public void WriteCodeElement_ShouldWriteCorrectOutput_WhenTypesIsNotEmpty() + { + CodeIntersectionType composedType = new CodeIntersectionType() { Name = "Test" }; + composedType.AddType(new CodeType { Name = "Type1" }); + composedType.AddType(new CodeType { Name = "Type2" }); + + var root = CodeNamespace.InitRootNamespace(); + var ns = root.AddNamespace("graphtests.models"); + ns.TryAddCodeFile(DefaultPath, composedType); + + codeElementWriter.WriteCodeElement(composedType, writer); + + var result = tw.ToString(); + Assert.Contains("export type Test = Type1 | Type2;", result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeUnionTypeWriterTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeUnionTypeWriterTests.cs new file mode 100644 index 0000000000..84f5e46104 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/CodeUnionTypeWriterTests.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using Kiota.Builder.CodeDOM; +using Moq; +using Xunit; + +namespace Kiota.Builder.Writers.TypeScript.Tests; +public sealed class CodeUnionTypeWriterTests : IDisposable +{ + private const string DefaultPath = "./"; + private const string DefaultName = "name"; + private readonly StringWriter tw; + private readonly LanguageWriter writer; + private readonly CodeUnionTypeWriter codeElementWriter; + + public CodeUnionTypeWriterTests() + { + writer = LanguageWriter.GetLanguageWriter(GenerationLanguage.TypeScript, DefaultPath, DefaultName); + codeElementWriter = new CodeUnionTypeWriter(new TypeScriptConventionService()); + tw = new StringWriter(); + writer.SetTextWriter(tw); + } + + public void Dispose() + { + tw?.Dispose(); + GC.SuppressFinalize(this); + } + + [Fact] + public void WriteCodeElement_ShouldThrowArgumentNullException_WhenCodeElementIsNull() + { + Assert.Throws(() => codeElementWriter.WriteCodeElement(null, writer)); + } + + [Fact] + public void WriteCodeElement_ShouldThrowArgumentNullException_WhenWriterIsNull() + { + var composedType = new Mock(); + Assert.Throws(() => codeElementWriter.WriteCodeElement(composedType.Object, null)); + } + + [Fact] + public void WriteCodeElement_ShouldThrowInvalidOperationException_WhenTypesIsEmpty() + { + var composedType = new Mock(); + Assert.Throws(() => codeElementWriter.WriteCodeElement(composedType.Object, writer)); + } + + [Fact] + public void WriteCodeElement_ShouldWriteCorrectOutput_WhenTypesIsNotEmpty() + { + CodeUnionType composedType = new CodeUnionType() { Name = "Test" }; + composedType.AddType(new CodeType { Name = "Type1" }); + composedType.AddType(new CodeType { Name = "Type2" }); + + var root = CodeNamespace.InitRootNamespace(); + var ns = root.AddNamespace("graphtests.models"); + ns.TryAddCodeFile(DefaultPath, composedType); + + codeElementWriter.WriteCodeElement(composedType, writer); + + var result = tw.ToString(); + Assert.Contains("export type Test = Type1 | Type2;", result); + } +} diff --git a/tests/Kiota.Builder.Tests/Writers/TypeScript/TypeScriptConventionServiceTests.cs b/tests/Kiota.Builder.Tests/Writers/TypeScript/TypeScriptConventionServiceTests.cs new file mode 100644 index 0000000000..3a311766b8 --- /dev/null +++ b/tests/Kiota.Builder.Tests/Writers/TypeScript/TypeScriptConventionServiceTests.cs @@ -0,0 +1,112 @@ +using System.Linq; +using Kiota.Builder.CodeDOM; +using Kiota.Builder.Writers.TypeScript; +using Xunit; + +namespace Kiota.Builder.Tests.Writers.TypeScript; + +public class TypeScriptConventionServiceTests +{ + + [Fact] + public void TranslateType_ThrowsArgumentNullException_WhenComposedTypeIsNull() + { + var result = TypeScriptConventionService.TranslateTypescriptType(null); + Assert.Equal(TypeScriptConventionService.TYPE_OBJECT, result); + } + + [Fact] + public void TranslateType_ReturnsCorrectTranslation_WhenComposedTypeIsNotNull() + { + var composedType = new CodeUnionType { Name = "test" }; + var result = TypeScriptConventionService.TranslateTypescriptType(composedType); + Assert.Equal("Test", result); + } + + private static CodeType CurrentType() + { + CodeType currentType = new CodeType { Name = "SomeType" }; + var root = CodeNamespace.InitRootNamespace(); + var parentClass = root.AddClass(new CodeClass { Name = "ParentClass" }).First(); + currentType.Parent = parentClass; + return currentType; + } + + [Fact] + public void IsComposedOfPrimitives_ShouldBeTrue_WhenComposedOfPrimitives() + { + var composedType = new CodeUnionType { Name = "test", Parent = CurrentType() }; + composedType.AddType(new CodeType { Name = "string", IsExternal = true }); + composedType.AddType(new CodeType { Name = "integer", IsExternal = true }); + Assert.True(composedType.IsComposedOfPrimitives(TypeScriptConventionService.IsPrimitiveType)); + } + + [Fact] + public void IsComposedOfPrimitives_ShouldBeFalse_WhenNotComposedOfPrimitives() + { + var composedType = new CodeUnionType { Name = "test", Parent = CurrentType() }; + composedType.AddType(new CodeType { Name = "string", IsExternal = true }); + var td = new CodeClass { Name = "SomeClass" }; + composedType.AddType(new CodeType { Name = "SomeCustomObject", IsExternal = false, TypeDefinition = td }); + Assert.False(composedType.IsComposedOfPrimitives(TypeScriptConventionService.IsPrimitiveType)); + } + + [Fact] + public void IsComposedOfObjectsAndPrimitives_OnlyPrimitives_ReturnsFalse() + { + // Arrange + var composedType = new CodeUnionType { Name = "test", Parent = CurrentType() }; + + composedType.AddType(new CodeType { Name = "string", IsExternal = true }); + + // Act + var result = composedType.IsComposedOfObjectsAndPrimitives(TypeScriptConventionService.IsPrimitiveType); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsComposedOfObjectsAndPrimitives_OnlyObjects_ReturnsFalse() + { + var composedType = new CodeUnionType { Name = "test", Parent = CurrentType() }; + + var td = new CodeClass { Name = "SomeClass" }; + composedType.AddType(new CodeType { Name = "SomeCustomObject", IsExternal = false, TypeDefinition = td }); + + // Act + var result = composedType.IsComposedOfObjectsAndPrimitives(TypeScriptConventionService.IsPrimitiveType); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsComposedOfObjectsAndPrimitives_BothPrimitivesAndObjects_ReturnsTrue() + { + var composedType = new CodeUnionType { Name = "test", Parent = CurrentType() }; + // Add primitive + composedType.AddType(new CodeType { Name = "string", IsExternal = true }); + var td = new CodeClass { Name = "SomeClass" }; + composedType.AddType(new CodeType { Name = "SomeCustomObject", IsExternal = false, TypeDefinition = td }); + + // Act + var result = composedType.IsComposedOfObjectsAndPrimitives(TypeScriptConventionService.IsPrimitiveType); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsComposedOfObjectsAndPrimitives_EmptyTypes_ReturnsFalse() + { + // Arrange + var composedType = new CodeUnionType { Name = "test", Parent = CurrentType() }; + + // Act + var result = composedType.IsComposedOfObjectsAndPrimitives(TypeScriptConventionService.IsPrimitiveType); + + // Assert + Assert.False(result); + } +}