Skip to content

Commit

Permalink
Create SystemTextJson package
Browse files Browse the repository at this point in the history
  • Loading branch information
badeend committed Apr 3, 2024
1 parent a044d58 commit 61af006
Show file tree
Hide file tree
Showing 16 changed files with 736 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Badeend.ValueCollections.SystemTextJson</PackageId>
<Description>System.Text.Json converters for Badeend.ValueCollections</Description>
<TargetFrameworks>netstandard2.0;net8</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="8.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />

<PackageReference Include="Badeend.ValueCollections" Version="$(Version)" Condition="'$(ProjectReferences)' == 'Remote'" />
<ProjectReference Include="..\Badeend.ValueCollections\Badeend.ValueCollections.csproj" Condition="'$(ProjectReferences)' == 'Local'" />
</ItemGroup>
</Project>
48 changes: 48 additions & 0 deletions Badeend.ValueCollections.SystemTextJson/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

/// <summary>
/// Methods for configuring <c>System.Text.Json</c>.
/// </summary>
[SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces", Justification = "Don't care; the name `Configuration` is too generic and the type `System.Configuration` is not widespread enough for me to care.")]
public static class Configuration
{
private static readonly JsonConverter ValueSliceConverterFactory = new JsonArrayConverterFactory(typeof(ValueSlice<>), typeof(ValueSliceConverter<>));

private static readonly JsonConverter ValueListConverterFactory = new JsonArrayConverterFactory(typeof(ValueList<>), typeof(ValueListConverter<>));

private static readonly JsonConverter ValueListBuilderConverterFactory = new JsonArrayConverterFactory(typeof(ValueListBuilder<>), typeof(ValueListBuilderConverter<>));

private static readonly JsonConverter ValueSetConverterFactory = new JsonArrayConverterFactory(typeof(ValueSet<>), typeof(ValueSetConverter<>));

private static readonly JsonConverter ValueSetBuilderConverterFactory = new JsonArrayConverterFactory(typeof(ValueSetBuilder<>), typeof(ValueSetBuilderConverter<>));

private static readonly JsonConverter ValueDictionaryConverterFactory = new JsonObjectConverterFactory(typeof(ValueDictionary<,>), typeof(ValueDictionaryConverter<,>));

private static readonly JsonConverter ValueDictionaryBuilderConverterFactory = new JsonObjectConverterFactory(typeof(ValueDictionaryBuilder<,>), typeof(ValueDictionaryBuilderConverter<,>));

/// <summary>
/// Configure <c>System.Text.Json</c> to serialize and deserialize <c>Badeend.ValueCollections</c> data types.
/// </summary>
/// <returns>The <paramref name="options"/> instance for further chaining.</returns>
public static JsonSerializerOptions AddValueCollections(this JsonSerializerOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}

options.Converters.Add(ValueSliceConverterFactory);
options.Converters.Add(ValueListConverterFactory);
options.Converters.Add(ValueListBuilderConverterFactory);
options.Converters.Add(ValueSetConverterFactory);
options.Converters.Add(ValueSetBuilderConverterFactory);
options.Converters.Add(ValueDictionaryConverterFactory);
options.Converters.Add(ValueDictionaryBuilderConverterFactory);

return options;
}
}
53 changes: 53 additions & 0 deletions Badeend.ValueCollections.SystemTextJson/JsonArrayConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

/// <summary>
/// Utility for converters that read/write JSON arrays.
/// </summary>
internal readonly struct JsonArrayConverter<T>
{
private readonly JsonConverter<T> valueConverter;
private readonly Type valueType;

internal JsonArrayConverter(JsonConverter<T> valueConverter)
{
this.valueConverter = valueConverter;
this.valueType = typeof(T);
}

internal void ReadInto(ref Utf8JsonReader reader, ICollection<T> destination, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return;
}

var value = this.valueConverter.Read(ref reader, this.valueType, options)!;

destination.Add(value);
}

throw new JsonException();
}

internal void Write(Utf8JsonWriter writer, IEnumerable<T> source, JsonSerializerOptions options)
{
writer.WriteStartArray();

foreach (var value in source)
{
this.valueConverter.Write(writer, value, options);
}

writer.WriteEndArray();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

internal sealed class JsonArrayConverterFactory : JsonConverterFactory
{
private readonly Type genericValueType;
private readonly Type genericConverterType;

internal JsonArrayConverterFactory(Type genericValueType, Type genericConverterType)
{
this.genericValueType = genericValueType;
this.genericConverterType = genericConverterType;
}

public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
{
return false;
}

return typeToConvert.GetGenericTypeDefinition() == this.genericValueType;
}

public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type[] typeArguments = type.GetGenericArguments();
Type valueType = typeArguments[0];

JsonConverter converter = (JsonConverter)Activator.CreateInstance(
type: this.genericConverterType.MakeGenericType([valueType]),
args: [options.GetConverter(valueType)])!;

return converter;
}
}
71 changes: 71 additions & 0 deletions Badeend.ValueCollections.SystemTextJson/JsonObjectConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

/// <summary>
/// Utility for converters that read/write JSON objects.
/// </summary>
internal readonly struct JsonObjectConverter<TKey, TValue>
where TKey : notnull
{
private readonly JsonConverter<TKey> keyConverter;
private readonly Type keyType;
private readonly JsonConverter<TValue> valueConverter;
private readonly Type valueType;

internal JsonObjectConverter(JsonConverter<TKey> keyConverter, JsonConverter<TValue> valueConverter)
{
this.keyConverter = keyConverter;
this.keyType = typeof(TKey);
this.valueConverter = valueConverter;
this.valueType = typeof(TValue);
}

internal void ReadInto(ref Utf8JsonReader reader, IDictionary<TKey, TValue> destination, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}

while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return;
}

if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}

var key = this.keyConverter.ReadAsPropertyName(ref reader, this.keyType, options);

if (!reader.Read())
{
throw new JsonException();
}

var value = this.valueConverter.Read(ref reader, this.valueType, options)!;

destination.Add(key, value);
}

throw new JsonException();
}

internal void Write(Utf8JsonWriter writer, IEnumerable<KeyValuePair<TKey, TValue>> source, JsonSerializerOptions options)
{
writer.WriteStartObject();

foreach (var entry in source)
{
this.keyConverter.WriteAsPropertyName(writer, entry.Key, options);
this.valueConverter.Write(writer, entry.Value, options);
}

writer.WriteEndObject();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

internal sealed class JsonObjectConverterFactory : JsonConverterFactory
{
private readonly Type genericValueType;
private readonly Type genericConverterType;

internal JsonObjectConverterFactory(Type genericValueType, Type genericConverterType)
{
this.genericValueType = genericValueType;
this.genericConverterType = genericConverterType;
}

public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
{
return false;
}

return typeToConvert.GetGenericTypeDefinition() == this.genericValueType;
}

public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type[] typeArguments = type.GetGenericArguments();
Type keyType = typeArguments[0];
Type valueType = typeArguments[1];

JsonConverter converter = (JsonConverter)Activator.CreateInstance(
type: this.genericConverterType.MakeGenericType([keyType, valueType]),
args: [options.GetConverter(keyType), options.GetConverter(valueType)])!;

return converter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated using reflection")]
internal sealed class ValueDictionaryBuilderConverter<TKey, TValue>(JsonConverter<TKey> keyConverter, JsonConverter<TValue> valueConverter) : JsonConverter<ValueDictionaryBuilder<TKey, TValue>>
where TKey : notnull
{
private readonly JsonObjectConverter<TKey, TValue> inner = new(keyConverter, valueConverter);

public override ValueDictionaryBuilder<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var builder = new ValueDictionaryBuilder<TKey, TValue>();
this.inner.ReadInto(ref reader, builder, options);
return builder;
}

public override void Write(Utf8JsonWriter writer, ValueDictionaryBuilder<TKey, TValue> dictionary, JsonSerializerOptions options)
{
this.inner.Write(writer, dictionary, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated using reflection")]
internal sealed class ValueDictionaryConverter<TKey, TValue>(JsonConverter<TKey> keyConverter, JsonConverter<TValue> valueConverter) : JsonConverter<ValueDictionary<TKey, TValue>>
where TKey : notnull
{
private readonly JsonObjectConverter<TKey, TValue> inner = new(keyConverter, valueConverter);

public override ValueDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var builder = new ValueDictionaryBuilder<TKey, TValue>();
this.inner.ReadInto(ref reader, builder, options);
return builder.Build();
}

public override void Write(Utf8JsonWriter writer, ValueDictionary<TKey, TValue> dictionary, JsonSerializerOptions options)
{
this.inner.Write(writer, dictionary, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated using reflection")]
internal sealed class ValueListBuilderConverter<T>(JsonConverter<T> valueConverter) : JsonConverter<ValueListBuilder<T>>
{
private readonly JsonArrayConverter<T> inner = new(valueConverter);

public override ValueListBuilder<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var builder = new ValueListBuilder<T>();
this.inner.ReadInto(ref reader, builder, options);
return builder;
}

public override void Write(Utf8JsonWriter writer, ValueListBuilder<T> builder, JsonSerializerOptions options)
{
this.inner.Write(writer, builder, options);
}
}
23 changes: 23 additions & 0 deletions Badeend.ValueCollections.SystemTextJson/ValueListConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated using reflection")]
internal sealed class ValueListConverter<T>(JsonConverter<T> valueConverter) : JsonConverter<ValueList<T>>
{
private readonly JsonArrayConverter<T> inner = new(valueConverter);

public override ValueList<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var builder = new ValueListBuilder<T>();
this.inner.ReadInto(ref reader, builder, options);
return builder.Build();
}

public override void Write(Utf8JsonWriter writer, ValueList<T> list, JsonSerializerOptions options)
{
this.inner.Write(writer, list, options);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Badeend.ValueCollections.SystemTextJson;

[SuppressMessage("Microsoft.Performance", "CA1812:AvoidUninstantiatedInternalClasses", Justification = "Instantiated using reflection")]
internal sealed class ValueSetBuilderConverter<T>(JsonConverter<T> valueConverter) : JsonConverter<ValueSetBuilder<T>>
{
private readonly JsonArrayConverter<T> inner = new(valueConverter);

public override ValueSetBuilder<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var builder = new ValueSetBuilder<T>();
this.inner.ReadInto(ref reader, builder, options);
return builder;
}

public override void Write(Utf8JsonWriter writer, ValueSetBuilder<T> builder, JsonSerializerOptions options)
{
this.inner.Write(writer, builder, options);
}
}
Loading

0 comments on commit 61af006

Please sign in to comment.