metadata.preview is now polymorphic
This commit is contained in:
parent
9473f3c1ac
commit
6da792fac1
@ -38,7 +38,6 @@ It's a json object with the following keys :
|
||||
"BPM": 120.0,
|
||||
"offset": 0.0,
|
||||
"preview": {},
|
||||
"preview path": "",
|
||||
}
|
||||
```
|
||||
Contains information that applies to the whole set of charts
|
||||
@ -60,11 +59,8 @@ Contains information that applies to the whole set of charts
|
||||
- number, required
|
||||
- In seconds, opposite of the time position of the first beat in the music file. For instance, if the first beat occurs at 0.15 seconds in the audio file, `offset̀` should be -0.15
|
||||
- **preview**
|
||||
- object, optional
|
||||
- If present, contains a {ref}`preview object <preview>`
|
||||
- **preview path**
|
||||
- string, optional
|
||||
- Path to a preview file to be played on loop at the music select screen. Alternative to the music sample described by `preview`.
|
||||
- object or string, optional
|
||||
- Contains either a {ref}`preview object <preview>` or a path to a bms-style preview file
|
||||
|
||||
(preview)=
|
||||
## Preview
|
||||
|
38
schema.json
38
schema.json
@ -39,24 +39,28 @@
|
||||
},
|
||||
"preview": {
|
||||
"description": "Describes the part of the music file that's to be played on loop when previewing this song",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"description": "In seconds, Time at which preview should start",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
"oneOf": [{
|
||||
"description": "Preview defined as a sample from the music file",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"position": {
|
||||
"description": "In seconds, Time at which preview should start",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"length": {
|
||||
"description": "In seconds, for how long should the preview be played past the starting point",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
}
|
||||
},
|
||||
"required": ["position", "length"]
|
||||
},
|
||||
"length": {
|
||||
"description": "In seconds, for how long should the preview be played past the starting point",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
{
|
||||
"description": "Preview defined as a separate audio file",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["position", "length"]
|
||||
},
|
||||
"preview path": {
|
||||
"description": "Path to the music preview, relative to the memon file",
|
||||
"type": "string"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["song title", "artist", "BPM", "offset"]
|
||||
@ -92,7 +96,7 @@
|
||||
},
|
||||
"t": {
|
||||
"description": "Timing, measured in \"ticks\" as specified with the resolution",
|
||||
"type":"integer",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"l": {
|
||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
67
tests/conftest.py
Normal file
67
tests/conftest.py
Normal file
@ -0,0 +1,67 @@
|
||||
|
||||
import json
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import py.path
|
||||
import pytest
|
||||
from jschon import create_catalog, JSON, JSONSchema, Catalog
|
||||
|
||||
from .utils import raise_if_invalid, result_of
|
||||
|
||||
|
||||
def pytest_collect_file(path: py.path.local, parent: pytest.Collector):
|
||||
fspath = Path(path)
|
||||
if fspath.name == "__init__.py" and fspath.parent.name == "data":
|
||||
return ExamplesFolder.from_parent(parent, name="data", fspath=path.dirpath())
|
||||
|
||||
|
||||
class ExamplesFolder(pytest.Collector):
|
||||
def collect(self):
|
||||
path = Path(self.fspath)
|
||||
for thing in path.glob("*-*"):
|
||||
if thing.is_dir():
|
||||
yield ExampleSubfolder.from_parent(
|
||||
self,
|
||||
name=thing.name,
|
||||
fspath=py.path.local(thing),
|
||||
)
|
||||
|
||||
|
||||
class ExampleSubfolder(pytest.Collector):
|
||||
def collect(self):
|
||||
path = Path(self.fspath)
|
||||
for file in (path / "pass").glob("*.json"):
|
||||
yield ValidExample.from_parent(self, name=file.stem, path=file)
|
||||
|
||||
for file in (path / "fail").glob("*.json"):
|
||||
yield InvalidExample.from_parent(self, name=file.stem, path=file)
|
||||
|
||||
|
||||
class ItemWithPath(pytest.Item):
|
||||
def __init__(self, *a, path: Path, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
self.path = path
|
||||
|
||||
def reportinfo(self):
|
||||
return self.path, 0, ""
|
||||
|
||||
|
||||
class ValidExample(ItemWithPath):
|
||||
def runtest(self):
|
||||
raise_if_invalid(result_of(self.path))
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
parent = self.getparent(ExampleSubfolder)
|
||||
return f"Example {parent.name}::{self.name} did not pass validation"
|
||||
|
||||
|
||||
class InvalidExample(ItemWithPath):
|
||||
def runtest(self):
|
||||
with pytest.raises(RuntimeError):
|
||||
raise_if_invalid(result_of(self.path))
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
parent = self.getparent(ExampleSubfolder)
|
||||
return f"Example {parent.name}::{self.name} passed validation but shouldn't"
|
10
tests/data/01-basic/pass/basic.json
Normal file
10
tests/data/01-basic/pass/basic.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0
|
||||
},
|
||||
"data": {}
|
||||
}
|
11
tests/data/02-polymorphic-preview/fail/array-is-invalid.json
Normal file
11
tests/data/02-polymorphic-preview/fail/array-is-invalid.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0,
|
||||
"preview": []
|
||||
},
|
||||
"data": {}
|
||||
}
|
14
tests/data/02-polymorphic-preview/fail/invalid-object.json
Normal file
14
tests/data/02-polymorphic-preview/fail/invalid-object.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0,
|
||||
"preview": {
|
||||
"position": 0,
|
||||
"duration": 0
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
11
tests/data/02-polymorphic-preview/fail/null-is-invalid.json
Normal file
11
tests/data/02-polymorphic-preview/fail/null-is-invalid.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0,
|
||||
"preview": null
|
||||
},
|
||||
"data": {}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0,
|
||||
"preview": 3
|
||||
},
|
||||
"data": {}
|
||||
}
|
14
tests/data/02-polymorphic-preview/pass/valid-object.json
Normal file
14
tests/data/02-polymorphic-preview/pass/valid-object.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0,
|
||||
"preview": {
|
||||
"position": 0,
|
||||
"length": 0
|
||||
}
|
||||
},
|
||||
"data": {}
|
||||
}
|
11
tests/data/02-polymorphic-preview/pass/valid-string.json
Normal file
11
tests/data/02-polymorphic-preview/pass/valid-string.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"metadata": {
|
||||
"song title": "",
|
||||
"artist": "",
|
||||
"BPM": 1,
|
||||
"offset": 0,
|
||||
"preview": "blablabla"
|
||||
},
|
||||
"data": {}
|
||||
}
|
2
tests/data/__init__.py
Normal file
2
tests/data/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""This file is here so the test code can use importlib as a portable way to
|
||||
open test data in this folder"""
|
@ -1,10 +1,7 @@
|
||||
import json
|
||||
from jschon import create_catalog, JSONSchema
|
||||
from jschon import JSONSchema
|
||||
|
||||
from .utils import raise_if_invalid, SCHEMA
|
||||
|
||||
def test_that_the_schema_follows_the_metaschema():
|
||||
catalog_2019_09 = create_catalog("2019-09", default=True)
|
||||
with open("schema.json") as file:
|
||||
raw = json.load(file)
|
||||
schema = JSONSchema(raw, catalog=catalog_2019_09)
|
||||
schema.validate()
|
||||
|
||||
schema_validity = SCHEMA.validate()
|
||||
raise_if_invalid(schema_validity)
|
||||
|
45
tests/utils.py
Normal file
45
tests/utils.py
Normal file
@ -0,0 +1,45 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Dict, Tuple, List
|
||||
|
||||
from jschon.jsonschema import Scope
|
||||
from jschon import create_catalog, JSONSchema, JSON
|
||||
import pytest
|
||||
|
||||
_catalog = create_catalog('2019-09')
|
||||
SCHEMA = JSONSchema.loadf("schema.json", catalog=_catalog)
|
||||
|
||||
def prettify_path(json_path: str) -> str:
|
||||
"""turns a JSONPath /that/looks/like/this into a.dotted.path.like.this"""
|
||||
split = json_path.split("/")
|
||||
return ".".join(split[1:])
|
||||
|
||||
def flatten_errors(scope: Scope) -> Dict[str, List[str]]:
|
||||
"""Returns a (path -> errors) mapping of the leaf nodes
|
||||
of the detailed error output"""
|
||||
def iter_errors(d: dict) -> Iterator[Tuple[str, str]]:
|
||||
if 'error' in d:
|
||||
yield prettify_path(d['instanceLocation']), d['error']
|
||||
elif 'errors' in d:
|
||||
for error in d['errors']:
|
||||
yield from iter_errors(error)
|
||||
|
||||
errors = defaultdict(list)
|
||||
for path, message in iter_errors(scope.output('detailed')):
|
||||
errors[path].append(message)
|
||||
|
||||
return {
|
||||
key: value[0] if len(value) == 1 else value
|
||||
for key, value in errors.items()
|
||||
}
|
||||
|
||||
def raise_if_invalid(scope: Scope):
|
||||
__tracebackhide__ = True
|
||||
if not scope.valid:
|
||||
raise RuntimeError(flatten_errors(scope))
|
||||
|
||||
def result_of(path: Path) -> Scope:
|
||||
raw = json.loads(path.read_text())
|
||||
instance = JSON(raw)
|
||||
return SCHEMA.evaluate(instance)
|
Loading…
Reference in New Issue
Block a user