Skip to content

Commit 6ff3872

Browse files
committed
Add schema bundling tools and multi-dialect publish workflow
Create tooling to bundle JSON schema fragments (from PR #226) into single-file schemas and convert to draft-07, 2019-09, and 2020-12 dialects. The 2020-12 output enables OpenAPI 3.1 compatibility. New files: - standard/schema/root.json: fragment entry point - tools/: bundle_schema, convert_dialect, patch_schema - requirements.txt - .github/workflows/publish-schema.yml: CI pipeline publishing all three dialect versions to gh-pages
1 parent bcfe98f commit 6ff3872

7 files changed

Lines changed: 479 additions & 0 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Publish schema
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- 'standard/schema/**'
9+
- 'tools/**'
10+
11+
jobs:
12+
bundle-and-publish:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v5
20+
with:
21+
python-version: '3.x'
22+
23+
- name: Install dependencies
24+
run: pip install -r requirements.txt
25+
26+
- name: Bundle schema fragments
27+
run: python -m tools.bundle_schema --schema-dir standard/schema --root /schemas/coveragejson --out dist/bundle.json
28+
29+
- name: Convert to draft-07
30+
run: |
31+
python -m tools.convert_dialect dist/bundle.json --dialect draft-07 --out dist/draft-07/coveragejson.json
32+
python -m tools.patch_schema dist/draft-07/coveragejson.json --set-id "https://schemas.opengis.net/covjson/1.0/coveragejson.json"
33+
34+
- name: Convert to 2019-09
35+
run: |
36+
python -m tools.convert_dialect dist/bundle.json --dialect 2019-09 --out dist/2019-09/coveragejson.json
37+
python -m tools.patch_schema dist/2019-09/coveragejson.json --set-id "https://schemas.opengis.net/covjson/1.0/coveragejson.json"
38+
39+
- name: Convert to 2020-12
40+
run: |
41+
python -m tools.convert_dialect dist/bundle.json --dialect 2020-12 --out dist/2020-12/coveragejson.json
42+
python -m tools.patch_schema dist/2020-12/coveragejson.json --set-id "https://schemas.opengis.net/covjson/1.0/coveragejson.json"
43+
44+
- name: Remove intermediate bundle
45+
run: rm dist/bundle.json
46+
47+
- name: Publish to GitHub Pages
48+
uses: peaceiris/actions-gh-pages@v4
49+
with:
50+
github_token: ${{ secrets.GITHUB_TOKEN }}
51+
publish_dir: ./dist
52+
publish_branch: gh-pages

‎requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
jsonschema

‎standard/schema/root.json‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"$id": "/schemas/coveragejson",
3+
"description": "A CoverageJSON object",
4+
"type": "object",
5+
"properties": {
6+
"type": {
7+
"enum": [
8+
"Domain",
9+
"NdArray",
10+
"TiledNdArray",
11+
"Coverage",
12+
"CoverageCollection"
13+
]
14+
}
15+
},
16+
"required": [
17+
"type"
18+
],
19+
"allOf": [
20+
{
21+
"if": {
22+
"properties": {
23+
"type": {
24+
"const": "Domain"
25+
}
26+
}
27+
},
28+
"then": {
29+
"$ref": "/schemas/domain"
30+
}
31+
},
32+
{
33+
"if": {
34+
"properties": {
35+
"type": {
36+
"const": "NdArray"
37+
}
38+
}
39+
},
40+
"then": {
41+
"$ref": "/schemas/ndArray"
42+
}
43+
},
44+
{
45+
"if": {
46+
"properties": {
47+
"type": {
48+
"const": "TiledNdArray"
49+
}
50+
}
51+
},
52+
"then": {
53+
"$ref": "/schemas/tiledNdArray"
54+
}
55+
},
56+
{
57+
"if": {
58+
"properties": {
59+
"type": {
60+
"const": "Coverage"
61+
}
62+
}
63+
},
64+
"then": {
65+
"$ref": "/schemas/coverage"
66+
}
67+
},
68+
{
69+
"if": {
70+
"properties": {
71+
"type": {
72+
"const": "CoverageCollection"
73+
}
74+
}
75+
},
76+
"then": {
77+
"$ref": "/schemas/coverageCollection"
78+
}
79+
}
80+
]
81+
}

‎tools/__init__.py‎

Whitespace-only changes.

‎tools/bundle_schema.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Bundles all referenced schema fragments into a single root schema.
2+
# Adapted from https://github.com/covjson/covjson-validator/blob/main/tools/bundle_schema.py
3+
#
4+
# Follows the method described in:
5+
# https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00#section-9.3.1
6+
7+
import argparse
8+
import json
9+
import copy
10+
import os
11+
12+
13+
def walk_dict(obj, match_key, fn):
14+
"""Recursively walk a dict and call fn(obj, key, value) for every match_key found."""
15+
for key, value in list(obj.items()):
16+
if key == match_key:
17+
fn(obj, key, value)
18+
elif isinstance(value, dict):
19+
walk_dict(value, match_key, fn)
20+
elif isinstance(value, list):
21+
for item in value:
22+
if isinstance(item, dict):
23+
walk_dict(item, match_key, fn)
24+
25+
26+
def create_schema_store(schema_dir):
27+
"""Load all JSON schema fragments from a directory into a dict keyed by $id."""
28+
schema_store = {}
29+
for entry in os.scandir(schema_dir):
30+
if entry.is_file() and entry.path.endswith(".json"):
31+
with open(entry.path) as f:
32+
schema = json.load(f)
33+
if "$id" in schema:
34+
schema_store[schema["$id"]] = schema
35+
return schema_store
36+
37+
38+
def bundle_schema(schema_store, root_schema_id):
39+
"""Bundle all referenced schemas into the root schema using $defs."""
40+
root_schema = copy.deepcopy(schema_store[root_schema_id])
41+
42+
# Collect all referenced schema IDs recursively
43+
refs = set()
44+
45+
def record_ref(obj, key, value):
46+
if value in schema_store:
47+
refs.add(value)
48+
49+
done = set()
50+
todo = {root_schema_id}
51+
while todo:
52+
for schema_id in todo:
53+
walk_dict(schema_store[schema_id], "$ref", record_ref)
54+
done.add(schema_id)
55+
todo = refs - done
56+
57+
# Remove self-reference if present
58+
refs.discard(root_schema_id)
59+
60+
# Embed referenced schemas under $defs
61+
if refs:
62+
defs = root_schema.setdefault("$defs", {})
63+
for schema_id in sorted(refs):
64+
defs[schema_id] = copy.deepcopy(schema_store[schema_id])
65+
66+
return root_schema
67+
68+
69+
if __name__ == "__main__":
70+
parser = argparse.ArgumentParser(description="Bundle CoverageJSON schema fragments into a single file")
71+
parser.add_argument("--schema-dir", default="standard/schema",
72+
help="Directory containing schema fragment JSON files")
73+
parser.add_argument("--root", default="/schemas/coveragejson",
74+
help="$id of the root schema fragment")
75+
parser.add_argument("--out", default="dist/coveragejson.json",
76+
help="Output file path")
77+
args = parser.parse_args()
78+
79+
store = create_schema_store(args.schema_dir)
80+
schema = bundle_schema(store, args.root)
81+
82+
os.makedirs(os.path.dirname(args.out), exist_ok=True)
83+
with open(args.out, "w") as f:
84+
json.dump(schema, f, indent=2)
85+
f.write("\n")
86+
87+
print(f"Bundled schema written to {args.out}")

0 commit comments

Comments
 (0)