Skip to content

Commit

Permalink
Step 5: Add form serialization libs
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Omondi committed Jul 5, 2024
1 parent 0e00c25 commit 782ac41
Show file tree
Hide file tree
Showing 18 changed files with 1,199 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Microsoft.Kiota.sln
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Kiota.Serializati
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Kiota.Serialization.Json", "src\serialization\json\Microsoft.Kiota.Serialization.Json.csproj", "{87168872-0B10-4BBB-9017-2501E76FE50A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Kiota.Serialization.Form", "src\serialization\form\Microsoft.Kiota.Serialization.Form.csproj", "{08C40934-39AB-4BD6-8306-7EF84122BF70}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Kiota.Serialization.Form.Tests", "tests\serialization\form\Microsoft.Kiota.Serialization.Form.Tests.csproj", "{E0C5DCE4-2CF5-4EA3-B351-3DD97081A9F4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -74,6 +78,14 @@ Global
{87168872-0B10-4BBB-9017-2501E76FE50A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87168872-0B10-4BBB-9017-2501E76FE50A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87168872-0B10-4BBB-9017-2501E76FE50A}.Release|Any CPU.Build.0 = Release|Any CPU
{08C40934-39AB-4BD6-8306-7EF84122BF70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08C40934-39AB-4BD6-8306-7EF84122BF70}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08C40934-39AB-4BD6-8306-7EF84122BF70}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08C40934-39AB-4BD6-8306-7EF84122BF70}.Release|Any CPU.Build.0 = Release|Any CPU
{E0C5DCE4-2CF5-4EA3-B351-3DD97081A9F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E0C5DCE4-2CF5-4EA3-B351-3DD97081A9F4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0C5DCE4-2CF5-4EA3-B351-3DD97081A9F4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0C5DCE4-2CF5-4EA3-B351-3DD97081A9F4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Read more about Kiota [here](https://github.com/microsoft/kiota/blob/main/README
1. [Authentication - Azure](./src/authentication/azure/README.md)
1. [Http - HttpClient](./src/http/httpClient/README.md)
1. [Serialization - JSON](./src/serialization/json/README.md)
1. [Serialization - FORM](./src/serialization/form/README.md)

## Debugging

Expand Down
Binary file added src/serialization/form/35MSSharedLib1024.snk
Binary file not shown.
260 changes: 260 additions & 0 deletions src/serialization/form/FormParseNode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
using System.Diagnostics;
using System.Reflection;
using System.Xml;
using System;
using System.Collections.Generic;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
#if NET5_0_OR_GREATER
using System.Diagnostics.CodeAnalysis;
#endif

namespace Microsoft.Kiota.Serialization.Form;
/// <summary>Represents a parse node that can be used to parse a form url encoded string.</summary>
public class FormParseNode : IParseNode
{
private readonly string RawValue;
private string DecodedValue => Uri.UnescapeDataString(RawValue);
private readonly Dictionary<string, string> Fields;
/// <summary>Initializes a new instance of the <see cref="FormParseNode"/> class.</summary>
/// <param name="rawValue">The raw value to parse.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="rawValue"/> is null.</exception>
public FormParseNode(string rawValue)
{
RawValue = rawValue ?? throw new ArgumentNullException(nameof(rawValue));
Fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
char[] pairDelimiter = new char[] { '=' };
string[] pairs = rawValue.Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string pair in pairs)
{
string[] keyValue = pair.Split(pairDelimiter, StringSplitOptions.RemoveEmptyEntries);
if (keyValue.Length == 2)
{
string key = SanitizeKey(keyValue[0]);
string value = keyValue[1].Trim();

if (Fields.ContainsKey(key))
{
Fields[key] += $",{value}";
}
else
{
Fields.Add(key, value);
}
}
}
}

private static string SanitizeKey(string key) {
if (string.IsNullOrEmpty(key)) return key;
return Uri.UnescapeDataString(key.Trim());
}
/// <inheritdoc/>
public Action<IParsable>? OnBeforeAssignFieldValues { get; set; }
/// <inheritdoc/>
public Action<IParsable>? OnAfterAssignFieldValues { get; set; }
/// <inheritdoc/>
public bool? GetBoolValue() => bool.TryParse(DecodedValue, out var result) && result;
/// <inheritdoc/>
public byte[]? GetByteArrayValue() {
var rawValue = DecodedValue;
if(string.IsNullOrEmpty(rawValue)) return null;
return Convert.FromBase64String(rawValue);
}
/// <inheritdoc/>
public byte? GetByteValue() => byte.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public IParseNode? GetChildNode(string identifier) => Fields.TryGetValue(SanitizeKey(identifier), out var value) ?
new FormParseNode(value){
OnBeforeAssignFieldValues = OnBeforeAssignFieldValues,
OnAfterAssignFieldValues = OnAfterAssignFieldValues
} : null;
/// <inheritdoc/>
public IEnumerable<T> GetCollectionOfObjectValues<T>(ParsableFactory<T> factory) where T : IParsable => throw new InvalidOperationException("collections are not supported with uri form encoding");

private static readonly Type booleanType = typeof(bool?);
private static readonly Type byteType = typeof(byte?);
private static readonly Type sbyteType = typeof(sbyte?);
private static readonly Type stringType = typeof(string);
private static readonly Type intType = typeof(int?);
private static readonly Type decimalType = typeof(decimal?);
private static readonly Type floatType = typeof(float?);
private static readonly Type doubleType = typeof(double?);
private static readonly Type guidType = typeof(Guid?);
private static readonly Type dateTimeOffsetType = typeof(DateTimeOffset?);
private static readonly Type timeSpanType = typeof(TimeSpan?);
private static readonly Type dateType = typeof(Date?);
private static readonly Type timeType = typeof(Time?);

/// <summary>
/// Get the collection of primitives of type <typeparam name="T"/>from the form node
/// </summary>
/// <returns>A collection of objects</returns>
public IEnumerable<T> GetCollectionOfPrimitiveValues<T>()
{
var genericType = typeof(T);
var primitiveValueCollection = DecodedValue.Split(new[] { ',' } , StringSplitOptions.RemoveEmptyEntries);
foreach(var collectionValue in primitiveValueCollection)
{
var currentParseNode = new FormParseNode(collectionValue)
{
OnBeforeAssignFieldValues = OnBeforeAssignFieldValues,
OnAfterAssignFieldValues = OnAfterAssignFieldValues
};
if(genericType == booleanType)
yield return (T)(object)currentParseNode.GetBoolValue()!;
else if(genericType == byteType)
yield return (T)(object)currentParseNode.GetByteValue()!;
else if(genericType == sbyteType)
yield return (T)(object)currentParseNode.GetSbyteValue()!;
else if(genericType == stringType)
yield return (T)(object)currentParseNode.GetStringValue()!;
else if(genericType == intType)
yield return (T)(object)currentParseNode.GetIntValue()!;
else if(genericType == floatType)
yield return (T)(object)currentParseNode.GetFloatValue()!;
else if(genericType == doubleType)
yield return (T)(object)currentParseNode.GetDoubleValue()!;
else if(genericType == decimalType)
yield return (T)(object)currentParseNode.GetDecimalValue()!;
else if(genericType == guidType)
yield return (T)(object)currentParseNode.GetGuidValue()!;
else if(genericType == dateTimeOffsetType)
yield return (T)(object)currentParseNode.GetDateTimeOffsetValue()!;
else if(genericType == timeSpanType)
yield return (T)(object)currentParseNode.GetTimeSpanValue()!;
else if(genericType == dateType)
yield return (T)(object)currentParseNode.GetDateValue()!;
else if(genericType == timeType)
yield return (T)(object)currentParseNode.GetTimeValue()!;
else
throw new InvalidOperationException($"unknown type for deserialization {genericType.FullName}");
}
}
/// <inheritdoc/>
public DateTimeOffset? GetDateTimeOffsetValue() => DateTimeOffset.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public Date? GetDateValue() => DateTime.TryParse(DecodedValue, out var result) ? new Date(result) : null;
/// <inheritdoc/>
public decimal? GetDecimalValue() => decimal.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public double? GetDoubleValue() => double.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public float? GetFloatValue() => float.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public Guid? GetGuidValue() => Guid.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public int? GetIntValue() => int.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public long? GetLongValue() => long.TryParse(DecodedValue, out var result) ? result : null;
/// <inheritdoc/>
public T GetObjectValue<T>(ParsableFactory<T> factory) where T : IParsable {
var item = factory(this);
OnBeforeAssignFieldValues?.Invoke(item);
AssignFieldValues(item);
OnAfterAssignFieldValues?.Invoke(item);
return item;
}
private void AssignFieldValues<T>(T item) where T : IParsable
{
if(Fields.Count == 0) return;
IDictionary<string, object>? itemAdditionalData = null;
if(item is IAdditionalDataHolder holder)
{
holder.AdditionalData ??= new Dictionary<string, object>();
itemAdditionalData = holder.AdditionalData;
}
var fieldDeserializers = item.GetFieldDeserializers();

foreach(var fieldValue in Fields)
{
if(fieldDeserializers.TryGetValue(fieldValue.Key, out var fieldDeserializer))
{
if("null".Equals(fieldValue.Value, StringComparison.OrdinalIgnoreCase))
continue;// If the property is already null just continue. As calling functions like GetDouble,GetBoolValue do not process null.

Debug.WriteLine($"found property {fieldValue.Key} to deserialize");
fieldDeserializer.Invoke(new FormParseNode(fieldValue.Value)
{
OnBeforeAssignFieldValues = OnBeforeAssignFieldValues,
OnAfterAssignFieldValues = OnAfterAssignFieldValues
});
}
else if (itemAdditionalData != null)
{
Debug.WriteLine($"found additional property {fieldValue.Key} to deserialize");
IDictionaryExtensions.TryAdd(itemAdditionalData, fieldValue.Key, fieldValue.Value);
}
else
{
Debug.WriteLine($"found additional property {fieldValue.Key} to deserialize but the model doesn't support additional data");
}
}
}

/// <inheritdoc/>
public sbyte? GetSbyteValue() => sbyte.TryParse(DecodedValue, out var result) ? result : null;

/// <inheritdoc/>
public string GetStringValue() => DecodedValue;

/// <inheritdoc/>
public TimeSpan? GetTimeSpanValue() {
var rawString = DecodedValue;
if(string.IsNullOrEmpty(rawString))
return null;

// Parse an ISO8601 duration.http://en.wikipedia.org/wiki/ISO_8601#Durations to a TimeSpan
return XmlConvert.ToTimeSpan(rawString);
}

/// <inheritdoc/>
public Time? GetTimeValue() => DateTime.TryParse(DecodedValue, out var result) ? new Time(result) : null;

#if NET5_0_OR_GREATER
IEnumerable<T?> IParseNode.GetCollectionOfEnumValues<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>()
#else
IEnumerable<T?> IParseNode.GetCollectionOfEnumValues<T>()
#endif
{
foreach (var v in DecodedValue.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
yield return GetEnumValueInternal<T>(v);
}

#if NET5_0_OR_GREATER
T? IParseNode.GetEnumValue<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicFields)] T>()
#else
T? IParseNode.GetEnumValue<T>()
#endif
{
return GetEnumValueInternal<T>(DecodedValue);
}

private static T? GetEnumValueInternal<T>(string rawValue) where T : struct, Enum
{
if(string.IsNullOrEmpty(rawValue))
return null;
if (typeof(T).IsDefined(typeof(FlagsAttribute)))
{
ReadOnlySpan<char> valueSpan = rawValue.AsSpan();
int value = 0;
while(valueSpan.Length > 0)
{
int commaIndex = valueSpan.IndexOf(',');
ReadOnlySpan<char> valueNameSpan = commaIndex < 0 ? valueSpan : valueSpan.Slice(0, commaIndex);
#if NET6_0_OR_GREATER
if(Enum.TryParse<T>(valueNameSpan, true, out var result))
#else
if(Enum.TryParse<T>(valueNameSpan.ToString(), true, out var result))
#endif
value |= (int)(object)result;
valueSpan = commaIndex < 0 ? ReadOnlySpan<char>.Empty : valueSpan.Slice(commaIndex + 1);
}
return (T)(object)value;
}
else if(Enum.TryParse<T>(rawValue, out var result))
return result;
return null;
}
}
44 changes: 44 additions & 0 deletions src/serialization/form/FormParseNodeFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Microsoft.Kiota.Abstractions.Serialization;
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Kiota.Serialization.Form;

/// <summary>
/// The <see cref="IAsyncParseNodeFactory"/> implementation for form content types
/// </summary>
public class FormParseNodeFactory : IAsyncParseNodeFactory
{
/// <inheritdoc/>
public string ValidContentType => "application/x-www-form-urlencoded";
/// <inheritdoc/>
public IParseNode GetRootParseNode(string contentType, Stream content) {
if(string.IsNullOrEmpty(contentType))
throw new ArgumentNullException(nameof(contentType));
if(!ValidContentType.Equals(contentType, StringComparison.OrdinalIgnoreCase))
throw new ArgumentOutOfRangeException($"expected a {ValidContentType} content type");
if( content == null)
throw new ArgumentNullException(nameof(content));

using var reader = new StreamReader(content);
var rawValue = reader.ReadToEnd();
return new FormParseNode(rawValue);
}
/// <inheritdoc/>
public async Task<IParseNode> GetRootParseNodeAsync(string contentType, Stream content,
CancellationToken cancellationToken = default)
{
if(string.IsNullOrEmpty(contentType))
throw new ArgumentNullException(nameof(contentType));
if(!ValidContentType.Equals(contentType, StringComparison.OrdinalIgnoreCase))
throw new ArgumentOutOfRangeException($"expected a {ValidContentType} content type");
if(content == null)
throw new ArgumentNullException(nameof(content));

using var reader = new StreamReader(content);
var rawValue = await reader.ReadToEndAsync().ConfigureAwait(false);
return new FormParseNode(rawValue);
}
}
Loading

0 comments on commit 782ac41

Please sign in to comment.