Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wrap parsed IJsonValue and its backing JsonDocument into a disposable container #279

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ public LocatedSchema GetLocatedSchema(JsonReference location)
/// Tries to get the located schema for the given scope.
/// </summary>
/// <param name="location">The Location for which to find the schema.</param>
/// <param name="schema">The schema found at the locatio.</param>
/// <param name="schema">The schema found at the location.</param>
/// <returns><see langword="true"/> when the schema is found.</returns>
public bool TryGetValue(JsonReference location, [NotNullWhen(true)] out LocatedSchema? schema)
{
Expand Down
85 changes: 85 additions & 0 deletions Solutions/Corvus.Json.ExtendedTypes/Corvus.Json/ParsedValue{T}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// <copyright file="ParsedValue{T}.cs" company="Endjin Limited">
// Copyright (c) Endjin Limited. All rights reserved.
// </copyright>

using System.Text.Json;

namespace Corvus.Json;

/// <summary>
/// Represents a parsed instance of a type.
/// </summary>
/// <typeparam name="T">The type of the <see cref="IJsonValue"/> to parse.</typeparam>
/// <remarks>
/// This provides a disposable wrapper around an underlying <see cref="JsonDocument"/> and the parsed value.
/// It saves you writing the boilerplate code to create and dispose the <see cref="JsonDocument"/> when you're done with it.
/// </remarks>
public readonly struct ParsedValue<T> : IDisposable
where T : struct, IJsonValue<T>
{
private readonly JsonDocument jsonDocument;

private ParsedValue(JsonDocument jsonDocument, T value)
{
this.jsonDocument = jsonDocument;
this.Instance = value;
}

/// <summary>
/// Gets the instance of the parsed value.
/// </summary>
public T Instance { get; }

/// <summary>
/// Parse a JSON document into a value.
/// </summary>
/// <param name="utf8Json">The UTF8 JSON stream to parse.</param>
/// <returns>The parsed value.</returns>
public static ParsedValue<T> Parse(Stream utf8Json)
{
var document = JsonDocument.Parse(utf8Json);
return new(document, T.FromJson(document.RootElement));
}

/// <summary>
/// Parse a JSON document into a value.
/// </summary>
/// <param name="json">The JSON string to parse.</param>
/// <returns>The parsed value.</returns>
public static ParsedValue<T> Parse(string json)
{
var document = JsonDocument.Parse(json);
return new(document, T.FromJson(document.RootElement));
}

/// <summary>
/// Parse a JSON document into a value.
/// </summary>
/// <param name="utf8Json">The JSON string to parse.</param>
/// <returns>The parsed value.</returns>
public static ParsedValue<T> Parse(ReadOnlyMemory<byte> utf8Json)
{
var document = JsonDocument.Parse(utf8Json);
return new(document, T.FromJson(document.RootElement));
}

/// <summary>
/// Parse a JSON document into a value.
/// </summary>
/// <param name="json">The JSON string to parse.</param>
/// <returns>The parsed value.</returns>
public static ParsedValue<T> Parse(ReadOnlyMemory<char> json)
{
var document = JsonDocument.Parse(json);
return new(document, T.FromJson(document.RootElement));
}

/// <inheritdoc/>
public void Dispose()
{
if (this.jsonDocument is JsonDocument d)
{
d.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,88 @@ Examples:
| "Hello" | JsonAny | "Hello" |
| [123,"Hello"] | JsonArray | [123, "Hello"] |
| { "foo": 123, "bar": "Hello" } | JsonObject | { "foo": 123, "bar": "Hello" } |

Scenario Outline: Parse a UTF8 span into a disposable value
When the utf8 span '<utf8Value>' is parsed with ParsedValue{T} into a <jsonTypeName>
Then the result should be equal to the JsonAny <result>

Examples:
| utf8Value | jsonTypeName | result |
| true | JsonBoolean | true |
| false | JsonBoolean | false |
| null | JsonNull | null |
| 123 | JsonNumber | 123 |
| 123.4 | JsonNumber | 123.4 |
| 123 | JsonInteger | 123 |
| "Hello" | JsonString | "Hello" |
| "Hello" | JsonAny | "Hello" |
| [123,"Hello"] | JsonArray | [123, "Hello"] |
| { "foo": 123, "bar": "Hello" } | JsonObject | { "foo": 123, "bar": "Hello" } |

Scenario Outline: Parse a char span into a disposable value
When the char span '<charValue>' is parsed with ParsedValue{T} into a <jsonTypeName>
Then the result should be equal to the JsonAny <result>

Examples:
| charValue | jsonTypeName | result |
| true | JsonBoolean | true |
| false | JsonBoolean | false |
| null | JsonNull | null |
| 123 | JsonNumber | 123 |
| 123.4 | JsonNumber | 123.4 |
| 123 | JsonInteger | 123 |
| "Hello" | JsonString | "Hello" |
| "Hello" | JsonAny | "Hello" |
| [123,"Hello"] | JsonArray | [123, "Hello"] |
| { "foo": 123, "bar": "Hello" } | JsonObject | { "foo": 123, "bar": "Hello" } |

Scenario Outline: Parse a UTF8 ReadOnlyMemory into a disposable value
When the utf8 ReadOnlyMemory '<utf8Value>' is parsed with ParsedValue{T} into a <jsonTypeName>
Then the result should be equal to the JsonAny <result>

Examples:
| utf8Value | jsonTypeName | result |
| true | JsonBoolean | true |
| false | JsonBoolean | false |
| null | JsonNull | null |
| 123 | JsonNumber | 123 |
| 123.4 | JsonNumber | 123.4 |
| 123 | JsonInteger | 123 |
| "Hello" | JsonString | "Hello" |
| "Hello" | JsonAny | "Hello" |
| [123,"Hello"] | JsonArray | [123, "Hello"] |
| { "foo": 123, "bar": "Hello" } | JsonObject | { "foo": 123, "bar": "Hello" } |

Scenario Outline: Parse a char ReadOnlyMemory into a disposable value
When the char ReadOnlyMemory '<charValue>' is parsed with ParsedValue{T} into a <jsonTypeName>
Then the result should be equal to the JsonAny <result>

Examples:
| charValue | jsonTypeName | result |
| true | JsonBoolean | true |
| false | JsonBoolean | false |
| null | JsonNull | null |
| 123 | JsonNumber | 123 |
| 123.4 | JsonNumber | 123.4 |
| 123 | JsonInteger | 123 |
| "Hello" | JsonString | "Hello" |
| "Hello" | JsonAny | "Hello" |
| [123,"Hello"] | JsonArray | [123, "Hello"] |
| { "foo": 123, "bar": "Hello" } | JsonObject | { "foo": 123, "bar": "Hello" } |

Scenario Outline: Parse a UTF8 stream into a disposable value
When the utf8 Stream '<utf8Value>' is parsed with ParsedValue{T} into a <jsonTypeName>
Then the result should be equal to the JsonAny <result>

Examples:
| utf8Value | jsonTypeName | result |
| true | JsonBoolean | true |
| false | JsonBoolean | false |
| null | JsonNull | null |
| 123 | JsonNumber | 123 |
| 123.4 | JsonNumber | 123.4 |
| 123 | JsonInteger | 123 |
| "Hello" | JsonString | "Hello" |
| "Hello" | JsonAny | "Hello" |
| [123,"Hello"] | JsonArray | [123, "Hello"] |
| { "foo": 123, "bar": "Hello" } | JsonObject | { "foo": 123, "bar": "Hello" } |
2 changes: 1 addition & 1 deletion Solutions/Corvus.Json.Specs/Steps/JsonValueSteps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public void ThenTheRound_TrippedResultShouldBeNull()
/* serialization */

/// <summary>
/// Serializes an <see cref="IJsonValue"/> from the context variable <see cref="SubjectUnderTest"/>, deserializaes and stores the resulting <see cref="JsonAny"/> in the context variable <see cref="SerializationResult"/>.
/// Serializes an <see cref="IJsonValue"/> from the context variable <see cref="SubjectUnderTest"/>, deserializes and stores the resulting <see cref="JsonAny"/> in the context variable <see cref="SerializationResult"/>.
/// </summary>
[When("the json value is round-tripped via a string")]
public void WhenTheJsonValueIsRound_TrippedViaAString()
Expand Down
115 changes: 107 additions & 8 deletions Solutions/Corvus.Json.Specs/Steps/ParseValueStepDefinitions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ public void WhenTheUtfSpanIsParsed(string span, string typeName)
this.scenarioContext.Set(jsonValue, ResultKey);
}

[Then("the result should be equal to the JsonAny (.*)")]
public void ThenTheResultShouldBeEqualToTheJsonAnyTrue(string value)
{
JsonAny result = this.scenarioContext.Get<IJsonValue>(ResultKey).AsAny;
var expected = JsonAny.Parse(value); // Use Parse here to avoid like-for-like.
Assert.AreEqual(expected, result);
}

[When("the char span '([^']*)' is parsed into a (.*)")]
public void WhenTheCharSpanIsParsed(string span, string typeName)
{
Expand All @@ -70,4 +62,111 @@ public void WhenTheCharSpanIsParsed(string span, string typeName)

this.scenarioContext.Set(jsonValue, ResultKey);
}

[When("the utf8 span '([^']*)' is parsed with ParsedValue{T} into a (.*)")]
public void WhenTheUtfSpanIsParsedWithParsedValueOfT(string span, string typeName)
{
byte[] utf8bytes = Encoding.UTF8.GetBytes(span);
IJsonValue result = typeName switch
{
"JsonBoolean" => ParsedValue<JsonBoolean>.Parse(utf8bytes).Instance,
"JsonNumber" => ParsedValue<JsonNumber>.Parse(utf8bytes).Instance,
"JsonInteger" => ParsedValue<JsonNumber>.Parse(utf8bytes).Instance,
"JsonNull" => ParsedValue<JsonNull>.Parse(utf8bytes).Instance,
"JsonString" => ParsedValue<JsonString>.Parse(utf8bytes).Instance,
"JsonArray" => ParsedValue<JsonArray>.Parse(utf8bytes).Instance,
"JsonObject" => ParsedValue<JsonArray>.Parse(utf8bytes).Instance,
"JsonAny" => ParsedValue<JsonAny>.Parse(utf8bytes).Instance,
_ => throw new InvalidOperationException($"Unsupported type name: {typeName}"),
};

this.scenarioContext.Set(result, ResultKey);
}

[When("the utf8 ReadOnlyMemory '([^']*)' is parsed with ParsedValue{T} into a (.*)")]
public void WhenTheUtfReadOnlyMemoryIsParsedWithParsedValueOfT(string span, string typeName)
{
byte[] utf8bytes = Encoding.UTF8.GetBytes(span);
IJsonValue result = typeName switch
{
"JsonBoolean" => ParsedValue<JsonBoolean>.Parse(utf8bytes.AsMemory()).Instance,
"JsonNumber" => ParsedValue<JsonNumber>.Parse(utf8bytes.AsMemory()).Instance,
"JsonInteger" => ParsedValue<JsonNumber>.Parse(utf8bytes.AsMemory()).Instance,
"JsonNull" => ParsedValue<JsonNull>.Parse(utf8bytes.AsMemory()).Instance,
"JsonString" => ParsedValue<JsonString>.Parse(utf8bytes.AsMemory()).Instance,
"JsonArray" => ParsedValue<JsonArray>.Parse(utf8bytes.AsMemory()).Instance,
"JsonObject" => ParsedValue<JsonArray>.Parse(utf8bytes.AsMemory()).Instance,
"JsonAny" => ParsedValue<JsonAny>.Parse(utf8bytes.AsMemory()).Instance,
_ => throw new InvalidOperationException($"Unsupported type name: {typeName}"),
};

this.scenarioContext.Set(result, ResultKey);
}

[When("the utf8 Stream '([^']*)' is parsed with ParsedValue{T} into a (.*)")]
public void WhenTheUtfStreamIsParsedWithParsedValueOfT(string span, string typeName)
{
byte[] utf8bytes = Encoding.UTF8.GetBytes(span);
using MemoryStream stream = new(utf8bytes);
IJsonValue result = typeName switch
{
"JsonBoolean" => ParsedValue<JsonBoolean>.Parse(stream).Instance,
"JsonNumber" => ParsedValue<JsonNumber>.Parse(stream).Instance,
"JsonInteger" => ParsedValue<JsonNumber>.Parse(stream).Instance,
"JsonNull" => ParsedValue<JsonNull>.Parse(stream).Instance,
"JsonString" => ParsedValue<JsonString>.Parse(stream).Instance,
"JsonArray" => ParsedValue<JsonArray>.Parse(stream).Instance,
"JsonObject" => ParsedValue<JsonArray>.Parse(stream).Instance,
"JsonAny" => ParsedValue<JsonAny>.Parse(stream).Instance,
_ => throw new InvalidOperationException($"Unsupported type name: {typeName}"),
};

this.scenarioContext.Set(result, ResultKey);
}

[When("the char span '([^']*)' is parsed with ParsedValue{T} into a (.*)")]
public void WhenTheCharSpanIsParsedParsedValueOfT(string span, string typeName)
{
IJsonValue jsonValue = typeName switch
{
"JsonBoolean" => ParsedValue<JsonBoolean>.Parse(span).Instance,
"JsonNumber" => ParsedValue<JsonNumber>.Parse(span).Instance,
"JsonInteger" => ParsedValue<JsonNumber>.Parse(span).Instance,
"JsonNull" => ParsedValue<JsonNull>.Parse(span).Instance,
"JsonString" => ParsedValue<JsonString>.Parse(span).Instance,
"JsonArray" => ParsedValue<JsonArray>.Parse(span).Instance,
"JsonObject" => ParsedValue<JsonArray>.Parse(span).Instance,
"JsonAny" => ParsedValue<JsonAny>.Parse(span).Instance,
_ => throw new InvalidOperationException($"Unsupported type name: {typeName}"),
};

this.scenarioContext.Set(jsonValue, ResultKey);
}

[When("the char ReadOnlyMemory '([^']*)' is parsed with ParsedValue{T} into a (.*)")]
public void WhenTheCharReadOnlyMemoryIsParsedParsedValueOfT(string span, string typeName)
{
IJsonValue jsonValue = typeName switch
{
"JsonBoolean" => ParsedValue<JsonBoolean>.Parse(span.AsMemory()).Instance,
"JsonNumber" => ParsedValue<JsonNumber>.Parse(span.AsMemory()).Instance,
"JsonInteger" => ParsedValue<JsonNumber>.Parse(span.AsMemory()).Instance,
"JsonNull" => ParsedValue<JsonNull>.Parse(span.AsMemory()).Instance,
"JsonString" => ParsedValue<JsonString>.Parse(span.AsMemory()).Instance,
"JsonArray" => ParsedValue<JsonArray>.Parse(span.AsMemory()).Instance,
"JsonObject" => ParsedValue<JsonArray>.Parse(span.AsMemory()).Instance,
"JsonAny" => ParsedValue<JsonAny>.Parse(span.AsMemory()).Instance,
_ => throw new InvalidOperationException($"Unsupported type name: {typeName}"),
};

this.scenarioContext.Set(jsonValue, ResultKey);
}

[Then("the result should be equal to the JsonAny (.*)")]
public void ThenTheResultShouldBeEqualToTheJsonAnyTrue(string value)
{
JsonAny result = this.scenarioContext.Get<IJsonValue>(ResultKey).AsAny;
var expected = JsonAny.Parse(value); // Use Parse here to avoid like-for-like.
Assert.AreEqual(expected, result);
}
}
Loading