Skip to content

Commit 73a219f

Browse files
committed
feat(Sdk): Added a new IOneOf abstraction and implementation
feat(IO): Added JSON and YAML serializers for the new OneOf type fix(Sdk): Transformed endpoints into OneOf Signed-off-by: Charles d'Avernas <[email protected]>
1 parent 141c77b commit 73a219f

17 files changed

+463
-13
lines changed

Diff for: ServerlessWorkflow.Sdk.sln

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessWorkflow.Sdk.Buil
2828
EndProject
2929
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessWorkflow.Sdk.IO", "src\ServerlessWorkflow.Sdk.IO\ServerlessWorkflow.Sdk.IO.csproj", "{9993989F-B8D6-481C-A59C-A76070CA32F4}"
3030
EndProject
31-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudFlows.Sdk.UnitTests", "tests\ServerlessWorkflow.Sdk.UnitTests\CloudFlows.Sdk.UnitTests.csproj", "{7BFC0DDB-7864-4C5A-AC91-EB7B3E93242E}"
31+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServerlessWorkflow.Sdk.UnitTests", "tests\ServerlessWorkflow.Sdk.UnitTests\ServerlessWorkflow.Sdk.UnitTests.csproj", "{7BFC0DDB-7864-4C5A-AC91-EB7B3E93242E}"
3232
EndProject
3333
Global
3434
GlobalSection(SolutionConfigurationPlatforms) = preSolution

Diff for: src/ServerlessWorkflow.Sdk.Builders/ServerlessWorkflow.Sdk.Builders.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<VersionPrefix>1.0.0</VersionPrefix>
8-
<VersionSuffix>alpha2.8</VersionSuffix>
8+
<VersionSuffix>alpha2.9</VersionSuffix>
99
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
1010
<FileVersion>$(VersionPrefix)</FileVersion>
1111
<NeutralLanguage>en</NeutralLanguage>

Diff for: src/ServerlessWorkflow.Sdk.IO/Extensions/IServiceCollectionExtensions.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,18 @@ public static IServiceCollection AddServerlessWorkflowIO(this IServiceCollection
3737
YamlSerializer.DefaultSerializerConfiguration(options.Serializer);
3838
YamlSerializer.DefaultDeserializerConfiguration(options.Deserializer);
3939
options.Deserializer.WithNodeDeserializer(
40-
inner => new TaskDefinitionYamlDeserializer(inner),
41-
syntax => syntax.InsteadOf<JsonSchemaDeserializer>());
40+
inner => new TaskDefinitionYamlDeserializer(inner),
41+
syntax => syntax.InsteadOf<JsonSchemaDeserializer>());
42+
options.Deserializer.WithNodeDeserializer(
43+
inner => new OneOfNodeDeserializer(inner),
44+
syntax => syntax.InsteadOf<TaskDefinitionYamlDeserializer>());
45+
options.Deserializer.WithNodeDeserializer(
46+
inner => new OneOfScalarDeserializer(inner),
47+
syntax => syntax.InsteadOf<StringEnumDeserializer>());
4248
var mapEntryConverter = new MapEntryYamlConverter(() => options.Serializer.Build(), () => options.Deserializer.Build());
4349
options.Deserializer.WithTypeConverter(mapEntryConverter);
4450
options.Serializer.WithTypeConverter(mapEntryConverter);
51+
options.Serializer.WithTypeConverter(new OneOfConverter());
4552
});
4653
services.AddSingleton<IWorkflowDefinitionReader, WorkflowDefinitionReader>();
4754
services.AddSingleton<IWorkflowDefinitionReader, WorkflowDefinitionReader>();

Diff for: src/ServerlessWorkflow.Sdk.IO/ServerlessWorkflow.Sdk.IO.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<VersionPrefix>1.0.0</VersionPrefix>
8-
<VersionSuffix>alpha2.8</VersionSuffix>
8+
<VersionSuffix>alpha2.9</VersionSuffix>
99
<AssemblyVersion>$(VersionPrefix)</AssemblyVersion>
1010
<FileVersion>$(VersionPrefix)</FileVersion>
1111
<NeutralLanguage>en</NeutralLanguage>

Diff for: src/ServerlessWorkflow.Sdk/IOneOf.cs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright © 2024-Present The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
namespace ServerlessWorkflow.Sdk;
15+
16+
/// <summary>
17+
/// Defines the fundamentals of a service that wraps around multiple alternative value types
18+
/// </summary>
19+
public interface IOneOf
20+
{
21+
22+
/// <summary>
23+
/// Gets the object's current value
24+
/// </summary>
25+
/// <returns>The object's current value</returns>
26+
object? GetValue();
27+
28+
}

Diff for: src/ServerlessWorkflow.Sdk/Models/Calls/HttpCallDefinition.cs

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

3838
/// <summary>
3939
/// Gets/sets a name/value mapping of the headers, if any, of the HTTP request to perform

Diff for: src/ServerlessWorkflow.Sdk/Models/OneOf.cs

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright © 2024-Present The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using ServerlessWorkflow.Sdk.Serialization.Json;
15+
16+
namespace ServerlessWorkflow.Sdk.Models;
17+
18+
/// <summary>
19+
/// Gets an object that is one of the specified types
20+
/// </summary>
21+
/// <typeparam name="T1">A first type alternative</typeparam>
22+
/// <typeparam name="T2">A second type alternative</typeparam>
23+
[DataContract, JsonConverter(typeof(OneOfConverter))]
24+
public class OneOf<T1, T2>
25+
: IOneOf
26+
{
27+
28+
/// <summary>
29+
/// Initializes a new <see cref="OneOf{T1, T2}"/>
30+
/// </summary>
31+
/// <param name="value">The value of the <see cref="OneOf{T1, T2}"/></param>
32+
public OneOf(T1 value)
33+
{
34+
this.TypeIndex = 1;
35+
this.T1Value = value!;
36+
}
37+
38+
/// <summary>
39+
/// Initializes a new <see cref="OneOf{T1, T2}"/>
40+
/// </summary>
41+
/// <param name="value">The value of the <see cref="OneOf{T1, T2}"/></param>
42+
public OneOf(T2 value)
43+
{
44+
this.TypeIndex = 2;
45+
this.T2Value = value!;
46+
}
47+
48+
/// <summary>
49+
/// Gets the index of the discriminated type
50+
/// </summary>
51+
public int TypeIndex { get; }
52+
53+
/// <summary>
54+
/// Gets the first possible value
55+
/// </summary>
56+
[DataMember(Order = 1), JsonIgnore, YamlIgnore]
57+
public T1? T1Value { get; }
58+
59+
/// <summary>
60+
/// Gets the second possible value
61+
/// </summary>
62+
[DataMember(Order = 2), JsonIgnore, YamlIgnore]
63+
public T2? T2Value { get; }
64+
65+
object? IOneOf.GetValue() => this.TypeIndex switch
66+
{
67+
1 => this.T1Value,
68+
2 => this.T2Value,
69+
_ => null
70+
};
71+
72+
/// <summary>
73+
/// Implicitly convert the specified value into a new <see cref="OneOf{T1, T2}"/>
74+
/// </summary>
75+
/// <param name="value">The value to convert</param>
76+
public static implicit operator OneOf<T1, T2>(T1 value) => new(value);
77+
78+
/// <summary>
79+
/// Implicitly convert the specified value into a new <see cref="OneOf{T1, T2}"/>
80+
/// </summary>
81+
/// <param name="value">The value to convert</param>
82+
public static implicit operator OneOf<T1, T2>(T2 value) => new(value);
83+
84+
/// <summary>
85+
/// Implicitly convert the specified <see cref="OneOf{T1, T2}"/> into a new value
86+
/// </summary>
87+
/// <param name="value">The <see cref="OneOf{T1, T2}"/> to convert</param>
88+
public static implicit operator T1?(OneOf<T1, T2> value) => value.T1Value;
89+
90+
/// <summary>
91+
/// Implicitly convert the specified <see cref="OneOf{T1, T2}"/> into a new value
92+
/// </summary>
93+
/// <param name="value">The <see cref="OneOf{T1, T2}"/> to convert</param>
94+
public static implicit operator T2?(OneOf<T1, T2> value) => value.T2Value;
95+
96+
}

Diff for: src/ServerlessWorkflow.Sdk/Models/SchemaDefinition.cs

-2
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
using System.Text.Json.Nodes;
15-
1614
namespace ServerlessWorkflow.Sdk.Models;
1715

1816
/// <summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright © 2024-Present The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using ServerlessWorkflow.Sdk.Models;
15+
using System.Collections.Concurrent;
16+
using System.Text.Json;
17+
18+
namespace ServerlessWorkflow.Sdk.Serialization.Json;
19+
20+
/// <summary>
21+
/// Represents the <see cref="JsonConverterFactory"/> used to serialize/deserialize to/from <see cref="IOneOf"/> instances
22+
/// </summary>
23+
public class OneOfConverter
24+
: JsonConverterFactory
25+
{
26+
27+
static readonly ConcurrentDictionary<Type, JsonConverter> ConverterCache = new();
28+
29+
/// <inheritdoc/>
30+
public override bool CanConvert(Type typeToConvert)
31+
{
32+
if (!typeToConvert.IsGenericType) return false;
33+
var genericType = typeToConvert.GetGenericTypeDefinition();
34+
return genericType == typeof(OneOf<,>);
35+
}
36+
37+
/// <inheritdoc/>
38+
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
39+
{
40+
return ConverterCache.GetOrAdd(typeToConvert, (type) =>
41+
{
42+
var typeArgs = type.GetGenericArguments();
43+
var converterType = typeof(OneOfConverterInner<,>).MakeGenericType(typeArgs);
44+
return (JsonConverter?)Activator.CreateInstance(converterType)!;
45+
});
46+
}
47+
48+
class OneOfConverterInner<T1, T2> : JsonConverter<OneOf<T1, T2>>
49+
{
50+
51+
/// <inheritdoc/>
52+
public override OneOf<T1, T2>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
53+
{
54+
if (reader.TokenType == JsonTokenType.Null) return null;
55+
var document = JsonDocument.ParseValue(ref reader);
56+
var rootElement = document.RootElement;
57+
try
58+
{
59+
var value1 = JsonSerializer.Deserialize<T1>(rootElement.GetRawText(), options);
60+
if (value1 != null) return new OneOf<T1, T2>(value1);
61+
}
62+
catch (JsonException) { }
63+
try
64+
{
65+
var value2 = JsonSerializer.Deserialize<T2>(rootElement.GetRawText(), options);
66+
if (value2 != null) return new OneOf<T1, T2>(value2);
67+
}
68+
catch (JsonException) { throw new JsonException($"Cannot deserialize {rootElement.GetRawText()} as either {typeof(T1).Name} or {typeof(T2).Name}"); }
69+
throw new JsonException("Unexpected error during deserialization.");
70+
}
71+
72+
public override void Write(Utf8JsonWriter writer, OneOf<T1, T2> value, JsonSerializerOptions options)
73+
{
74+
if (value is null)
75+
{
76+
writer.WriteNullValue();
77+
return;
78+
}
79+
switch (value.TypeIndex)
80+
{
81+
case 1:
82+
JsonSerializer.Serialize(writer, value.T1Value, options);
83+
break;
84+
case 2:
85+
JsonSerializer.Serialize(writer, value.T2Value, options);
86+
break;
87+
default:
88+
throw new JsonException("Invalid index value.");
89+
}
90+
}
91+
92+
}
93+
94+
}
95+

Diff for: src/ServerlessWorkflow.Sdk/Serialization/Json/TaskDefinitionJsonConverter.cs

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

51-
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright © 2024-Present The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using Neuroglia.Serialization.Json;
15+
using Neuroglia.Serialization.Yaml;
16+
using ServerlessWorkflow.Sdk.Models;
17+
using YamlDotNet.Core;
18+
using YamlDotNet.Core.Events;
19+
20+
namespace ServerlessWorkflow.Sdk.Serialization.Yaml;
21+
22+
/// <summary>
23+
/// Represents the <see cref="IYamlTypeConverter"/> used to serialize and deserialize <see cref="OneOf{T1, T2}"/> instances
24+
/// </summary>
25+
public class OneOfConverter
26+
: IYamlTypeConverter
27+
{
28+
29+
/// <inheritdoc/>
30+
public virtual bool Accepts(Type type) => type.GetGenericType(typeof(OneOf<,>)) != null;
31+
32+
/// <inheritdoc/>
33+
public virtual object? ReadYaml(IParser parser, Type type) => throw new NotImplementedException();
34+
35+
/// <inheritdoc/>
36+
public virtual void WriteYaml(IEmitter emitter, object? value, Type type)
37+
{
38+
if (value == null || value is not IOneOf oneOf)
39+
{
40+
emitter.Emit(new Scalar(null, null, string.Empty));
41+
return;
42+
}
43+
var toSerialize = oneOf.GetValue();
44+
if (toSerialize == null)
45+
{
46+
emitter.Emit(new Scalar(null, null, string.Empty));
47+
return;
48+
}
49+
var jsonNode = JsonSerializer.Default.SerializeToNode(toSerialize);
50+
new JsonNodeTypeConverter().WriteYaml(emitter, jsonNode, toSerialize.GetType());
51+
}
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright © 2024-Present The Serverless Workflow Specification Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using ServerlessWorkflow.Sdk.Models;
15+
using Neuroglia.Serialization.Json;
16+
using YamlDotNet.Core;
17+
18+
namespace ServerlessWorkflow.Sdk.Serialization.Yaml;
19+
20+
/// <summary>
21+
/// Represents the service used to deserialize <see cref="OneOf{T1, T2}"/>s from YAML
22+
/// </summary>
23+
/// <param name="inner">The inner <see cref="INodeDeserializer"/></param>
24+
public class OneOfNodeDeserializer(INodeDeserializer inner)
25+
: INodeDeserializer
26+
{
27+
28+
/// <summary>
29+
/// Gets the inner <see cref="INodeDeserializer"/>
30+
/// </summary>
31+
protected INodeDeserializer Inner { get; } = inner;
32+
33+
/// <inheritdoc/>
34+
public virtual bool Deserialize(IParser reader, Type expectedType, Func<IParser, Type, object?> nestedObjectDeserializer, out object? value)
35+
{
36+
if (!typeof(IOneOf).IsAssignableFrom(expectedType)) return this.Inner.Deserialize(reader, expectedType, nestedObjectDeserializer, out value);
37+
if (!this.Inner.Deserialize(reader, typeof(Dictionary<object, object>), nestedObjectDeserializer, out value)) return false;
38+
value = JsonSerializer.Default.Deserialize(JsonSerializer.Default.SerializeToText(value!), expectedType);
39+
return true;
40+
}
41+
42+
}

0 commit comments

Comments
 (0)