Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix serialization and deserialization of composed types in TS #5358

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Fixed a bug where collection/array of primitive types members for union/intersection types would be ignored. [#5283](https://github.com/microsoft/kiota/issues/5283)
- Fixed a when generating a plugin when only an operation is selected in the root node in the extension. [#5300](https://github.com/microsoft/kiota/issues/5300)
- Fix serialization of composed types in TypeScript. [#5353](https://github.com/microsoft/kiota/issues/5353)

## [1.18.0] - 2024-09-05

Expand Down
7 changes: 0 additions & 7 deletions src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,4 @@ public DeprecationInformation? Deprecation
set;
}

public bool IsComposedOfPrimitives(Func<CodeType, CodeComposedTypeBase, bool> checkIfPrimitive) => Types.All(x => checkIfPrimitive(x, this));
public bool IsComposedOfObjectsAndPrimitives(Func<CodeType, CodeComposedTypeBase, bool> checkIfPrimitive)
{
// Count the number of primitives in Types
return Types.Any(x => checkIfPrimitive(x, this)) && Types.Any(x => !checkIfPrimitive(x, this));
}

}
177 changes: 10 additions & 167 deletions src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
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
Expand All @@ -21,10 +20,6 @@
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,
Expand Down Expand Up @@ -169,31 +164,10 @@
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)
Expand Down Expand Up @@ -303,150 +277,19 @@

private static CodeFile? GenerateModelCodeFile(CodeInterface codeInterface, CodeNamespace codeNamespace)
{
var functions = GetSerializationAndFactoryFunctions(codeInterface, codeNamespace).ToArray();

if (functions.Length == 0)
return null;

var composedType = GetOriginalComposedType(codeInterface);
var elements = composedType is null ? new List<CodeElement> { codeInterface }.Concat(functions) : GetCodeFileElementsForComposedType(codeInterface, codeNamespace, composedType, functions);

return codeNamespace.TryAddCodeFile(codeInterface.Name, elements.ToArray());
}

private static IEnumerable<CodeFunction> GetSerializationAndFactoryFunctions(CodeInterface codeInterface, CodeNamespace codeNamespace)
{
return codeNamespace.GetChildElements(true)
.OfType<CodeFunction>()
.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);
}
var functions = codeNamespace.GetChildElements(true).OfType<CodeFunction>().Where(codeFunction =>
codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Deserializer or CodeMethodKind.Serializer &&
codeFunction.OriginalLocalMethod.Parameters
.Any(x => x.Type.Name.Equals(codeInterface.Name, StringComparison.OrdinalIgnoreCase)) ||

private static bool IsFactoryFunction(CodeFunction codeFunction, CodeInterface codeInterface, CodeNamespace codeNamespace)
{
return codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Factory &&
codeFunction.OriginalLocalMethod.Kind is CodeMethodKind.Factory &&
codeInterface.Name.EqualsIgnoreCase(codeFunction.OriginalMethodParentClass.Name) &&
codeFunction.OriginalMethodParentClass.IsChildOf(codeNamespace);
}

private static List<CodeElement> GetCodeFileElementsForComposedType(CodeInterface codeInterface, CodeNamespace codeNamespace, CodeComposedTypeBase composedType, CodeFunction[] functions)
{
var children = new List<CodeElement>(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<CodeElement> elements, CodeMethodKind kind)
{
return elements.OfType<CodeFunction>().FirstOrDefault(function => function.OriginalLocalMethod.IsOfKind(kind));
}

private static void RemoveUnusedDeserializerImport(List<CodeElement> children, CodeFunction factoryFunction)
{
if (FindFunctionOfKind(children, CodeMethodKind.Deserializer) is { } deserializerMethod)
factoryFunction.RemoveUsingsByDeclarationName(deserializerMethod.Name);
}

private static void ReplaceFactoryMethodForComposedType(CodeComposedTypeBase composedType, List<CodeElement> 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<CodeElement> 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<CodeInterface>()
.Select(static x => x.OriginalClass)
.OfType<CodeClass>())
{
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<CodeElement> 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);
}
codeFunction.OriginalMethodParentClass.IsChildOf(codeNamespace)
).ToArray();

// 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.",
},
};
if (functions.Length == 0)
return null;
return codeNamespace.TryAddCodeFile(codeInterface.Name, [codeInterface, .. functions]);
}

public static CodeComposedTypeBase? GetOriginalComposedType(CodeElement element)
Expand Down Expand Up @@ -704,7 +547,7 @@
m.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator) &&
m.Parameters.Any(IsMultipartBody);
// for Kiota abstraction library if the code is not required for runtime purposes e.g. interfaces then the IsErasable flag is set to true
private static readonly AdditionalUsingEvaluator[] defaultUsingEvaluators = {

Check warning on line 550 in src/Kiota.Builder/Refiners/TypeScriptRefiner.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this field to reduce its Cognitive Complexity from 16 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
new (static x => x is CodeMethod method && method.Kind is CodeMethodKind.ClientConstructor,
AbstractionsPackageName, true, "RequestAdapter"),
new (static x => x is CodeMethod method && method.Kind is CodeMethodKind.RequestGenerator,
Expand Down
4 changes: 2 additions & 2 deletions src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
}
}

private void WriteNavigationMetadataConstant(CodeConstant codeElement, LanguageWriter writer)

Check warning on line 38 in src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'WriteNavigationMetadataConstant' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
if (codeElement.OriginalCodeElement is not CodeClass codeClass) throw new InvalidOperationException("Original CodeElement cannot be null");

Check warning on line 40 in src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs

View workflow job for this annotation

GitHub Actions / Build

Define a constant instead of using this literal 'Original CodeElement cannot be null' 4 times. (https://rules.sonarsource.com/csharp/RSPEC-1192)
if (codeElement.Parent is not CodeFile parentCodeFile || parentCodeFile.FindChildByName<CodeInterface>(codeElement.Name.Replace(CodeConstant.NavigationMetadataSuffix, string.Empty, StringComparison.Ordinal), false) is not CodeInterface currentInterface)
throw new InvalidOperationException("Couldn't find the associated interface for the navigation metadata constant");
var navigationMethods = codeClass.Methods
Expand Down Expand Up @@ -84,7 +84,7 @@
$"_{original.ToUpperInvariant()}" : // to avoid emitting strings that can't be minified
original.ToUpperInvariant();

private void WriteRequestsMetadataConstant(CodeConstant codeElement, LanguageWriter writer)

Check warning on line 87 in src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs

View workflow job for this annotation

GitHub Actions / Build

Refactor this method to reduce its Cognitive Complexity from 34 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
{
if (codeElement.OriginalCodeElement is not CodeClass codeClass) throw new InvalidOperationException("Original CodeElement cannot be null");
if (codeClass.Methods
Expand Down Expand Up @@ -114,7 +114,7 @@
writer.StartBlock("errorMappings: {");
foreach (var errorMapping in executorMethod.ErrorMappings)
{
writer.WriteLine($"{GetErrorMappingKey(errorMapping.Key)}: {GetFactoryMethodName(errorMapping.Value, codeElement, writer)} as ParsableFactory<Parsable>,");
writer.WriteLine($"{GetErrorMappingKey(errorMapping.Key)}: {conventions.GetFactoryMethodName(errorMapping.Value, codeElement, writer)} as ParsableFactory<Parsable>,");
}
writer.CloseBlock("},");
}
Expand Down Expand Up @@ -156,7 +156,7 @@
return "\"setContentFromParsable\"";
return "\"setContentFromScalar\"";
}
private string? GetBodySerializer(CodeParameter requestBody)

Check warning on line 159 in src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'GetBodySerializer' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
if (requestBody.Type is CodeType currentType && (currentType.TypeDefinition is CodeInterface || currentType.Name.Equals("MultipartBody", StringComparison.OrdinalIgnoreCase)))
{
Expand All @@ -169,7 +169,7 @@
if (isVoid) return string.Empty;
var typeName = conventions.TranslateType(codeElement.ReturnType);
if (isStream || IsPrimitiveType(typeName)) return $" \"{typeName}\"";
return $" {GetFactoryMethodName(codeElement.ReturnType, codeElement, writer)}";
return $" {conventions.GetFactoryMethodName(codeElement.ReturnType, codeElement, writer)}";
}
private string GetReturnTypeWithoutCollectionSymbol(CodeMethod codeElement, string fullTypeName)
{
Expand All @@ -179,7 +179,7 @@
return conventions.GetTypeString(clone, codeElement);
}

private string GetSendRequestMethodName(bool isVoid, bool isStream, bool isCollection, bool isPrimitive, bool isEnum)

Check warning on line 182 in src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'GetSendRequestMethodName' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
return (isVoid, isEnum, isPrimitive || isStream, isCollection) switch
{
Expand All @@ -193,7 +193,7 @@
};
}

private void WriteUriTemplateConstant(CodeConstant codeElement, LanguageWriter writer)

Check warning on line 196 in src/Kiota.Builder/Writers/TypeScript/CodeConstantWriter.cs

View workflow job for this annotation

GitHub Actions / Build

Make 'WriteUriTemplateConstant' a static method. (https://rules.sonarsource.com/csharp/RSPEC-2325)
{
writer.WriteLine($"export const {codeElement.Name.ToFirstCharacterUpperCase()} = {codeElement.UriTemplate};");
}
Expand Down
Loading
Loading