Skip to content

Commit 2e2b305

Browse files
authored
Adding a Unit type (#22)
* Unit type and JSON converter * F# support for RustyOptions.Unit * F# unit conversion, tests * Unit… tests * Bumping version number * Json serialization tests
1 parent 213a6cc commit 2e2b305

File tree

13 files changed

+326
-9
lines changed

13 files changed

+326
-9
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Option and Result types for C#, inspired by Rust
44
[![CI](https://github.com/jtmueller/RustyOptions/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/jtmueller/RustyOptions/actions/workflows/build-and-test.yml)
55
[![CodeQL](https://github.com/jtmueller/RustyOptions/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/jtmueller/RustyOptions/actions/workflows/codeql-analysis.yml)
66
[![codecov](https://codecov.io/gh/jtmueller/RustyOptions/branch/main/graph/badge.svg?token=M81EJH4ZEI)](https://codecov.io/gh/jtmueller/RustyOptions)
7+
[![NuGet](https://buildstats.info/nuget/RustyOptions)](https://www.nuget.org/packages/RustyOptions/)
78

89
```
910
dotnet add package RustyOptions
@@ -173,8 +174,10 @@ For performance and convenience:
173174

174175
## FAQ
175176

176-
- This library only supports .NET 6 and above. What about .NET Framework?
177+
- This library only supports .NET 6 and above. What about .NET Framework? .NET 5? .NET Core 3.1?
177178
- You may want to consider the [Optional](https://github.com/nlkl/Optional) library for legacy framework support.
179+
- .NET Core 3.1 and .NET 5 are not supported because as of this writing they are no longer supported by Microsoft.
180+
However, if these runtimes are important to you, we welcome pull requests.
178181
- Why create this library if [Optional](https://github.com/nlkl/Optional) already exists?
179182
- I prefer the Rust Option/Result API methods and wanted to replicate those in C#.
180183
- I wanted to take advantage of modern .NET features like `ISpanParsable<T>` and `INumber<T>`.

src/RustyOptions.FSharp.Tests/RustyOptions.FSharp.Tests.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
55
<GenerateDocumentationFile>false</GenerateDocumentationFile>
66
<IsPackable>false</IsPackable>
7-
<ReleaseVersion>0.6.1</ReleaseVersion>
7+
<ReleaseVersion>0.7.0</ReleaseVersion>
88
<AssemblyName>RustyOptions.FSharp.Tests</AssemblyName>
99
</PropertyGroup>
1010

src/RustyOptions.FSharp.Tests/Tests.fs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,28 @@ let ``Can convert Result with module functions`` () =
7070
Assert.Equal(ok, okConverted)
7171
Assert.Equal(err, errConverted)
7272

73+
[<Fact>]
74+
let ``Can convert Unit`` () =
75+
let rustyUnit = RustyOptions.Unit.Default;
76+
let fsUnit = ()
77+
78+
Assert.Equal(fsUnit, rustyUnit.AsFSharpUnit());
79+
Assert.Equal(rustyUnit, fsUnit.AsRustyUnit());
80+
81+
[<Fact>]
82+
let ``Can convert Unit Result`` () =
83+
let rustyOk = RustyOptions.Result.Ok<RustyOptions.Unit, string>(RustyOptions.Unit.Default)
84+
let rustyErr = RustyOptions.Result.Err<RustyOptions.Unit, string>("oops")
85+
86+
let fsOk = Ok ()
87+
let fsErr = Error("oops")
88+
89+
Assert.Equal(fsOk, rustyOk |> Result.ofRustyUnitResult)
90+
Assert.Equal(fsErr, rustyErr |> Result.ofRustyUnitResult)
91+
92+
Assert.Equal(rustyOk, fsOk |> Result.toRustyUnitResult)
93+
Assert.Equal(rustyErr, fsErr |> Result.toRustyUnitResult)
94+
7395
#if NET7_0_OR_GREATER
7496
[<Fact>]
7597
let ``Can convert NumericOption with extension methods`` () =

src/RustyOptions.FSharp/Library.fs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,19 @@ module TypeExtensions =
2626
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
2727
member x.AsFSharpResult() =
2828
match x.IsOk() with
29-
| (true, value) -> Ok value
29+
| (true, value) -> Ok(value)
3030
| _ -> Error(x.UnwrapErr())
3131

32+
type RustyOptions.Unit with
33+
/// Converts a RustyOptions Unit into an F# unit.
34+
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
35+
member _.AsFSharpUnit() : unit = ()
36+
37+
type Microsoft.FSharp.Core.Unit with
38+
/// Converts an F# unit into a RustyOptions Unit.
39+
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
40+
member _.AsRustyUnit() = RustyOptions.Unit.Default
41+
3242
#if NET7_0_OR_GREATER
3343
type RustyOptions.NumericOption<'a when 'a : struct and 'a :> System.ValueType and 'a : (new : unit -> 'a) and 'a :> System.Numerics.INumber<'a>> with
3444

@@ -75,6 +85,17 @@ module CSharpTypeExtensions =
7585
| (true, value) -> Ok value
7686
| _ -> Error(x.UnwrapErr())
7787

88+
/// Converts a RustyOptions Unit Result into an F# unit Result.
89+
[<Extension>]
90+
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
91+
let AsFSharpUnitResult(x: RustyOptions.Result<RustyOptions.Unit, 'err>) =
92+
match x.IsOk() with
93+
| (true, _) -> Ok ()
94+
| _ -> Error(x.UnwrapErr())
95+
96+
// NOTE: We can't have an AsFSharpUnit method for C#, because C# interprets
97+
// a unit-returning F# function as if it's a void-returning C# function.
98+
7899
#if NET7_0_OR_GREATER
79100
/// This module provides C# extension methods on RustyOptions numeric types.
80101
[<Extension>]
@@ -176,3 +197,17 @@ module Result =
176197
match x with
177198
| Ok(value) -> RustyOptions.Result.Ok<'a, 'err>(value)
178199
| Error(err) -> RustyOptions.Result.Err<'a, 'err>(err)
200+
201+
/// Converts a RustyOptions Unit Result into an F# unit Result.
202+
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
203+
let ofRustyUnitResult (x: RustyOptions.Result<RustyOptions.Unit, 'err>) =
204+
match x.IsOk() with
205+
| (true, _) -> Ok ()
206+
| _ -> Error(x.UnwrapErr())
207+
208+
/// Converts an F# unit Result into a RustyOptions Unit Result.
209+
[<MethodImpl(MethodImplOptions.AggressiveInlining)>]
210+
let toRustyUnitResult (x: Result<unit, 'err>) =
211+
match x with
212+
| Ok(_) -> RustyOptions.Result.Ok<RustyOptions.Unit, 'err>(RustyOptions.Unit.Default)
213+
| Error(err) -> RustyOptions.Result.Err<RustyOptions.Unit, 'err>(err)

src/RustyOptions.FSharp/RustyOptions.FSharp.fsproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
<TargetFrameworks>net7.0;net6.0</TargetFrameworks>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
66
<AssemblyName>RustyOptions.FSharp</AssemblyName>
7-
<Version>0.6.1</Version>
8-
<ReleaseVersion>0.6.1</ReleaseVersion>
7+
<Version>0.7.0</Version>
8+
<ReleaseVersion>0.7.0</ReleaseVersion>
99
<PackageId>RustyOptions.FSharp</PackageId>
1010
<Authors>Joel Mueller</Authors>
1111
<Company></Company>

src/RustyOptions.Tests/FSharpConversionTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ public void CanConvertResult()
4343
Assert.Equal("oops", errFs.ErrorValue);
4444
}
4545

46+
[Fact]
47+
public void CanConvertUnitResult()
48+
{
49+
var ok = Result.Ok(Unit.Default);
50+
var err = Result.Err<Unit>("oops");
51+
52+
var okFs = ok.AsFSharpUnitResult();
53+
var errFs = err.AsFSharpUnitResult();
54+
55+
Assert.True(okFs.IsOk);
56+
Assert.Equal("oops", errFs.ErrorValue);
57+
}
58+
4659
#if NET7_0_OR_GREATER
4760
[Fact]
4861
public void CanConvertNumericOption()

src/RustyOptions.Tests/JsonSerializationTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,52 @@ public void CanSerializeResultErr()
216216
Assert.Equal(ResultErr, serialized);
217217
}
218218

219+
[Fact]
220+
public void CanSerializeUnit()
221+
{
222+
Unit unit;
223+
224+
var serialized = JsonSerializer.Serialize(unit);
225+
226+
Assert.Equal("null", serialized);
227+
}
228+
229+
[Fact]
230+
public void CanDeserializeUnit()
231+
{
232+
var deserialized = JsonSerializer.Deserialize<Unit>("null");
233+
234+
Assert.Equal(Unit.Default, deserialized);
235+
}
236+
237+
[Fact]
238+
public void CanSerializeUnitResult()
239+
{
240+
var ok = Result.Ok(Unit.Default);
241+
var err = Result.Err<Unit>("oops");
242+
243+
var serOk = JsonSerializer.Serialize(ok);
244+
var serErr = JsonSerializer.Serialize(err);
245+
246+
Assert.Equal("""{"ok":null}""", serOk);
247+
Assert.Equal("""{"err":"oops"}""", serErr);
248+
}
249+
250+
[Fact]
251+
public void CanDeserializeUnitResult()
252+
{
253+
var ok = Result.Ok(Unit.Default);
254+
var err = Result.Err<Unit>("oops");
255+
256+
var desOk = JsonSerializer.Deserialize<Result<Unit, string>>("""{"ok":null}""");
257+
var desErr = JsonSerializer.Deserialize<Result<Unit, string>>("""{"err":"oops"}""");
258+
259+
_ = Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<Result<Unit, string>>("""{"ok":1}"""));
260+
261+
Assert.Equal(ok, desOk);
262+
Assert.Equal(err, desErr);
263+
}
264+
219265
private const string OptionsAllSome = $$"""
220266
{"foo":42,"bar":17,"name":"Frank","lastUpdated":"{{DtoString}}"}
221267
""";

src/RustyOptions.Tests/RustyOptions.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<LangVersion>11</LangVersion>
99
<IsPackable>false</IsPackable>
1010
<AssemblyName>RustyOptions.Tests</AssemblyName>
11-
<ReleaseVersion>0.6.1</ReleaseVersion>
11+
<ReleaseVersion>0.7.0</ReleaseVersion>
1212
</PropertyGroup>
1313

1414
<ItemGroup>
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
namespace RustyOptions.Tests;
2+
3+
public sealed class UnitTests
4+
{
5+
[Fact]
6+
public void CanCreateAndCompare()
7+
{
8+
var u1 = Unit.Default;
9+
Unit u2;
10+
11+
Assert.True(u1.Equals(u2));
12+
Assert.True(u1.Equals((object)u2));
13+
Assert.False(u1.Equals(""));
14+
Assert.Equal(0, u1.GetHashCode());
15+
16+
Assert.True(u1 == u2);
17+
Assert.False(u1 != u2);
18+
Assert.False(u1 < u2);
19+
Assert.True(u1 <= u2);
20+
Assert.False(u1 > u2);
21+
Assert.True(u1 >= u2);
22+
Assert.Equal(u1, u1 + u2);
23+
24+
Assert.Equal(0, u1.CompareTo(u2));
25+
}
26+
27+
[Fact]
28+
public void CanConvertToString()
29+
{
30+
var u1 = Unit.Default;
31+
32+
Assert.Equal("()", u1.ToString());
33+
Assert.Equal("()", u1.ToString(null, null));
34+
35+
Span<char> buffer = stackalloc char[10];
36+
var success = u1.TryFormat(buffer, out int written, ReadOnlySpan<char>.Empty, null);
37+
38+
Assert.True(success);
39+
Assert.Equal(2, written);
40+
Assert.True(buffer[..written].SequenceEqual("()"));
41+
42+
buffer = Span<char>.Empty;
43+
success = u1.TryFormat(buffer, out written, ReadOnlySpan<char>.Empty, null);
44+
45+
Assert.False(success);
46+
Assert.Equal(0, written);
47+
}
48+
49+
[Fact]
50+
public void CanConvertToValueTuple()
51+
{
52+
Unit u1;
53+
ValueTuple vt1;
54+
55+
Unit u2 = vt1;
56+
ValueTuple vt2 = u1;
57+
58+
Assert.Equal(vt1, vt2);
59+
Assert.Equal(u1, u2);
60+
}
61+
}

src/RustyOptions.sln

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ Global
4747
EndGlobalSection
4848
GlobalSection(MonoDevelopProperties) = preSolution
4949
description = Option and Result types for C#, inspired by Rust
50-
version = 0.6.1
50+
version = 0.7.0
5151
EndGlobalSection
5252
EndGlobal

0 commit comments

Comments
 (0)