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

added a keyword impl for unrecognized keywords instead of otherdata #270

Merged
merged 6 commits into from
May 26, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
60 changes: 30 additions & 30 deletions .github/workflows/dotnet-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,36 @@ on:
workflow_dispatch:

jobs:
check-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup .NET Core
uses: actions/setup-dotnet@v2
with:
# See https://stackoverflow.com/a/71619953/878701
dotnet-version: 6.0.100
- id: dotnet-format
run: |
dotnet format --verify-no-changes --report bin || true
FORMAT_REPORT=$(cat bin/format-report.json)
if [ "${FORMAT_REPORT}" != "[]" ]; then
echo '<details><summary>Expand to see formatting issues</summary>' >> pr-message.md
echo >> pr-message.md
echo '```json' >> pr-message.md
cat bin/format-report.json >> pr-message.md
echo >> pr-message.md
echo '```' >> pr-message.md
echo >> pr-message.md
echo '</details>' >> pr-message.md
echo "::set-output name=POST_REPORT::true"
fi
- if: ${{ steps.dotnet-format.outputs.POST_REPORT == 'true' }}
uses: machine-learning-apps/pr-comment@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
path: pr-message.md
# check-format:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v3
# - name: Setup .NET Core
# uses: actions/setup-dotnet@v2
# with:
# # See https://stackoverflow.com/a/71619953/878701
# dotnet-version: 6.0.100
# - id: dotnet-format
# run: |
# dotnet format --verify-no-changes --report bin || true
# FORMAT_REPORT=$(cat bin/format-report.json)
# if [ "${FORMAT_REPORT}" != "[]" ]; then
# echo '<details><summary>Expand to see formatting issues</summary>' >> pr-message.md
# echo >> pr-message.md
# echo '```json' >> pr-message.md
# cat bin/format-report.json >> pr-message.md
# echo >> pr-message.md
# echo '```' >> pr-message.md
# echo >> pr-message.md
# echo '</details>' >> pr-message.md
# echo "::set-output name=POST_REPORT::true"
# fi
# - if: ${{ steps.dotnet-format.outputs.POST_REPORT == 'true' }}
# uses: machine-learning-apps/pr-comment@master
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# with:
# path: pr-message.md
build:
runs-on: ubuntu-latest
steps:
Expand Down
46 changes: 46 additions & 0 deletions JsonSchema.Tests/UnrecognizedKeywordTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Linq;
using System.Text.Json;
using Json.More;
using NUnit.Framework;

namespace Json.Schema.Tests;

public class UnrecognizedKeywordTests
{
[Test]
public void FooIsNotAKeyword()
{
var schemaText = "{\"foo\": \"bar\"}";

var schema = JsonSerializer.Deserialize<JsonSchema>(schemaText);

Assert.AreEqual(1, schema!.Keywords!.Count);
Assert.IsInstanceOf<UnrecognizedKeyword>(schema.Keywords.First());
}

[Test]
public void FooProducesAnAnnotation()
{
var schemaText = "{\"foo\": \"bar\"}";

var schema = JsonSerializer.Deserialize<JsonSchema>(schemaText);

var result = schema!.Validate("{}", new ValidationOptions { OutputFormat = OutputFormat.Detailed });

Assert.IsTrue(result.IsValid);
Assert.AreEqual(1, result.Annotations.Count());
Assert.IsTrue("bar".AsJsonElement().IsEquivalentTo((JsonElement)result.Annotations.First().Value));
}

[Test]
public void FooIsIncludedInSerialization()
{
var schemaText = "{\"foo\":\"bar\"}";

var schema = JsonSerializer.Deserialize<JsonSchema>(schemaText);

var reText = JsonSerializer.Serialize(schema);

Assert.AreEqual(schemaText, reText);
}
}
46 changes: 20 additions & 26 deletions JsonSchema/JsonSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using Json.More;
using Json.Pointer;

#pragma warning disable CS0618

namespace Json.Schema;

/// <summary>
Expand All @@ -21,23 +23,28 @@ public class JsonSchema : IRefResolvable, IEquatable<JsonSchema>
/// <summary>
/// The empty schema <code>{}</code>. Functionally equivalent to <see cref="True"/>.
/// </summary>
public static readonly JsonSchema Empty = new JsonSchema(Enumerable.Empty<IJsonSchemaKeyword>(), null);
public static readonly JsonSchema Empty = new(Enumerable.Empty<IJsonSchemaKeyword>(), null);
/// <summary>
/// The <code>true</code> schema. Passes all instances.
/// </summary>
public static readonly JsonSchema True = new JsonSchema(true);
public static readonly JsonSchema True = new(true);
/// <summary>
/// The <code>false</code> schema. Fails all instances.
/// </summary>
public static readonly JsonSchema False = new JsonSchema(false);
public static readonly JsonSchema False = new(false);

/// <summary>
/// Gets the keywords contained in the schema. Only populated for non-boolean schemas.
/// </summary>
public IReadOnlyCollection<IJsonSchemaKeyword>? Keywords { get; }
/// <summary>
/// Gets other non-keyword (or unknown keyword) properties in the schema.
/// (obsolete) Gets other non-keyword (or unknown keyword) properties in the schema.
/// </summary>
/// <remarks>
/// This property is now obsolete and no longer used. It will be removed at the next major version.
/// Until then, it will remain populated.
/// </remarks>
[Obsolete("Unrecognized keyword data now appears as UnrecognizedKeyword instances in the Keywords collection.")]
public IReadOnlyDictionary<string, JsonElement>? OtherData { get; }

/// <summary>
Expand Down Expand Up @@ -293,22 +300,15 @@ public void ValidateSubschema(ValidationContext context)
break;
}

if (newResolvable == null)
if (newResolvable is UnrecognizedKeyword unrecognized)
{
// TODO: document that this process does not consider `$id` in extraneous data
if (resolvable is JsonSchema { OtherData: { } } subSchema &&
subSchema.OtherData.TryGetValue(segment.Value, out var element))
{
var newPointer = JsonPointer.Create(pointer.Segments.Skip(i + 1), true);
var value = newPointer.Evaluate(element);
var asSchema = FromText(value.ToString());
return (asSchema, currentUri);
}

return (null, currentUri);
var newPointer = JsonPointer.Create(pointer.Segments.Skip(i + 1), true);
var value = newPointer.Evaluate(unrecognized.Value);
var asSchema = FromText(value.ToString());
return (asSchema, currentUri);
}

resolvable = newResolvable;
resolvable = newResolvable!;
}

return (resolvable as JsonSchema, currentUri);
Expand Down Expand Up @@ -431,6 +431,9 @@ public override JsonSchema Read(ref Utf8JsonReader reader, Type typeToConvert, J
using var document = JsonDocument.ParseValue(ref reader);
var element = document.RootElement;
otherData[keyword] = element.Clone();

var unrecognizedKeyword = new UnrecognizedKeyword(keyword, element);
keywords.Add(unrecognizedKeyword);
break;
}

Expand Down Expand Up @@ -473,15 +476,6 @@ public override void Write(Utf8JsonWriter writer, JsonSchema value, JsonSerializ
JsonSerializer.Serialize(writer, keyword, keyword.GetType(), options);
}

if (value.OtherData != null)
{
foreach (var data in value.OtherData)
{
writer.WritePropertyName(data.Key);
JsonSerializer.Serialize(writer, data.Value, options);
}
}

writer.WriteEndObject();
}
}
Expand Down
4 changes: 2 additions & 2 deletions JsonSchema/JsonSchema.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<PackageProjectUrl>https://github.com/gregsdennis/json-everything</PackageProjectUrl>
<RepositoryUrl>https://github.com/gregsdennis/json-everything</RepositoryUrl>
<PackageTags>json-schema validation schema json</PackageTags>
<Version>2.3.0</Version>
<FileVersion>2.3.0.0</FileVersion>
<Version>2.4.0</Version>
<FileVersion>2.4.0.0</FileVersion>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<AssemblyName>JsonSchema.Net</AssemblyName>
Expand Down
26 changes: 26 additions & 0 deletions JsonSchema/JsonSchemaBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,32 @@ public static JsonSchemaBuilder UniqueItems(this JsonSchemaBuilder builder, bool
return builder;
}

/// <summary>
/// Adds a keyword that's not recognized by any vocabulary - extra data - to the schema.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="name">The keyword name.</param>
/// <param name="value">The value.</param>
/// <returns>The builder.</returns>
public static JsonSchemaBuilder Unrecognized(this JsonSchemaBuilder builder, string name, JsonElement value)
{
builder.Add(new UnrecognizedKeyword(name, value));
return builder;
}

/// <summary>
/// Adds a keyword that's not recognized by any vocabulary - extra data - to the schema.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="name">The keyword name.</param>
/// <param name="value">The value.</param>
/// <returns>The builder.</returns>
public static JsonSchemaBuilder Unrecognized(this JsonSchemaBuilder builder, string name, JsonElementProxy value)
{
builder.Add(new UnrecognizedKeyword(name, value));
return builder;
}

/// <summary>
/// Add an `$vocabulary` keyword.
/// </summary>
Expand Down
5 changes: 4 additions & 1 deletion JsonSchema/KeywordExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public static class KeywordExtensions
.GetTypes()
.Where(t => typeof(IJsonSchemaKeyword).IsAssignableFrom(t) &&
!t.IsAbstract &&
!t.IsInterface)
!t.IsInterface &&
t != typeof(UnrecognizedKeyword))
.ToDictionary(t => t, t => t.GetCustomAttribute<SchemaKeywordAttribute>().Name);

/// <summary>
Expand All @@ -29,6 +30,8 @@ public static string Keyword(this IJsonSchemaKeyword keyword)
{
if (keyword == null) throw new ArgumentNullException(nameof(keyword));

if (keyword is UnrecognizedKeyword unrecognized) return unrecognized.Name;

var keywordType = keyword.GetType();
if (!_attributes.TryGetValue(keywordType, out var name))
{
Expand Down
92 changes: 92 additions & 0 deletions JsonSchema/UnrecognizedKeyword.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Json.More;

namespace Json.Schema;

/// <summary>
/// Handles unrecognized keywords.
/// </summary>
[SchemaDraft(Draft.Draft6)]
[SchemaDraft(Draft.Draft7)]
[SchemaDraft(Draft.Draft201909)]
[SchemaDraft(Draft.Draft202012)]
[JsonConverter(typeof(UnrecognizedKeywordJsonConverter))]
public class UnrecognizedKeyword : IJsonSchemaKeyword, IEquatable<UnrecognizedKeyword>
{
/// <summary>
/// The name or key of the keyword.
/// </summary>
public string Name { get; }

/// <summary>
/// The value of the keyword.
/// </summary>
public JsonElement Value { get; }

/// <summary>
/// Creates a new <see cref="UnrecognizedKeyword"/>.
/// </summary>
/// <param name="name">The name of the keyword.</param>
/// <param name="value">Whether items should be unique.</param>
public UnrecognizedKeyword(string name, JsonElement value)
{
Name = name;
Value = value.Clone();
}

/// <summary>
/// Provides validation for the keyword.
/// </summary>
/// <param name="context">Contextual details for the validation process.</param>
public void Validate(ValidationContext context)
{
context.EnterKeyword(Name);
context.LocalResult.SetAnnotation(Name, Value);
context.LocalResult.Pass();
context.ExitKeyword(Name, context.LocalResult.IsValid);
}

/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
/// <param name="other">An object to compare with this object.</param>
/// <returns>true if the current object is equal to the <paramref name="other">other</paramref> parameter; otherwise, false.</returns>
public bool Equals(UnrecognizedKeyword? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Name == other.Name && Value.IsEquivalentTo(other.Value);
}

/// <summary>Determines whether the specified object is equal to the current object.</summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns>true if the specified object is equal to the current object; otherwise, false.</returns>
public override bool Equals(object obj)
{
return Equals(obj as UnrecognizedKeyword);
}

/// <summary>Serves as the default hash function.</summary>
/// <returns>A hash code for the current object.</returns>
public override int GetHashCode()
{
unchecked
{
return (Name.GetHashCode() * 397) ^ Value.GetHashCode();
}
}
}

internal class UnrecognizedKeywordJsonConverter : JsonConverter<UnrecognizedKeyword>
{
public override UnrecognizedKeyword Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw new NotImplementedException("Unrecognized keywords should be handled manually during JsonSchema deserialization.");
}

public override void Write(Utf8JsonWriter writer, UnrecognizedKeyword value, JsonSerializerOptions options)
{
writer.WritePropertyName(value.Name);
writer.WriteValue(value.Value);
}
}
4 changes: 4 additions & 0 deletions json-everything.net/wwwroot/md/release-notes/json-schema.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# [2.4.0](https://github.com/gregsdennis/json-everything/pull/270)

Added `UnrecognizedKeyword` to represent keywords that were not recognized by any known vocabulary. The values of these keywords are then captured in the validation results as annotations. As a result of this change `JsonSchema.OtherData` has been marked obsolete.

# [2.3.0](https://github.com/gregsdennis/json-everything/pull/249)

[#190](https://github.com/gregsdennis/json-everything/issues/190) - Added support for custom and localized error messages.
Expand Down