Skip to content

Add annotation tests from @hyperjump/json-schema #770

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
21 changes: 21 additions & 0 deletions .github/workflows/annotation-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Validate annotation tests

on:
pull_request:
paths:
- "annotations/**"

jobs:
annotate:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Deno
uses: denoland/setup-deno@v2
with:
deno-version: "2.x"

- name: Validate annotation tests
run: deno --node-modules-dir=auto --allow-read --no-prompt bin/annotation-tests.ts
104 changes: 104 additions & 0 deletions annotations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Annotation Tests

The annotations Test Suite tests which annotations should appear (or not appear)
on which values of an instance. These tests are agnostic of any output format.

## Supported Dialects

Although the concept of annotations didn't appear in the spec until 2019-09, the
concept is compatible with every version of JSON Schema. Test Cases in this Test
Suite are designed to be compatible with as many releases of JSON Schema as
possible. They do not include `$schema` or `$id`/`id` keywords so that
implementations can run the same Test Suite for each dialect they support.

Since this Test Suite can be used for a variety of dialects, there are a couple
of options that can be used by Test Runners to filter out Test Cases that don't
apply to the dialect under test.

## Test Case Components

### description

A short description of what behavior the Test Case is covering.

### compatibility

The `compatibility` option allows you to set which dialects the Test Case is
compatible with. Test Runners can use this value to filter out Test Cases that
don't apply the to dialect currently under test. Dialects are indicated by the
number corresponding to their release. Date-based releases use just the year.

If this option isn't present, it means the Test Case is compatible with any
dialect.

If this option is present with a number, the number indicates the minimum
release the Test Case is compatible with. This example indicates that the Test
Case is compatible with draft-07 and up.

**Example**: `"compatibility": "7"`

You can use a `<=` operator to indicate that the Test Case is compatible with
releases less then or equal to the given release. This example indicates that
the Test Case is compatible with 2019-09 and under.

**Example**: `"compatibility": "<=2019"`

You can use comma-separated values to indicate multiple constraints if needed.
This example indicates that the Test Case is compatible with releases between
draft-06 and 2019-09.

**Example**: `"compatibility": "6,<=2019"`

For convenience, you can use the `=` operator to indicate a Test Case is only
compatible with a single release. This example indicates that the Test Case is
compatible only with 2020-12.

**Example**: `"compatibility": "=2020"`

### schema

The schema that will serve as the subject for the tests. Whenever possible, this
schema shouldn't include `$schema` or `id`/`$id` because Test Cases should be
designed to work with as many releases as possible.

### externalSchemas

`externalSchemas` allows you to define additional schemas that `schema` makes
references to. The value is an object where the keys are retrieval URIs and
values are schemas. Most external schemas aren't self identifying (using
`id`/`$id`) and rely on the retrieval URI for identification. This is done to
increase the number of dialects that the test is compatible with. Because `id`
changed to `$id` in draft-06, if you use `$id`, the test becomes incompatible
with draft-03/4 and in most cases, that's not necessary.

### tests

A collection of Tests to run to verify the Test Case.

## Test Components

### instance

The JSON instance to be annotated.

### assertions

`assertions` are a collection of assertions that must be true for the test to pass.

## Assertions Components

### location

The instance location.

### keyword

The annotating keyword.

### expected

An array of annotations on the `keyword` - instance `location` pair. `expected`
is an array because there's always a chance that an annotation is applied
multiple times to any given instance location. The `expected` array should be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can dictate an order for this, because there is no keyword execution order defined in the specification.

For example, we don't say whether applicator keywords are evaluated first before validation, or vice versa. It is up to the individual implementation to decide whether to approach schemas in a breadth-first order (leaf nodes, e.g. validation keywords, first, then descend into subschemas via applicator keywords) or depth-first (applicators first).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant for this to be a convention to keep the tests consistent, not as a requirement. Test harnesses can match on these as an unordered set rather than an ordered array. However, there is an intuitive ordering that people expect when they read a schema and I'd like the tests to stay close to that expectation.

For example, here's a common example of a schema extension scenario where the user would expect their annotation to take precedence over the annotation from the extended schema.

{
  "$ref": "#/$defs/base",
  "properties": {
    "foo": { "title": "Extended Foo" }
  },
  "$defs": {
    "base": {
      "type": "object",
      "properties": {
        "foo": { "title": "Base Foo" }
      }
    }
  }
}

An implementation could choose to evaluate properties before $ref and the order would be ["Base Foo", "Extended Foo"] instead of the expected ["Extended Foo", "Base Foo"]. I'm just saying that I want the tests to stick to that expected order even though it's valid for it to be the other way around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test harnesses can match on these as an unordered set rather than an ordered array. ... I'm just saying that I want the tests to stick to that expected order

Both of these things should be stated explicitly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already done commit. Let me know if the new text is sufficient.

sorted such that the most recently encountered value for an annotation during
evaluation comes before previously encountered values.
21 changes: 21 additions & 0 deletions annotations/assertion.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",

"type": "object",
"properties": {
"location": {
"markdownDescription": "The instance location.",
"type": "string",
"format": "json-pointer"
},
"keyword": {
"markdownDescription": "The annotation keyword.",
"type": "string"
},
"expected": {
"markdownDescription": "An array of annotations on the `keyword` - instance `location` pair.",
"type": "array"
}
},
"required": ["location", "keyword", "expected"]
}
38 changes: 38 additions & 0 deletions annotations/test-case.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",

"type": "object",
"properties": {
"description": {
"markdownDescription": "A short description of what behavior the Test Case is covering.",
"type": "string"
},
"compatibility": {
"markdownDescription": "Set which dialects the Test Case is compatible with. Examples:\n- `\"7\"` -- draft-07 and above\n- `\"<=2019\"` -- 2019-09 and previous\n- `\"6,<=2019\"` -- Between draft-06 and 2019-09\n- `\"=2020\"` -- 2020-12 only",
"type": "string",
"pattern": "^(<=|=)?([123467]|2019|2020)(,(<=|=)?([123467]|2019|2020))*$"
},
"schema": {
"markdownDescription": "This schema shouldn't include `$schema` or `id`/`$id` unless necesary for the test because Test Cases should be designed to work with as many releases as possible.",
"type": ["boolean", "object"]
},
"externalSchemas": {
"markdownDescription": "The keys are retrieval URIs and values are schemas.",
"type": "object",
"patternProperties": {
"": {
"type": ["boolean", "object"]
}
},
"propertyNames": {
"format": "uri"
}
},
"tests": {
"markdownDescription": "A collection of Tests to run to verify the Test Case.",
"type": "array",
"items": { "$ref": "./test.schema.json" }
}
},
"required": ["description", "schema", "tests"]
}
15 changes: 15 additions & 0 deletions annotations/test-suite.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",

"type": "object",
"properties": {
"description": {
"type": "string"
},
"suite": {
"type": "array",
"items": { "$ref": "./test-case.schema.json" }
}
},
"required": ["description", "suite"]
}
16 changes: 16 additions & 0 deletions annotations/test.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",

"type": "object",
"properties": {
"instance": {
"markdownDescription": "The JSON instance to be annotated."
},
"assertions": {
"markdownDescription": "An array of annotations on the `keyword` - instance `location` pair.",
"type": "array",
"items": { "$ref": "./assertion.schema.json" }
}
},
"required": ["instance", "assertions"]
}
Loading