Skip to content

Commit 99105cd

Browse files
feat: add Parse and ToString support for PostgreSQL range literals
Implemented `Parse` and `ToString` methods across range types for interoperability with PostgreSQL range literal syntax. Added comprehensive unit tests for parsing and formatting, including roundtrip and edge cases. Updated documentation and introduced JSON converters for seamless serialization. Incremented version to `2.0.0`.
1 parent 7a9aebf commit 99105cd

19 files changed

Lines changed: 1524 additions & 35 deletions

CodoMetis.ValueRanges.Tests/CodoMetis.ValueRanges.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<IsPackable>false</IsPackable>
8+
<NoWarn>$(NoWarn);1591</NoWarn>
89
</PropertyGroup>
910

1011
<ItemGroup>
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using System.Text.Json;
2+
using CodoMetis.ValueRanges;
3+
using CodoMetis.ValueRanges.Serialization;
4+
5+
namespace CodoMetis.ValueRanges.Tests;
6+
7+
[TestClass]
8+
public class RangeJsonConverterTests
9+
{
10+
private static readonly JsonSerializerOptions Options =
11+
new JsonSerializerOptions().AddRangeConverters();
12+
13+
// -----------------------------------------------------------------------
14+
// Serialize
15+
// -----------------------------------------------------------------------
16+
17+
[TestMethod]
18+
public void Serialize_Empty_ProducesEmptyLiteral()
19+
{
20+
var json = JsonSerializer.Serialize(Int32Range.Empty, Options);
21+
Assert.AreEqual("\"empty\"", json);
22+
}
23+
24+
[TestMethod]
25+
public void Serialize_Infinity_ProducesInfinityLiteral()
26+
{
27+
var json = JsonSerializer.Serialize(Int32Range.Infinite, Options);
28+
Assert.AreEqual("\"(,)\"", json);
29+
}
30+
31+
[TestMethod]
32+
public void Serialize_FiniteInt32Range()
33+
{
34+
var range = Int32Range.CreateFinite(1, 10);
35+
var json = JsonSerializer.Serialize(range, Options);
36+
Assert.AreEqual("\"[1,10]\"", json);
37+
}
38+
39+
[TestMethod]
40+
public void Serialize_FiniteDecimalRange_HalfOpen()
41+
{
42+
var range = DecimalRange.CreateFinite(1.5m, 9.9m);
43+
var json = JsonSerializer.Serialize(range, Options);
44+
Assert.AreEqual("\"[1.5,9.9)\"", json);
45+
}
46+
47+
[TestMethod]
48+
public void Serialize_DateRange_UsesIso8601()
49+
{
50+
var range = DateRange.CreateFinite(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31));
51+
var json = JsonSerializer.Serialize(range, Options);
52+
Assert.AreEqual("\"[2024-01-01,2024-12-31]\"", json);
53+
}
54+
55+
[TestMethod]
56+
public void Serialize_AsPropertyInAnonymousObject()
57+
{
58+
var obj = new { Period = DateRange.CreateFinite(new DateOnly(2024, 1, 1), new DateOnly(2024, 12, 31)) };
59+
var json = JsonSerializer.Serialize(obj, Options);
60+
Assert.IsTrue(json.Contains("\"Period\":\"[2024-01-01,2024-12-31]\""), json);
61+
}
62+
63+
// -----------------------------------------------------------------------
64+
// Deserialize
65+
// -----------------------------------------------------------------------
66+
67+
[TestMethod]
68+
public void Deserialize_Empty_ReturnsEmptyRange()
69+
{
70+
var result = JsonSerializer.Deserialize<Int32Range>("\"empty\"", Options);
71+
Assert.IsNotNull(result);
72+
Assert.IsInstanceOfType<Int32Range.EmptyRange>(result);
73+
}
74+
75+
[TestMethod]
76+
public void Deserialize_Infinity_ReturnsInfinityRange()
77+
{
78+
var result = JsonSerializer.Deserialize<Int32Range>("\"(,)\"", Options);
79+
Assert.IsNotNull(result);
80+
Assert.IsInstanceOfType<Int32Range.Infinity>(result);
81+
}
82+
83+
[TestMethod]
84+
public void Deserialize_FiniteInt32Range()
85+
{
86+
var result = JsonSerializer.Deserialize<Int32Range>("\"[1,10]\"", Options);
87+
var finite = Assert.IsInstanceOfType<Int32Range.Finite>(result!);
88+
Assert.AreEqual(1, finite.Start);
89+
Assert.AreEqual(10, finite.End);
90+
}
91+
92+
[TestMethod]
93+
public void Deserialize_FiniteDecimalRange_HalfOpen()
94+
{
95+
var result = JsonSerializer.Deserialize<DecimalRange>("\"[1.5,9.9)\"", Options);
96+
var finite = Assert.IsInstanceOfType<DecimalRange.Finite>(result!);
97+
Assert.AreEqual(1.5m, finite.Start);
98+
Assert.AreEqual(9.9m, finite.End);
99+
Assert.IsTrue(finite.StartInclusive);
100+
Assert.IsFalse(finite.EndInclusive);
101+
}
102+
103+
[TestMethod]
104+
public void Deserialize_DateRange()
105+
{
106+
var result = JsonSerializer.Deserialize<DateRange>("\"[2024-01-01,2024-12-31]\"", Options);
107+
var finite = Assert.IsInstanceOfType<DateRange.Finite>(result!);
108+
Assert.AreEqual(new DateOnly(2024, 1, 1), finite.Start);
109+
Assert.AreEqual(new DateOnly(2024, 12, 31), finite.End);
110+
}
111+
112+
[TestMethod]
113+
public void Deserialize_NullJson_ThrowsJsonException()
114+
=> Assert.ThrowsExactly<JsonException>(
115+
() => JsonSerializer.Deserialize<Int32Range>("null", Options));
116+
117+
[TestMethod]
118+
public void Deserialize_InvalidLiteral_ThrowsJsonException()
119+
=> Assert.ThrowsExactly<JsonException>(
120+
() => JsonSerializer.Deserialize<Int32Range>("\"not-a-range\"", Options));
121+
122+
// -----------------------------------------------------------------------
123+
// RangeSet serialization
124+
// -----------------------------------------------------------------------
125+
126+
[TestMethod]
127+
public void Serialize_EmptySet()
128+
{
129+
var json = JsonSerializer.Serialize(RangeSet<Int32Range, int>.Empty, Options);
130+
Assert.AreEqual("\"{}\"", json);
131+
}
132+
133+
[TestMethod]
134+
public void Serialize_TwoElementSet()
135+
{
136+
var set = RangeSet<Int32Range, int>.From([
137+
Int32Range.CreateFinite(1, 5),
138+
Int32Range.CreateFinite(7, 10)
139+
]);
140+
var json = JsonSerializer.Serialize(set, Options);
141+
Assert.AreEqual("\"{[1,5],[7,10]}\"", json);
142+
}
143+
144+
[TestMethod]
145+
public void Deserialize_EmptySet()
146+
{
147+
var result = JsonSerializer.Deserialize<RangeSet<Int32Range, int>>("\"{}\"", Options);
148+
Assert.IsNotNull(result);
149+
Assert.AreEqual(0, result.Count);
150+
}
151+
152+
[TestMethod]
153+
public void Deserialize_TwoElementSet()
154+
{
155+
var result = JsonSerializer.Deserialize<RangeSet<Int32Range, int>>("\"{[1,5],[7,10]}\"", Options);
156+
Assert.IsNotNull(result);
157+
Assert.AreEqual(2, result.Count);
158+
}
159+
160+
// -----------------------------------------------------------------------
161+
// Roundtrip via JsonSerializer
162+
// -----------------------------------------------------------------------
163+
164+
[TestMethod]
165+
public void JsonRoundtrip_AllInt32RangeVariants()
166+
{
167+
Int32Range[] cases =
168+
[
169+
Int32Range.Empty,
170+
Int32Range.Infinite,
171+
Int32Range.CreateFinite(1, 100),
172+
Int32Range.CreateUnboundedStart(end: 50, endInclusive: true),
173+
Int32Range.CreateUnboundedEnd(start: 10, startInclusive: true)
174+
];
175+
176+
foreach (var original in cases)
177+
{
178+
var json = JsonSerializer.Serialize(original, Options);
179+
var parsed = JsonSerializer.Deserialize<Int32Range>(json, Options);
180+
Assert.AreEqual(original, parsed, $"Roundtrip failed for: {json}");
181+
}
182+
}
183+
184+
[TestMethod]
185+
public void JsonRoundtrip_DecimalRange()
186+
{
187+
var original = DecimalRange.CreateFinite(0.001m, 999.999m, startInclusive: true, endInclusive: false);
188+
var json = JsonSerializer.Serialize(original, Options);
189+
var parsed = JsonSerializer.Deserialize<DecimalRange>(json, Options);
190+
Assert.AreEqual(original, parsed);
191+
}
192+
193+
[TestMethod]
194+
public void JsonRoundtrip_RangeSet()
195+
{
196+
var original = RangeSet<DecimalRange, decimal>.From([
197+
DecimalRange.CreateFinite(0m, 10m),
198+
DecimalRange.CreateFinite(20m, 30m)
199+
]);
200+
var json = JsonSerializer.Serialize(original, Options);
201+
var parsed = JsonSerializer.Deserialize<RangeSet<DecimalRange, decimal>>(json, Options);
202+
Assert.AreEqual(original, parsed);
203+
}
204+
205+
// -----------------------------------------------------------------------
206+
// Per-type converters (explicit registration)
207+
// -----------------------------------------------------------------------
208+
209+
[TestMethod]
210+
public void ExplicitConverter_Int32Range_Roundtrip()
211+
{
212+
var opts = new JsonSerializerOptions();
213+
opts.Converters.Add(new Int32RangeJsonConverter());
214+
215+
var original = Int32Range.CreateFinite(3, 7);
216+
var json = JsonSerializer.Serialize(original, opts);
217+
var parsed = JsonSerializer.Deserialize<Int32Range>(json, opts);
218+
Assert.AreEqual(original, parsed);
219+
}
220+
221+
[TestMethod]
222+
public void ExplicitConverter_DateRangeSet_Roundtrip()
223+
{
224+
var opts = new JsonSerializerOptions();
225+
opts.Converters.Add(new DateRangeSetJsonConverter());
226+
227+
var original = RangeSet<DateRange, DateOnly>.From([
228+
DateRange.CreateFinite(new DateOnly(2024, 1, 1), new DateOnly(2024, 3, 31)),
229+
DateRange.CreateFinite(new DateOnly(2024, 7, 1), new DateOnly(2024, 9, 30))
230+
]);
231+
var json = JsonSerializer.Serialize(original, opts);
232+
var parsed = JsonSerializer.Deserialize<RangeSet<DateRange, DateOnly>>(json, opts);
233+
Assert.AreEqual(original, parsed);
234+
}
235+
}

0 commit comments

Comments
 (0)