Skip to content

Commit

Permalink
Merge pull request #930 from MrLuje/fsharp-option
Browse files Browse the repository at this point in the history
Add FSharpOption support

+semver:feature
  • Loading branch information
EdwardCooke committed Jun 16, 2024
2 parents 191087f + cb1c48a commit 23991bd
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 9 deletions.
87 changes: 87 additions & 0 deletions YamlDotNet.Fsharp.Test/DeserializerTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
module DeserializerTests

open System
open Xunit
open YamlDotNet.Serialization
open YamlDotNet.Serialization.NamingConventions
open FsUnit.Xunit
open System.ComponentModel

[<CLIMutable>]
type Spec = {
EngineType: string
DriveType: string
}

[<CLIMutable>]
type Car = {
Name: string
Year: int
Spec: Spec option
Nickname: string option
}

[<CLIMutable>]
type Person = {
Name: string
MomentOfBirth: DateTime
Cars: Car array
}

[<Fact>]
let Deserialize_YamlWithScalarOptions() =
let yaml = """
name: Jack
momentOfBirth: 1983-04-21T20:21:03.0041599Z
cars:
- name: Mercedes
year: 2018
nickname: Jessy
- name: Honda
year: 2021
"""
let sut = DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build()

let person = sut.Deserialize<Person>(yaml)
person.Name |> should equal "Jack"
person.Cars |> should haveLength 2
person.Cars[0].Name |> should equal "Mercedes"
person.Cars[0].Nickname |> should equal (Some "Jessy")
person.Cars[1].Name |> should equal "Honda"
person.Cars[1].Nickname |> should equal None


[<Fact>]
let Deserialize_YamlWithObjectOptions() =
let yaml = """
name: Jack
momentOfBirth: 1983-04-21T20:21:03.0041599Z
cars:
- name: Mercedes
year: 2018
spec:
engineType: V6
driveType: AWD
- name: Honda
year: 2021
"""
let sut = DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build()

let person = sut.Deserialize<Person>(yaml)
person.Name |> should equal "Jack"
person.Cars |> should haveLength 2

person.Cars[0].Name |> should equal "Mercedes"
person.Cars[0].Spec |> should not' (be null)
person.Cars[0].Spec |> Option.isSome |> should equal true
person.Cars[0].Spec.Value.EngineType |> should equal "V6"
person.Cars[0].Spec.Value.DriveType |> should equal "AWD"

person.Cars[1].Name |> should equal "Honda"
person.Cars[1].Spec |> should be null
person.Cars[1].Spec |> should equal None
person.Cars[1].Nickname |> should equal None
152 changes: 152 additions & 0 deletions YamlDotNet.Fsharp.Test/SerializerTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
module SerializerTests

open System
open Xunit
open YamlDotNet.Serialization
open YamlDotNet.Serialization.NamingConventions
open FsUnit.Xunit
open YamlDotNet.Core

[<CLIMutable>]
type Spec = {
EngineType: string
DriveType: string
}

[<CLIMutable>]
type Car = {
Name: string
Year: int
Spec: Spec option
Nickname: string option
}

[<CLIMutable>]
type Person = {
Name: string
MomentOfBirth: DateTime
KidsSeat: int option
Cars: Car array
}

[<Fact>]
let Serialize_YamlWithScalarOptions() =
let jackTheDriver = {
Name = "Jack"
MomentOfBirth = DateTime(1983, 4, 21, 20, 21, 03, 4)
KidsSeat = Some 1
Cars = [|
{ Name = "Mercedes"
Year = 2018
Nickname = Some "Jessy"
Spec = None };
{ Name = "Honda"
Year = 2021
Nickname = None
Spec = None }
|]
}

let yaml = """name: Jack
momentOfBirth: 1983-04-21T20:21:03.0040000
kidsSeat: 1
cars:
- name: Mercedes
year: 2018
spec:
nickname: Jessy
- name: Honda
year: 2021
spec:
nickname:
"""
let sut = SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build()

let person = sut.Serialize(jackTheDriver)
person |> should equal yaml


[<Fact>]
let Serialize_YamlWithScalarOptions_OmitNull() =
let jackTheDriver = {
Name = "Jack"
MomentOfBirth = DateTime(1983, 4, 21, 20, 21, 03, 4)
KidsSeat = Some 1
Cars = [|
{ Name = "Mercedes"
Year = 2018
Nickname = Some "Jessy"
Spec = None };
{ Name = "Honda"
Year = 2021
Nickname = None
Spec = None }
|]
}

let yaml = """name: Jack
momentOfBirth: 1983-04-21T20:21:03.0040000
kidsSeat: 1
cars:
- name: Mercedes
year: 2018
nickname: Jessy
- name: Honda
year: 2021
"""
let sut = SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
.Build()

let person = sut.Serialize(jackTheDriver)
person |> should equal yaml


[<Fact>]
let Serialize_YamlWithObjectOptions_OmitNull() =
let jackTheDriver = {
Name = "Jack"
MomentOfBirth = DateTime(1983, 4, 21, 20, 21, 03, 4)
KidsSeat = Some 1
Cars = [|
{ Name = "Mercedes"
Year = 2018
Nickname = None
Spec = Some {
EngineType = "V6"
DriveType = "AWD"
} };
{ Name = "Honda"
Year = 2021
Nickname = None
Spec = None }
|]
}

let yaml = """name: Jack
momentOfBirth: 1983-04-21T20:21:03.0040000
kidsSeat: 1
cars:
- name: Mercedes
year: 2018
spec:
engineType: V6
driveType: AWD
- name: Honda
year: 2021
"""
let sut = SerializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull)
.Build()

let person = sut.Serialize(jackTheDriver)
person |> should equal yaml

type TestOmit = {
name: string
plop: int option
}
25 changes: 25 additions & 0 deletions YamlDotNet.Fsharp.Test/YamlDotNet.Fsharp.Test.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net7.0;net6.0;net47</TargetFrameworks>
<IsPackable>false</IsPackable>
<AssemblyOriginatorKeyFile>..\YamlDotNet.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<LangVersion>8.0</LangVersion>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<Compile Include="DeserializerTests.fs" />
<Compile Include="SerializerTests.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="FsUnit.xUnit" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YamlDotNet\YamlDotNet.csproj" />
<ProjectReference Include="..\YamlDotNet.Analyzers.StaticGenerator\YamlDotNet.Analyzers.StaticGenerator.csproj" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions YamlDotNet.sln
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "YamlDotNet.Samples.Fsharp",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YamlDotNet.Core7AoTCompileTest.Model", "YamlDotNet.Core7AoTCompileTest.Model\YamlDotNet.Core7AoTCompileTest.Model.csproj", "{BFE15564-7C2C-47DA-8302-9BCB39B6864B}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "YamlDotNet.Fsharp.Test", "YamlDotNet.Fsharp.Test\YamlDotNet.Fsharp.Test.fsproj", "{294EFEB3-4DC2-4105-ADE7-E429F5522419}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -69,6 +71,10 @@ Global
{BFE15564-7C2C-47DA-8302-9BCB39B6864B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFE15564-7C2C-47DA-8302-9BCB39B6864B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFE15564-7C2C-47DA-8302-9BCB39B6864B}.Release|Any CPU.Build.0 = Release|Any CPU
{294EFEB3-4DC2-4105-ADE7-E429F5522419}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{294EFEB3-4DC2-4105-ADE7-E429F5522419}.Debug|Any CPU.Build.0 = Debug|Any CPU
{294EFEB3-4DC2-4105-ADE7-E429F5522419}.Release|Any CPU.ActiveCfg = Release|Any CPU
{294EFEB3-4DC2-4105-ADE7-E429F5522419}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
38 changes: 38 additions & 0 deletions YamlDotNet/Helpers/FsharpHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using YamlDotNet.Serialization;

namespace YamlDotNet.Helpers
{
public static class FsharpHelper
{
private static bool IsFsharp(Type t)
{
return t.Namespace == "Microsoft.FSharp.Core";
}

public static bool IsOptionType(Type t)
{
return IsFsharp(t) && t.Name == "FSharpOption`1";
}

public static Type? GetOptionUnderlyingType(Type t)
{
return t.IsGenericType && IsOptionType(t) ? t.GenericTypeArguments[0] : null;
}

public static object? GetValue(IObjectDescriptor objectDescriptor)
{
if (!IsOptionType(objectDescriptor.Type))
{
throw new InvalidOperationException("Should not be called on non-Option<> type");
}

if (objectDescriptor.Value is null)
{
return null;
}

return objectDescriptor.Type.GetProperty("Value").GetValue(objectDescriptor.Value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using System.Runtime.Serialization;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Helpers;
using YamlDotNet.Serialization.Utilities;

namespace YamlDotNet.Serialization.NodeDeserializers
Expand Down Expand Up @@ -60,8 +61,10 @@ public bool Deserialize(IParser parser, Type expectedType, Func<IParser, Type, o
return false;
}

// Strip off the nullable type, if present. This is needed for nullable structs.
var implementationType = Nullable.GetUnderlyingType(expectedType) ?? expectedType;
// Strip off the nullable & fsharp option type, if present. This is needed for nullable structs.
var implementationType = Nullable.GetUnderlyingType(expectedType)
?? FsharpHelper.GetOptionUnderlyingType(expectedType)
?? expectedType;

value = objectFactory.Create(implementationType);
objectFactory.ExecuteOnDeserializing(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ public bool Deserialize(IParser parser, Type expectedType, Func<IParser, Type, o
return false;
}

// Strip off the nullable type, if present
var underlyingType = Nullable.GetUnderlyingType(expectedType) ?? expectedType;
// Strip off the nullable & fsharp option type, if present
var underlyingType = Nullable.GetUnderlyingType(expectedType)
?? FsharpHelper.GetOptionUnderlyingType(expectedType)
?? expectedType;

if (underlyingType.IsEnum())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,31 @@ protected virtual void Traverse<TContext>(object name, IObjectDescriptor value,
break;
}

var underlyingType = Nullable.GetUnderlyingType(value.Type);
if (underlyingType != null)
var nullableUnderlyingType = Nullable.GetUnderlyingType(value.Type);
var optionUnderlyingType = nullableUnderlyingType ?? FsharpHelper.GetOptionUnderlyingType(value.Type);
var optionValue = optionUnderlyingType != null ? FsharpHelper.GetValue(value) : null;

if (nullableUnderlyingType != null)
{
// This is a nullable type, recursively handle it with its underlying type.
// Note that if it contains null, the condition above already took care of it
Traverse("Value", new ObjectDescriptor(value.Value, underlyingType, value.Type, value.ScalarStyle), visitor, context, path);
Traverse(
"Value",
new ObjectDescriptor(value.Value, nullableUnderlyingType, value.Type, value.ScalarStyle),
visitor,
context,
path
);
}
else if (optionUnderlyingType != null && optionValue != null)
{
Traverse(
"Value",
new ObjectDescriptor(FsharpHelper.GetValue(value), optionUnderlyingType, value.Type, value.ScalarStyle),
visitor,
context,
path
);
}
else
{
Expand Down
Loading

0 comments on commit 23991bd

Please sign in to comment.