Skip to content

Commit

Permalink
Merge pull request #1603 from microsoft/feature/union-types-serializa…
Browse files Browse the repository at this point in the history
…tion

adds support for deserialization of composed types
  • Loading branch information
baywet authored Sep 8, 2022
2 parents 0ccd62d + 8310c8b commit 860f56e
Show file tree
Hide file tree
Showing 71 changed files with 3,400 additions and 704 deletions.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added none output formatter to CLI commons. (Shell)
- Added 'Accept' field of http request header in Ruby. [#1660](https://github.com/microsoft/kiota/issues/1660)
- Added support for text serialization in Python. [#1406](https://github.com/microsoft/kiota/issues/1406)
- Added support for composed types (union, intersection) in CSharp, Java and Go. [#1411](https://github.com/microsoft/kiota/issues/1411)
- Added support for implicit discriminator mapping.
- Added support for default values of enum properties in CSharp, Java and Go.


### Changed

Expand All @@ -29,6 +33,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove all overloads for GO request executors
- Adds a context object in all GO requests
- Remove all overloads for GO request executors and Adds a context object in all GO requests [GO#176](https://github.com/microsoftgraph/msgraph-sdk-go/issues/176)
- Fixed a bug where the Hashing method for type names differentiation could lock the process.
- Fixed a bug where CSharp declaration writer would add usings for inner classes.
- Fixed a bug with inline schema class naming.
- Fixed a bug where symbols starting with a number would be invalid.
- Fixed a bug where classes could end up with duplicated methods.
- Fixed a bug where Go writer would try to import multiple times the same symbol.
- Fixed a bug where the core generator engine would fail to recognize meaningful schemas.
- Fixed a bug where Go and Java inner class imports would be missing.
- Fixed a bug where Go and Java collection bodies would not generate properly.
- Aligns request options types in Java with other collections type.
- Fixed a bug where Java would skip duplicated imports instead of deduplicating them.
- Fixed a bug where Java would not convert date types for query parameters.
- Fixed a bug where Java doc comments could contain invalid characters.


## [0.4.0] - 2022-08-18
Expand Down
18 changes: 13 additions & 5 deletions src/Kiota.Builder/CodeDOM/CodeBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,19 @@ returnedValue is CodeProperty cProp &&
// indexer retrofitted to method in the parent request builder on the path and conflicting with the collection request builder property
returnedValue = InnerChildElements.GetOrAdd($"{element.Name}-indexerbackcompat", element);
added = true;
} else if(currentMethod.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator, CodeMethodKind.Constructor, CodeMethodKind.RawUrlConstructor)) {
// allows for methods overload
var methodOverloadNameSuffix = currentMethod.Parameters.Any() ? currentMethod.Parameters.Select(x => x.Name).OrderBy(x => x).Aggregate((x, y) => x + y) : "1";
returnedValue = InnerChildElements.GetOrAdd($"{element.Name}-{methodOverloadNameSuffix}", element);
added = true;
} else if(currentMethod.IsOfKind(CodeMethodKind.RequestExecutor, CodeMethodKind.RequestGenerator, CodeMethodKind.Constructor, CodeMethodKind.RawUrlConstructor) &&
returnedValue is CodeMethod existingMethod) {
var currentMethodParameterNames = currentMethod.Parameters.Select(static x => x.Name).ToHashSet();
var returnedMethodParameterNames = existingMethod.Parameters.Select(static x => x.Name).ToHashSet();
if(currentMethodParameterNames.Count != returnedMethodParameterNames.Count ||
currentMethodParameterNames.Union(returnedMethodParameterNames)
.Except(currentMethodParameterNames.Intersect(returnedMethodParameterNames))
.Any()) {
// allows for methods overload
var methodOverloadNameSuffix = currentMethodParameterNames.Any() ? currentMethodParameterNames.OrderBy(static x => x).Aggregate(static (x, y) => x + y) : "1";
returnedValue = InnerChildElements.GetOrAdd($"{element.Name}-{methodOverloadNameSuffix}", element);
added = true;
}
}

if(!added && returnedValue.GetType() != element.GetType())
Expand Down
26 changes: 22 additions & 4 deletions src/Kiota.Builder/CodeDOM/CodeClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,17 @@ public enum CodeClassKind {
/// <summary>
/// CodeClass represents an instance of a Class to be generated in source code
/// </summary>
public class CodeClass : ProprietableBlock<CodeClassKind, ClassDeclaration>, ITypeDefinition
public class CodeClass : ProprietableBlock<CodeClassKind, ClassDeclaration>, ITypeDefinition, IDiscriminatorInformationHolder
{
public bool IsErrorDefinition { get; set; }

/// <summary>
/// Original composed type this class was generated for.
/// </summary>
public CodeComposedTypeBase OriginalComposedType { get; set; }
public void SetIndexer(CodeIndexer indexer)
{
if(indexer == null)
throw new ArgumentNullException(nameof(indexer));
ArgumentNullException.ThrowIfNull(indexer);
if(InnerChildElements.Values.OfType<CodeIndexer>().Any() || InnerChildElements.Values.OfType<CodeMethod>().Any(static x => x.IsOfKind(CodeMethodKind.IndexerBackwardCompatibility))) {
var existingIndexer = InnerChildElements.Values.OfType<CodeIndexer>().FirstOrDefault();
if(existingIndexer != null) {
Expand Down Expand Up @@ -80,6 +84,20 @@ public CodeClass GetGreatestGrandparent(CodeClass startClassToSkip = null) {
else
return parentClass.GetGreatestGrandparent(startClassToSkip);
}
private DiscriminatorInformation _discriminatorInformation;
/// <inheritdoc />
public DiscriminatorInformation DiscriminatorInformation {
get {
if (_discriminatorInformation == null)
DiscriminatorInformation = new DiscriminatorInformation();
return _discriminatorInformation;
}
set {
ArgumentNullException.ThrowIfNull(value);
EnsureElementsAreChildren(value);
_discriminatorInformation = value;
}
}
}
public class ClassDeclaration : ProprietableBlockDeclaration
{
Expand All @@ -103,7 +121,7 @@ public CodeProperty GetOriginalPropertyDefinedFromBaseType(string propertyName)
}

public bool InheritsFrom(CodeClass candidate) {
ArgumentNullException.ThrowIfNull(candidate, nameof(candidate));
ArgumentNullException.ThrowIfNull(candidate);

if (inherits is CodeType currentInheritsType &&
currentInheritsType.TypeDefinition is CodeClass currentParentClass)
Expand Down
21 changes: 20 additions & 1 deletion src/Kiota.Builder/CodeDOM/CodeComposedTypeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,39 @@ namespace Kiota.Builder;
/// <summary>
/// The base class for composed types like union or exclusion.
/// </summary>
public abstract class CodeComposedTypeBase : CodeTypeBase {
public abstract class CodeComposedTypeBase : CodeTypeBase, IDiscriminatorInformationHolder {
public void AddType(params CodeType[] codeTypes) {
EnsureElementsAreChildren(codeTypes);
foreach(var codeType in codeTypes.Where(x => x != null && !Types.Contains(x)))
types.Add(codeType);
}
private readonly List<CodeType> types = new ();
public IEnumerable<CodeType> Types { get => types; }
private DiscriminatorInformation _discriminatorInformation;
/// <inheritdoc />
public DiscriminatorInformation DiscriminatorInformation {
get {
if (_discriminatorInformation == null)
DiscriminatorInformation = new DiscriminatorInformation();
return _discriminatorInformation;
}
set {
ArgumentNullException.ThrowIfNull(value);
EnsureElementsAreChildren(value);
_discriminatorInformation = value;
}
}
protected override ChildType BaseClone<ChildType>(CodeTypeBase source) {
if (source is not CodeComposedTypeBase sourceComposed)
throw new InvalidCastException($"Cannot cast {source.GetType().Name} to {nameof(CodeComposedTypeBase)}");
base.BaseClone<ChildType>(source);
if(sourceComposed.Types?.Any() ?? false)
AddType(sourceComposed.Types.ToArray());
DiscriminatorInformation = sourceComposed.DiscriminatorInformation?.Clone() as DiscriminatorInformation;
return this as ChildType;
}
/// <summary>
/// The target namespace if the composed type needs to be represented by a class
/// </summary>
public CodeNamespace TargetNamespace { get; set; }
}
2 changes: 1 addition & 1 deletion src/Kiota.Builder/CodeDOM/CodeEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ public void AddOption(params CodeEnumOption[] codeEnumOptions)
OptionsInternal.Enqueue(option);
}
}
public IEnumerable<CodeEnumOption> Options => OptionsInternal.ToArray();
public IEnumerable<CodeEnumOption> Options => OptionsInternal;
}
11 changes: 10 additions & 1 deletion src/Kiota.Builder/CodeDOM/CodeFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,19 @@ public CodeMethod OriginalLocalMethod
{
get; private set;
}
public CodeClass OriginalMethodParentClass
{
get;
private set;
}
public CodeFunction(CodeMethod method)
{
if (method == null) throw new ArgumentNullException(nameof(method));
ArgumentNullException.ThrowIfNull(method);
if (!method.IsStatic) throw new InvalidOperationException("The original method must be static");
if (method.Parent is CodeClass parentClass)
OriginalMethodParentClass = parentClass;
else
throw new InvalidOperationException("The original method must be a member of a class");
EnsureElementsAreChildren(method);
OriginalLocalMethod = method;
}
Expand Down
1 change: 1 addition & 0 deletions src/Kiota.Builder/CodeDOM/CodeInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum CodeInterfaceKind {

public class CodeInterface : ProprietableBlock<CodeInterfaceKind, InterfaceDeclaration>, ITypeDefinition
{
public CodeClass OriginalClass { get; set; }
}
public class InterfaceDeclaration : ProprietableBlockDeclaration
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace Kiota.Builder;
/// <summary>
/// The base class for exclusion types. (one of the properties at a time)
/// </summary>
public class CodeExclusionType : CodeComposedTypeBase, ICloneable {
public class CodeIntersectionType : CodeComposedTypeBase, ICloneable {
public override object Clone() {
var value = new CodeExclusionType{
}.BaseClone<CodeExclusionType>(this);
var value = new CodeIntersectionType{
}.BaseClone<CodeIntersectionType>(this);
return value;
}
}
49 changes: 3 additions & 46 deletions src/Kiota.Builder/CodeDOM/CodeMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,8 @@ public class CodeMethod : CodeTerminalWithKind<CodeMethodKind>, ICloneable, IDoc
{
public static CodeMethod FromIndexer(CodeIndexer originalIndexer, CodeClass indexerClass, string methodNameSuffix, bool parameterNullable)
{
if(originalIndexer == null)
throw new ArgumentNullException(nameof(originalIndexer));
if(indexerClass == null)
throw new ArgumentNullException(nameof(indexerClass));
ArgumentNullException.ThrowIfNull(originalIndexer);
ArgumentNullException.ThrowIfNull(indexerClass);
var method = new CodeMethod {
IsAsync = false,
IsStatic = false,
Expand Down Expand Up @@ -224,26 +222,6 @@ public void ReplaceErrorMapping(CodeTypeBase oldType, CodeTypeBase newType)
errorMappings[code] = newType;
}
}
private ConcurrentDictionary<string, CodeTypeBase> discriminatorMappings = new();
/// <summary>
/// Gets/Sets the discriminator values for the class where the key is the value as represented in the payload.
/// </summary>
public IOrderedEnumerable<KeyValuePair<string, CodeTypeBase>> DiscriminatorMappings
{
get
{
return discriminatorMappings.OrderBy(static x => x.Key);
}
}
/// <summary>
/// Gets/Sets the name of the property to use for discrimination during deserialization.
/// </summary>
public string DiscriminatorPropertyName { get; set; }

public bool ShouldWriteDiscriminatorSwitch { get {
return !string.IsNullOrEmpty(DiscriminatorPropertyName) && DiscriminatorMappings.Any();
} }

public object Clone()
{
var method = new CodeMethod {
Expand All @@ -264,8 +242,6 @@ public object Clone()
Parent = Parent,
OriginalIndexer = OriginalIndexer,
errorMappings = errorMappings == null ? null : new (errorMappings),
discriminatorMappings = discriminatorMappings == null ? null : new (discriminatorMappings),
DiscriminatorPropertyName = DiscriminatorPropertyName?.Clone() as string,
acceptedResponseTypes = acceptedResponseTypes == null ? null : new (acceptedResponseTypes),
PagingInformation = PagingInformation?.Clone() as PagingInformation,
};
Expand All @@ -285,29 +261,10 @@ public void AddParameter(params CodeParameter[] methodParameters)
}
public void AddErrorMapping(string errorCode, CodeTypeBase type)
{
if(type == null) throw new ArgumentNullException(nameof(type));
ArgumentNullException.ThrowIfNull(type);
if(string.IsNullOrEmpty(errorCode)) throw new ArgumentNullException(nameof(errorCode));
errorMappings.TryAdd(errorCode, type);
}

public void AddDiscriminatorMapping(string key, CodeTypeBase type)
{
if(type == null) throw new ArgumentNullException(nameof(type));
if(string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));
discriminatorMappings.TryAdd(key, type);
}
public CodeTypeBase GetDiscriminatorMappingValue(string key)
{
if(string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));
if(discriminatorMappings.TryGetValue(key, out var value))
return value;
return null;
}
public void RemoveDiscriminatorMapping(params string[] keys) {
ArgumentNullException.ThrowIfNull(keys, nameof(keys));
foreach(var key in keys)
discriminatorMappings.TryRemove(key, out var _);
}
public CodeTypeBase GetErrorMappingValue(string key)
{
if(string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));
Expand Down
7 changes: 3 additions & 4 deletions src/Kiota.Builder/CodeDOM/CodeNamespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ public override string Name
}
}
public bool IsParentOf(CodeNamespace childNamespace) {
if(childNamespace == null)
throw new ArgumentNullException(nameof(childNamespace));
ArgumentNullException.ThrowIfNull(childNamespace);
if(this == childNamespace)
return false;
return childNamespace.Name.StartsWith(Name + ".", StringComparison.OrdinalIgnoreCase);
Expand Down Expand Up @@ -59,6 +58,7 @@ public CodeNamespace FindNamespaceByName(string nsName) {
}
return result;
}
public CodeNamespace FindOrAddNamespace(string nsName) => FindNamespaceByName(nsName) ?? AddNamespace(nsName);
public CodeNamespace AddNamespace(string namespaceName) {
if(string.IsNullOrEmpty(namespaceName))
throw new ArgumentNullException(nameof(namespaceName));
Expand Down Expand Up @@ -132,8 +132,7 @@ public IEnumerable<CodeInterface> AddInterface(params CodeInterface[] interfaces
}
public NamespaceDifferentialTracker GetDifferential(CodeNamespace importNamespace, string namespacePrefix, char separator = '.')
{
if(importNamespace == null)
throw new ArgumentNullException(nameof(importNamespace));
ArgumentNullException.ThrowIfNull(importNamespace);
if(string.IsNullOrEmpty(namespacePrefix))
throw new ArgumentNullException(nameof(namespacePrefix));
if (this == importNamespace || Name.Equals(importNamespace.Name, StringComparison.OrdinalIgnoreCase)) // we're in the same namespace
Expand Down
4 changes: 2 additions & 2 deletions src/Kiota.Builder/CodeDOM/CodeProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ public enum CodePropertyKind
/// </summary>
Options,
/// <summary>
/// The request response handler. Used when request parameters are wrapped in a class.
/// Serialization hint for composed type wrappers.
/// </summary>
ResponseHandler,
SerializationHint,
}

public class CodeProperty : CodeTerminalWithKind<CodePropertyKind>, IDocumentedElement
Expand Down
78 changes: 78 additions & 0 deletions src/Kiota.Builder/CodeDOM/DiscriminatorInformation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Kiota.Builder;
public class DiscriminatorInformation : CodeElement, ICloneable
{
private ConcurrentDictionary<string, CodeTypeBase> discriminatorMappings = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets the discriminator values for the class where the key is the value as represented in the payload.
/// </summary>
public IOrderedEnumerable<KeyValuePair<string, CodeTypeBase>> DiscriminatorMappings
{
get
{
return (Parent is not CodeUnionType &&
Parent?.GetImmediateParentOfType<CodeClass>() is CodeClass parentClass ?
discriminatorMappings.Where(x => x.Value is not CodeType currentType || currentType.TypeDefinition != parentClass) :
discriminatorMappings)
.OrderBy(static x => x.Key);
}
}
/// <summary>
/// Gets/Sets the name of the property to use for discrimination during deserialization.
/// </summary>
public string DiscriminatorPropertyName
{
get; set;
}

public void AddDiscriminatorMapping(string key, CodeTypeBase type)
{
ArgumentNullException.ThrowIfNull(type);
if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));
discriminatorMappings.TryAdd(key, type);
}

public CodeTypeBase GetDiscriminatorMappingValue(string key)
{
if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key));
if (discriminatorMappings.TryGetValue(key, out var value))
return value;
return null;
}

public void RemoveDiscriminatorMapping(params string[] keys) {
ArgumentNullException.ThrowIfNull(keys);
foreach(var key in keys)
discriminatorMappings.TryRemove(key, out var _);
}

public object Clone()
{
return new DiscriminatorInformation
{
DiscriminatorPropertyName = DiscriminatorPropertyName,
discriminatorMappings = discriminatorMappings == null ? null : new(discriminatorMappings),
Parent = Parent,
Name = Name?.Clone() as string,
};
}
public bool HasBasicDiscriminatorInformation => !string.IsNullOrEmpty(DiscriminatorPropertyName) && discriminatorMappings.Any();
public bool ShouldWriteDiscriminatorForInheritedType => HasBasicDiscriminatorInformation && IsComplexType;
public bool ShouldWriteDiscriminatorForUnionType => IsUnionType; // if union of scalar types, then we don't always get discriminator information
public bool ShouldWriteDiscriminatorForIntersectionType => IsIntersectionType; // if intersection of scalar types, then we don't always get discriminator information
public bool ShouldWriteParseNodeCheck => ShouldWriteDiscriminatorForInheritedType || ShouldWriteDiscriminatorForUnionType || ShouldWriteDiscriminatorForIntersectionType;
public bool ShouldWriteDiscriminatorBody => ShouldWriteParseNodeCheck && HasBasicDiscriminatorInformation;
private bool IsUnionType => Is<CodeUnionType>();
private bool IsIntersectionType => Is<CodeIntersectionType>();
private bool IsComplexType => Parent is CodeClass currentClass && currentClass.OriginalComposedType is null ||
Parent is CodeMethod parentMethod && parentMethod.Parent is CodeFunction currentFunction && currentFunction.OriginalMethodParentClass?.OriginalComposedType is null; //static factories outside of classes (TS/Go)
private bool Is<T>() where T : CodeComposedTypeBase
{
return Parent is CodeClass currentClass && currentClass.OriginalComposedType is T ||
Parent is T;
}
}
Loading

0 comments on commit 860f56e

Please sign in to comment.