Skip to content

Commit

Permalink
feat(Sdk): Added a new IOneOf abstraction and implementation
Browse files Browse the repository at this point in the history
feat(IO): Added JSON and YAML serializers for the new OneOf type
fix(Sdk): Transformed endpoints into OneOf

Signed-off-by: Charles d'Avernas <charles.davernas@neuroglia.io>
  • Loading branch information
cdavernas committed Aug 21, 2024
1 parent 141c77b commit 73a219f
Show file tree
Hide file tree
Showing 17 changed files with 463 additions and 13 deletions.
2 changes: 1 addition & 1 deletion ServerlessWorkflow.Sdk.sln
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessWorkflow.Sdk.Buil
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessWorkflow.Sdk.IO", "src\ServerlessWorkflow.Sdk.IO\ServerlessWorkflow.Sdk.IO.csproj", "{9993989F-B8D6-481C-A59C-A76070CA32F4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudFlows.Sdk.UnitTests", "tests\ServerlessWorkflow.Sdk.UnitTests\CloudFlows.Sdk.UnitTests.csproj", "{7BFC0DDB-7864-4C5A-AC91-EB7B3E93242E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessWorkflow.Sdk.UnitTests", "tests\ServerlessWorkflow.Sdk.UnitTests\ServerlessWorkflow.Sdk.UnitTests.csproj", "{7BFC0DDB-7864-4C5A-AC91-EB7B3E93242E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha2.8</VersionSuffix>
<VersionSuffix>alpha2.9</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<NeutralLanguage>en</NeutralLanguage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,18 @@ public static IServiceCollection AddServerlessWorkflowIO(this IServiceCollection
YamlSerializer.DefaultSerializerConfiguration(options.Serializer);
YamlSerializer.DefaultDeserializerConfiguration(options.Deserializer);
options.Deserializer.WithNodeDeserializer(
inner => new TaskDefinitionYamlDeserializer(inner),
syntax => syntax.InsteadOf<JsonSchemaDeserializer>());
inner => new TaskDefinitionYamlDeserializer(inner),
syntax => syntax.InsteadOf<JsonSchemaDeserializer>());
options.Deserializer.WithNodeDeserializer(
inner => new OneOfNodeDeserializer(inner),
syntax => syntax.InsteadOf<TaskDefinitionYamlDeserializer>());
options.Deserializer.WithNodeDeserializer(
inner => new OneOfScalarDeserializer(inner),
syntax => syntax.InsteadOf<StringEnumDeserializer>());
var mapEntryConverter = new MapEntryYamlConverter(() => options.Serializer.Build(), () => options.Deserializer.Build());
options.Deserializer.WithTypeConverter(mapEntryConverter);
options.Serializer.WithTypeConverter(mapEntryConverter);
options.Serializer.WithTypeConverter(new OneOfConverter());
});
services.AddSingleton<IWorkflowDefinitionReader, WorkflowDefinitionReader>();
services.AddSingleton<IWorkflowDefinitionReader, WorkflowDefinitionReader>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix>alpha2.8</VersionSuffix>
<VersionSuffix>alpha2.9</VersionSuffix>
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
<FileVersion>$(VersionPrefix)</FileVersion>
<NeutralLanguage>en</NeutralLanguage>
Expand Down
28 changes: 28 additions & 0 deletions src/ServerlessWorkflow.Sdk/IOneOf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright © 2024-Present The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"),
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace ServerlessWorkflow.Sdk;

/// <summary>
/// Defines the fundamentals of a service that wraps around multiple alternative value types
/// </summary>
public interface IOneOf
{

/// <summary>
/// Gets the object's current value
/// </summary>
/// <returns>The object's current value</returns>
object? GetValue();

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public record HttpCallDefinition
/// </summary>
[Required]
[DataMember(Name = "endpoint", Order = 2), JsonPropertyName("endpoint"), JsonPropertyOrder(2), YamlMember(Alias = "endpoint", Order = 2)]
public required virtual EndpointDefinition Endpoint { get; set; }
public required virtual OneOf<EndpointDefinition, Uri> Endpoint { get; set; }

/// <summary>
/// Gets/sets a name/value mapping of the headers, if any, of the HTTP request to perform
Expand Down
96 changes: 96 additions & 0 deletions src/ServerlessWorkflow.Sdk/Models/OneOf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright © 2024-Present The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"),
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using ServerlessWorkflow.Sdk.Serialization.Json;

namespace ServerlessWorkflow.Sdk.Models;

/// <summary>
/// Gets an object that is one of the specified types
/// </summary>
/// <typeparam name="T1">A first type alternative</typeparam>
/// <typeparam name="T2">A second type alternative</typeparam>
[DataContract, JsonConverter(typeof(OneOfConverter))]
public class OneOf<T1, T2>
: IOneOf
{

/// <summary>
/// Initializes a new <see cref="OneOf{T1, T2}"/>
/// </summary>
/// <param name="value">The value of the <see cref="OneOf{T1, T2}"/></param>
public OneOf(T1 value)
{
this.TypeIndex = 1;
this.T1Value = value!;
}

/// <summary>
/// Initializes a new <see cref="OneOf{T1, T2}"/>
/// </summary>
/// <param name="value">The value of the <see cref="OneOf{T1, T2}"/></param>
public OneOf(T2 value)
{
this.TypeIndex = 2;
this.T2Value = value!;
}

/// <summary>
/// Gets the index of the discriminated type
/// </summary>
public int TypeIndex { get; }

/// <summary>
/// Gets the first possible value
/// </summary>
[DataMember(Order = 1), JsonIgnore, YamlIgnore]
public T1? T1Value { get; }

/// <summary>
/// Gets the second possible value
/// </summary>
[DataMember(Order = 2), JsonIgnore, YamlIgnore]
public T2? T2Value { get; }

object? IOneOf.GetValue() => this.TypeIndex switch
{
1 => this.T1Value,
2 => this.T2Value,
_ => null
};

/// <summary>
/// Implicitly convert the specified value into a new <see cref="OneOf{T1, T2}"/>
/// </summary>
/// <param name="value">The value to convert</param>
public static implicit operator OneOf<T1, T2>(T1 value) => new(value);

/// <summary>
/// Implicitly convert the specified value into a new <see cref="OneOf{T1, T2}"/>
/// </summary>
/// <param name="value">The value to convert</param>
public static implicit operator OneOf<T1, T2>(T2 value) => new(value);

/// <summary>
/// Implicitly convert the specified <see cref="OneOf{T1, T2}"/> into a new value
/// </summary>
/// <param name="value">The <see cref="OneOf{T1, T2}"/> to convert</param>
public static implicit operator T1?(OneOf<T1, T2> value) => value.T1Value;

/// <summary>
/// Implicitly convert the specified <see cref="OneOf{T1, T2}"/> into a new value
/// </summary>
/// <param name="value">The <see cref="OneOf{T1, T2}"/> to convert</param>
public static implicit operator T2?(OneOf<T1, T2> value) => value.T2Value;

}
2 changes: 0 additions & 2 deletions src/ServerlessWorkflow.Sdk/Models/SchemaDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Text.Json.Nodes;

namespace ServerlessWorkflow.Sdk.Models;

/// <summary>
Expand Down
95 changes: 95 additions & 0 deletions src/ServerlessWorkflow.Sdk/Serialization/Json/OneOfConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright © 2024-Present The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"),
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using ServerlessWorkflow.Sdk.Models;
using System.Collections.Concurrent;
using System.Text.Json;

namespace ServerlessWorkflow.Sdk.Serialization.Json;

/// <summary>
/// Represents the <see cref="JsonConverterFactory"/> used to serialize/deserialize to/from <see cref="IOneOf"/> instances
/// </summary>
public class OneOfConverter
: JsonConverterFactory
{

static readonly ConcurrentDictionary<Type, JsonConverter> ConverterCache = new();

/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType) return false;
var genericType = typeToConvert.GetGenericTypeDefinition();
return genericType == typeof(OneOf<,>);
}

/// <inheritdoc/>
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return ConverterCache.GetOrAdd(typeToConvert, (type) =>
{
var typeArgs = type.GetGenericArguments();
var converterType = typeof(OneOfConverterInner<,>).MakeGenericType(typeArgs);
return (JsonConverter?)Activator.CreateInstance(converterType)!;
});
}

class OneOfConverterInner<T1, T2> : JsonConverter<OneOf<T1, T2>>
{

/// <inheritdoc/>
public override OneOf<T1, T2>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null) return null;
var document = JsonDocument.ParseValue(ref reader);
var rootElement = document.RootElement;
try
{
var value1 = JsonSerializer.Deserialize<T1>(rootElement.GetRawText(), options);
if (value1 != null) return new OneOf<T1, T2>(value1);
}
catch (JsonException) { }
try
{
var value2 = JsonSerializer.Deserialize<T2>(rootElement.GetRawText(), options);
if (value2 != null) return new OneOf<T1, T2>(value2);
}
catch (JsonException) { throw new JsonException($"Cannot deserialize {rootElement.GetRawText()} as either {typeof(T1).Name} or {typeof(T2).Name}"); }
throw new JsonException("Unexpected error during deserialization.");
}

public override void Write(Utf8JsonWriter writer, OneOf<T1, T2> value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}
switch (value.TypeIndex)
{
case 1:
JsonSerializer.Serialize(writer, value.T1Value, options);
break;
case 2:
JsonSerializer.Serialize(writer, value.T2Value, options);
break;
default:
throw new JsonException("Invalid index value.");
}
}

}

}

Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ public override TaskDefinition Read(ref Utf8JsonReader reader, Type typeToConver
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, TaskDefinition value, JsonSerializerOptions options) => JsonSerializer.Serialize(writer, value, value.GetType(), options);

}
}
53 changes: 53 additions & 0 deletions src/ServerlessWorkflow.Sdk/Serialization/Yaml/OneOfConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright © 2024-Present The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"),
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Neuroglia.Serialization.Json;
using Neuroglia.Serialization.Yaml;
using ServerlessWorkflow.Sdk.Models;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;

namespace ServerlessWorkflow.Sdk.Serialization.Yaml;

/// <summary>
/// Represents the <see cref="IYamlTypeConverter"/> used to serialize and deserialize <see cref="OneOf{T1, T2}"/> instances
/// </summary>
public class OneOfConverter
: IYamlTypeConverter
{

/// <inheritdoc/>
public virtual bool Accepts(Type type) => type.GetGenericType(typeof(OneOf<,>)) != null;

/// <inheritdoc/>
public virtual object? ReadYaml(IParser parser, Type type) => throw new NotImplementedException();

/// <inheritdoc/>
public virtual void WriteYaml(IEmitter emitter, object? value, Type type)
{
if (value == null || value is not IOneOf oneOf)
{
emitter.Emit(new Scalar(null, null, string.Empty));
return;
}
var toSerialize = oneOf.GetValue();
if (toSerialize == null)
{
emitter.Emit(new Scalar(null, null, string.Empty));
return;
}
var jsonNode = JsonSerializer.Default.SerializeToNode(toSerialize);
new JsonNodeTypeConverter().WriteYaml(emitter, jsonNode, toSerialize.GetType());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright © 2024-Present The Serverless Workflow Specification Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"),
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using ServerlessWorkflow.Sdk.Models;
using Neuroglia.Serialization.Json;
using YamlDotNet.Core;

namespace ServerlessWorkflow.Sdk.Serialization.Yaml;

/// <summary>
/// Represents the service used to deserialize <see cref="OneOf{T1, T2}"/>s from YAML
/// </summary>
/// <param name="inner">The inner <see cref="INodeDeserializer"/></param>
public class OneOfNodeDeserializer(INodeDeserializer inner)
: INodeDeserializer
{

/// <summary>
/// Gets the inner <see cref="INodeDeserializer"/>
/// </summary>
protected INodeDeserializer Inner { get; } = inner;

/// <inheritdoc/>
public virtual bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
{
if (!typeof(IOneOf).IsAssignableFrom(expectedType)) return this.Inner.Deserialize(reader, expectedType, nestedObjectDeserializer, out value);
if (!this.Inner.Deserialize(reader, typeof(Dictionary<object, object>), nestedObjectDeserializer, out value)) return false;
value = JsonSerializer.Default.Deserialize(JsonSerializer.Default.SerializeToText(value!), expectedType);
return true;
}

}
Loading

0 comments on commit 73a219f

Please sign in to comment.