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 #5461

Merged
merged 10 commits into from
Nov 12, 2024
Merged
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 @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- Fixed Python error when a class inherits from a base class and implements an interface. [5637](https://github.com/microsoft/kiota/issues/5637)
- Fix anyOf/oneOf generation in TypeScript. [5353](https://github.com/microsoft/kiota/issues/5353)

## [1.20.0] - 2024-11-07

Expand Down
4 changes: 0 additions & 4 deletions it/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,6 @@
{
"Language": "php",
"Rationale": "https://github.com/microsoft/kiota/issues/5354"
},
{
"Language": "typescript",
"Rationale": "https://github.com/microsoft/kiota/issues/5353"
}
],
"IdempotencySuppressions": [
Expand Down
1 change: 1 addition & 0 deletions src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,6 @@ public bool IsComposedOfObjectsAndPrimitives(Func<CodeType, CodeComposedTypeBase
// Count the number of primitives in Types
return Types.Any(x => checkIfPrimitive(x, this)) && Types.Any(x => !checkIfPrimitive(x, this));
}
public bool IsComposedOfObjects(Func<CodeType, CodeComposedTypeBase, bool> checkIfPrimitive) => Types.All(x => !checkIfPrimitive(x, this));

}
23 changes: 1 addition & 22 deletions src/Kiota.Builder/Refiners/TypeScriptRefiner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,31 +169,10 @@ public override Task RefineAsync(CodeNamespace generatedCode, CancellationToken
GroupReusableModelsInSingleFile(modelsNamespace);
RemoveSelfReferencingUsings(generatedCode);
AddAliasToCodeFileUsings(generatedCode);
CorrectSerializerParameters(generatedCode);
cancellationToken.ThrowIfCancellationRequested();
}, cancellationToken);
}

private static void CorrectSerializerParameters(CodeElement currentElement)
{
if (currentElement is CodeFunction currentFunction &&
currentFunction.OriginalLocalMethod.Kind is CodeMethodKind.Serializer)
{
foreach (var parameter in currentFunction.OriginalLocalMethod.Parameters
.Where(p => GetOriginalComposedType(p.Type) is CodeComposedTypeBase composedType &&
composedType.IsComposedOfObjectsAndPrimitives(IsPrimitiveType)))
{
var composedType = GetOriginalComposedType(parameter.Type)!;
var newType = (CodeComposedTypeBase)composedType.Clone();
var nonPrimitiveTypes = composedType.Types.Where(x => !IsPrimitiveType(x, composedType)).ToArray();
newType.SetTypes(nonPrimitiveTypes);
parameter.Type = newType;
}
}

CrawlTree(currentElement, CorrectSerializerParameters);
}

private static void AddAliasToCodeFileUsings(CodeElement currentElement)
{
if (currentElement is CodeFile codeFile)
Expand Down Expand Up @@ -1482,7 +1461,7 @@ private static void AddDeserializerUsingToDiscriminatorFactoryForComposedTypePar

function.AddUsing(new CodeUsing
{
Name = modelDeserializerFunction.Parent.Name,
Name = modelDeserializerFunction.Name,
Declaration = new CodeType
{
Name = modelDeserializerFunction.Name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override void WriteCodeElement(CodeFileDeclaration codeElement, LanguageW
.Where(static x => x is { IsExternal: false, Declaration.TypeDefinition: not null })
.GroupBy(static x =>
$"{x.Declaration!.TypeDefinition!.GetImmediateParentOfType<CodeNamespace>().Name}.{x.Declaration?.Name.ToLowerInvariant()}")
.Select(static x => x.OrderBy(static x => x.Parent?.Name).First()));
.Select(static x => x.OrderByDescending(static x => x.Alias, StringComparer.OrdinalIgnoreCase).ThenBy(static x => x.Parent?.Name, StringComparer.OrdinalIgnoreCase).First()));

_codeUsingWriter.WriteCodeElement(filteredUsing, ns, writer);
}
Expand Down
86 changes: 49 additions & 37 deletions src/Kiota.Builder/Writers/TypeScript/CodeFunctionWriter.cs

Large diffs are not rendered by default.

13 changes: 7 additions & 6 deletions src/Kiota.Builder/Writers/TypeScript/CodeMethodWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ public override void WriteCodeElement(CodeMethod codeElement, LanguageWriter wri
ArgumentNullException.ThrowIfNull(writer);
if (codeElement.Parent is CodeFunction) return;

var returnType = GetTypescriptTypeString(codeElement.ReturnType, codeElement, inlineComposedTypeString: true);
var codeFile = codeElement.GetImmediateParentOfType<CodeFile>();
var returnType = GetTypescriptTypeString(codeElement.ReturnType, codeFile, inlineComposedTypeString: true);
var isVoid = "void".EqualsIgnoreCase(returnType);
WriteMethodDocumentation(codeElement, writer, isVoid);
WriteMethodDocumentation(codeFile, codeElement, writer, isVoid);
WriteMethodPrototype(codeElement, writer, returnType, isVoid);
if (codeElement.Parent is CodeClass)
throw new InvalidOperationException("No method implementations are generated in typescript: either functions or constants.");
}

private void WriteMethodDocumentation(CodeMethod code, LanguageWriter writer, bool isVoid)
private void WriteMethodDocumentation(CodeFile codeFile, CodeMethod code, LanguageWriter writer, bool isVoid)
{
WriteMethodDocumentationInternal(code, writer, isVoid, conventions);
WriteMethodDocumentationInternal(codeFile, code, writer, isVoid, conventions);
}
internal static void WriteMethodDocumentationInternal(CodeMethod code, LanguageWriter writer, bool isVoid, TypeScriptConventionService typeScriptConventionService)
internal static void WriteMethodDocumentationInternal(CodeFile codeFile, CodeMethod code, LanguageWriter writer, bool isVoid, TypeScriptConventionService typeScriptConventionService)
{
var returnRemark = (isVoid, code.IsAsync) switch
{
Expand All @@ -41,7 +42,7 @@ internal static void WriteMethodDocumentationInternal(CodeMethod code, LanguageW
code.Parameters
.Where(static x => x.Documentation.DescriptionAvailable)
.OrderBy(static x => x.Name)
.Select(x => $"@param {x.Name} {x.Documentation.GetDescription(type => GetTypescriptTypeString(type, code, inlineComposedTypeString: true), ReferenceTypePrefix, ReferenceTypeSuffix, RemoveInvalidDescriptionCharacters)}")
.Select(x => $"@param {x.Name} {x.Documentation.GetDescription(type => GetTypescriptTypeString(type, codeFile, inlineComposedTypeString: true), ReferenceTypePrefix, ReferenceTypeSuffix, RemoveInvalidDescriptionCharacters)}")
.Union([returnRemark])
.Union(GetThrownExceptionsRemarks(code)));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,32 @@ public override string GetAccessModifier(AccessModifier access)
};
}

private static bool ShouldIncludeCollectionInformationForParameter(CodeParameter parameter)
{
return !(GetOriginalComposedType(parameter) is not null
&& parameter.Parent is CodeMethod codeMethod
&& (codeMethod.IsOfKind(CodeMethodKind.Serializer) || codeMethod.IsOfKind(CodeMethodKind.Deserializer)));
}

public override string GetParameterSignature(CodeParameter parameter, CodeElement targetElement, LanguageWriter? writer = null)
{
ArgumentNullException.ThrowIfNull(parameter);
var includeCollectionInformation = ShouldIncludeCollectionInformationForParameter(parameter);
var paramType = GetTypescriptTypeString(parameter.Type, targetElement, includeCollectionInformation: includeCollectionInformation, inlineComposedTypeString: true);
var isComposedOfPrimitives = GetOriginalComposedType(parameter.Type) is CodeComposedTypeBase composedType && composedType.IsComposedOfPrimitives(IsPrimitiveType);
var composedType = GetOriginalComposedType(parameter.Type);
var paramType = GetTypescriptTypeString(parameter.Type, targetElement, includeCollectionInformation: true, inlineComposedTypeString: true);

if (composedType != null && parameter.Parent is CodeMethod cm && cm.IsOfKind(CodeMethodKind.Serializer))
{
// eliminate primitive types from serializers with composed type signature
var newType = (CodeComposedTypeBase)composedType.Clone();
var nonPrimitiveTypes = composedType.Types.Where(x => !IsPrimitiveTypeOrPrimitiveCollection(x, composedType)).ToArray();
newType.SetTypes(nonPrimitiveTypes);
paramType = GetTypescriptTypeString(newType, targetElement, includeCollectionInformation: false, inlineComposedTypeString: true);
}
var isComposedOfPrimitives = composedType != null && composedType.IsComposedOfPrimitives(IsPrimitiveType);

// add a 'Parsable' type to the parameter if it is composed of non-Parsable types
var parsableTypes = (
composedType != null && !composedType.IsComposedOfObjects(IsPrimitiveType),
parameter.Parent is CodeMethod method && (method.IsOfKind(CodeMethodKind.Deserializer, CodeMethodKind.Serializer))
) switch
{
(true, true) => "Parsable | ",
_ => string.Empty,
};

var defaultValueSuffix = (string.IsNullOrEmpty(parameter.DefaultValue), parameter.Kind, isComposedOfPrimitives) switch
{
(false, CodeParameterKind.DeserializationTarget, false) when parameter.Parent is CodeMethod codeMethod && codeMethod.Kind is CodeMethodKind.Serializer
Expand All @@ -107,7 +120,7 @@ public override string GetParameterSignature(CodeParameter parameter, CodeElemen
(false, CodeParameterKind.DeserializationTarget) => ("Partial<", ">"),
_ => (string.Empty, string.Empty),
};
return $"{parameter.Name.ToFirstCharacterLowerCase()}{(parameter.Optional && parameter.Type.IsNullable ? "?" : string.Empty)}: {partialPrefix}{paramType}{partialSuffix}{(parameter.Type.IsNullable ? " | undefined" : string.Empty)}{defaultValueSuffix}";
return $"{parameter.Name.ToFirstCharacterLowerCase()}{(parameter.Optional && parameter.Type.IsNullable ? "?" : string.Empty)}: {partialPrefix}{parsableTypes}{paramType}{partialSuffix}{(parameter.Type.IsNullable ? " | undefined" : string.Empty)}{defaultValueSuffix}";
}

public override string GetTypeString(CodeTypeBase code, CodeElement targetElement, bool includeCollectionInformation = true, LanguageWriter? writer = null)
Expand Down Expand Up @@ -237,6 +250,8 @@ TYPE_LOWERCASE_BOOLEAN or

public static bool IsPrimitiveType(CodeType codeType, CodeComposedTypeBase codeComposedTypeBase, bool includeCollectionInformation) => IsPrimitiveType(GetTypescriptTypeString(codeType, codeComposedTypeBase, includeCollectionInformation));

private static bool IsPrimitiveTypeOrPrimitiveCollection(CodeType codeType, CodeComposedTypeBase codeComposedTypeBase) => IsPrimitiveType(codeType, codeComposedTypeBase, false);

internal static string RemoveInvalidDescriptionCharacters(string originalDescription) => originalDescription?.Replace("\\", "/", StringComparison.OrdinalIgnoreCase) ?? string.Empty;
public override bool WriteShortDescription(IDocumentedElement element, LanguageWriter writer, string prefix = "", string suffix = "")
{
Expand Down Expand Up @@ -308,12 +323,12 @@ public static string GetFactoryMethodName(CodeTypeBase targetClassType, CodeElem
return definitionClass.GetImmediateParentOfType<CodeFile>(definitionClass)?.FindChildByName<CodeFunction>(factoryMethodName);
}

public string GetDeserializationMethodName(CodeTypeBase codeType, CodeMethod method, bool? IsCollection = null)
public string GetDeserializationMethodName(CodeTypeBase codeType, CodeElement targetElement, bool? IsCollection = null)
{
ArgumentNullException.ThrowIfNull(codeType);
ArgumentNullException.ThrowIfNull(method);
ArgumentNullException.ThrowIfNull(targetElement);
var isCollection = IsCollection == true || codeType.IsCollection;
var propertyType = GetTypescriptTypeString(codeType, method, false);
var propertyType = GetTypescriptTypeString(codeType, targetElement, false);

CodeTypeBase _codeType = GetOriginalComposedType(codeType) is CodeComposedTypeBase composedType ? new CodeType() { Name = composedType.Name, TypeDefinition = composedType } : codeType;

Expand All @@ -324,19 +339,19 @@ public string GetDeserializationMethodName(CodeTypeBase codeType, CodeMethod met
(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)
(_, true, _) => $"getCollectionOfObjectValues<{propertyType.ToFirstCharacterUpperCase()}>({GetFactoryMethodName(_codeType, targetElement)})",
_ => GetDeserializationMethodNameForPrimitiveOrObject(_codeType, propertyType, targetElement)
};
}
return GetDeserializationMethodNameForPrimitiveOrObject(_codeType, propertyType, method);
return GetDeserializationMethodNameForPrimitiveOrObject(_codeType, propertyType, targetElement);
}

private static string GetDeserializationMethodNameForPrimitiveOrObject(CodeTypeBase propType, string propertyTypeName, CodeMethod method)
private static string GetDeserializationMethodNameForPrimitiveOrObject(CodeTypeBase propType, string propertyTypeName, CodeElement targetElement)
{
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)})"
_ => $"getObjectValue<{propertyTypeName.ToFirstCharacterUpperCase()}>({GetFactoryMethodName(propType, targetElement)})"
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1252,10 +1252,8 @@ public async Task Writes_UnionOfPrimitiveValues_SerializerFunctionAsync()
writer.Write(serializerFunction);
var serializerFunctionStr = tw.ToString();
Assert.Contains("return", serializerFunctionStr);
Assert.Contains("switch", serializerFunctionStr);
Assert.Contains("case \"number\":", serializerFunctionStr);
Assert.Contains("case \"string\":", serializerFunctionStr);
Assert.Contains("break", serializerFunctionStr);
Assert.Contains("typeof primitives === \"number\"", serializerFunctionStr);
Assert.Contains("typeof primitives === \"string\"", serializerFunctionStr);
AssertExtensions.CurlyBracesAreClosed(serializerFunctionStr, 1);
}

Expand Down Expand Up @@ -1454,9 +1452,9 @@ public async Task Writes_CodeUnionBetweenObjectsAndPrimitiveTypes_SerializerAsyn
writer.Write(serializeFunction);
var result = tw.ToString();

Assert.Contains("case typeof parentClass.property === \"string\"", result);
Assert.Contains("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("typeof parentClass.property === \"number\"", result);
Assert.Contains("writer.writeNumberValue(\"property\", parentClass.property as number);", result);
Assert.Contains(
"writer.writeCollectionOfObjectValues<ArrayOfObjects>(\"property\", parentClass.property as ArrayOfObjects[] | undefined | null",
Expand Down
Loading