diff --git a/examples/full/valid/extension-schema-example.json b/examples/full/valid/extension-schema-example.json new file mode 100644 index 0000000..991a1b8 --- /dev/null +++ b/examples/full/valid/extension-schema-example.json @@ -0,0 +1,84 @@ +{ + "$schema": "../../../schema/core-schema.json", + "locations": [ + { + "guid": "82d39ac8-1a12-4801-835a-85d319927548", + "location-type": "factory", + "language": "en", + "products": [ + "Accessories", + "Hats" + ], + "processing-types": [ + "Final Product Assembly", + "Cutting", + "Embroidery", + "Finishing", + "Ironing", + "Knitting" + ], + "location-identifier": "osid:CN2021250D1DTN7", + "name": "Huai An Yuan Tong Headwear Mfg. Co., Ltd.", + "coordinates": { + "latitude": 33.7862099, + "longitude": 119.2787399 + }, + "address": { + "street-name": "Yan Huang Avenue", + "building-number": "No.30 & 32 & 99", + "town-name": "Huaian", + "country-sub-division": "Jiangsu", + "country": "CN", + "address-line": [ + "No.30 & 32 & 99 Yan Huang Avenue", + "Lian Shui Economic Developmental District", + "Huaian, Jiangsu - China" + ] + }, + "responsible-recruiting": { + "recruitment-fee": 200, + "employer-coverage": 150, + "initial-contact-date": "2024-01-15", + "contract-signing-date": "2024-02-01", + "first-paycheck-date": "2024-03-01" + } + } + ], + "organizations": [ + { + "guid": "91582fdd-6f72-47cf-a113-f68fe4e74865", + "organization-type": "retailer", + "language": "en", + "organizational-identifier": "lei:3538002LJMRZ83SU0B85" + }, + { + "guid": "91582fdd-6f72-47cf-a113-f68fe4e74866", + "organization-type": "retailer", + "language": "en", + "organizational-identifier": "lei:353800ZCXKHDPY0N5218" + }, + { + "guid": "91582fdd-6f72-47cf-a113-f68fe4e74867", + "organization-type": "retailer", + "language": "en", + "organizational-identifier": "lei:549300D9GZ4BMLDW5T40" + } + ], + "affiliations": [ + { + "from-guid": "82d39ac8-1a12-4801-835a-85d319927548", + "to-guid": "91582fdd-6f72-47cf-a113-f68fe4e74865", + "affiliation-type": "is supplier of" + }, + { + "from-guid": "82d39ac8-1a12-4801-835a-85d319927548", + "to-guid": "91582fdd-6f72-47cf-a113-f68fe4e74866", + "affiliation-type": "is supplier of" + }, + { + "from-guid": "82d39ac8-1a12-4801-835a-85d319927548", + "to-guid": "91582fdd-6f72-47cf-a113-f68fe4e74867", + "affiliation-type": "is supplier of" + } + ] +} \ No newline at end of file diff --git a/schema/components/affiliation.json b/schema/components/affiliation.json new file mode 100644 index 0000000..df16d43 --- /dev/null +++ b/schema/components/affiliation.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Affiliation Schema", + "type": "object", + "properties": { + "from-guid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the source entity" + }, + "to-guid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the target entity" + }, + "affiliation-type": { + "type": "string", + "description": "Type of relationship between the entities" + } + }, + "required": [ + "from-guid", + "to-guid", + "affiliation-type" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schema/components/location.json b/schema/components/location.json new file mode 100644 index 0000000..bd60ee6 --- /dev/null +++ b/schema/components/location.json @@ -0,0 +1,94 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Location Schema", + "type": "object", + "properties": { + "guid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the location data" + }, + "language": { + "$ref":"../definitions/language.json" + }, + "location-type": { + "type": "string", + "description": "Type of location (e.g., factory, warehouse, farm)" + }, + "location-identifier": { + "description": "Accepted location identifier, which can be a single string (e.g., 'osid:XXXXXXXX', 'gfid:XXXXXXXX') or an array of such strings (osid stands for open supply hub ID, and gfid for Global Field ID)", + "anyOf": [ + { + "$ref": "../definitions/location-identifier" + }, + { + "type": "array", + "items": { + "$ref": "../definitions/location-identifier" + }, + "minItems": 1, + "description": "An array of location identifier strings" + } + ] + }, + "coordinates": { + "type": "object", + "description": "Geographic coordinates with latitude and longitude in ISO 6709 format", + "properties": { + "latitude": { + "type": "number", + "description": "Latitude of the location, ranging from -90 to 90", + "minimum": -90, + "maximum": 90 + }, + "longitude": { + "type": "number", + "description": "Longitude of the location, ranging from -180 to 180", + "minimum": -180, + "maximum": 180 + } + }, + "required": [ + "latitude", + "longitude" + ] + }, + "name": { + "type": "string", + "description": "Name of the location, if available" + }, + "address": { + "$ref": "../definitions/address.json" + }, + "country": { + "type": "string", + "description": "Country code in ISO 3166 Alpha-2 or Alpha-3 format", + "pattern": "^[A-Z]{2,3}$" + }, + "processing-types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Processing activities conducted at the location" + }, + "products": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Products produced at the location" + } + }, + "allOf": [ + { + "$ref": "../extensions/location-extensions.json" + } + ], + "required": [ + "guid", + "location-type", + "location-identifier" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schema/components/organization.json b/schema/components/organization.json new file mode 100644 index 0000000..2a19038 --- /dev/null +++ b/schema/components/organization.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Organization Schema", + "type": "object", + "properties": { + "guid": { + "type": "string", + "format": "uuid", + "description": "Unique identifier for the organization" + }, + "language": { + "$ref": "../definitions/language.json" + }, + "organization-type": { + "type": "string", + "description": "Type of organization (e.g., supplier, retailer)" + }, + "name": { + "type": "string", + "description": "Name of the organization, if available" + }, + "address": { + "$ref":"../definitions/address.json" + }, + "organizational-identifier": { + "description": "Accepted organizational identifier (e.g., LEI, GLN, CRN)", + "anyOf": [ + { + "$ref": "../definitions/organization-identifier" + }, + { + "type": "array", + "items": { + "$ref": "../definitions/organization-identifier" + }, + "minItems": 1, + "description": "An array of organization identifier strings" + } + ] + } + }, + "required": [ + "guid", + "organization-type", + "organizational-identifier" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schema/core-schema.json b/schema/core-schema.json index 4d5e97c..69adc70 100644 --- a/schema/core-schema.json +++ b/schema/core-schema.json @@ -7,260 +7,24 @@ "locations": { "type": "array", "items": { - "type": "object", - "properties": { - "guid": { - "type": "string", - "format": "uuid", - "description": "Unique identifier for the location data" - }, - "language": { - "type": "string", - "description": "Language code in ISO 639-1 or ISO 639-2 format", - "pattern": "^[a-z]{2,3}$" - }, - "location-type": { - "type": "string", - "description": "Type of location (e.g., factory, warehouse, farm)" - }, - "location-identifier": { - "description": "Accepted location identifier, which can be a single string (e.g., 'osid:XXXXXXXX', 'gfid:XXXXXXXX') or an array of such strings (osid stands for open supply hub ID, and gfid for Global Field ID)", - "anyOf": [ - { - "$ref": "#/definitions/location-identifier" - }, - { - "type": "array", - "items": { - "$ref": "#/definitions/location-identifier" - }, - "minItems": 1, - "description": "An array of location identifier strings" - } - ] - }, - "coordinates": { - "type": "object", - "description": "Geographic coordinates with latitude and longitude in ISO 6709 format", - "properties": { - "latitude": { - "type": "number", - "description": "Latitude of the location, ranging from -90 to 90", - "minimum": -90, - "maximum": 90 - }, - "longitude": { - "type": "number", - "description": "Longitude of the location, ranging from -180 to 180", - "minimum": -180, - "maximum": 180 - } - }, - "required": [ - "latitude", - "longitude" - ] - }, - "name": { - "type": "string", - "description": "Name of the location, if available" - }, - "address": { - "type": "object", - "description": "Address of the location in ISO 20022 format", - "properties": { - "street-name": { - "type": "string", - "description": "Street name of the address" - }, - "building-number": { - "type": "string", - "description": "Building number of the address" - }, - "post-code": { - "type": "string", - "description": "Postal code of the address" - }, - "town-name": { - "type": "string", - "description": "Town or city name of the address" - }, - "country-sub-division": { - "type": "string", - "description": "Subdivision of the country (e.g., state, province)" - }, - "country": { - "type": "string", - "description": "Country in ISO 3166 Alpha-2 format", - "pattern": "^[A-Z]{2}$" - }, - "address-line": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Address lines for unstructured address information" - } - }, - "required": [ - "country" - ] - }, - "country": { - "type": "string", - "description": "Country code in ISO 3166 Alpha-2 or Alpha-3 format", - "pattern": "^[A-Z]{2,3}$" - }, - "processing-types": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Processing activities conducted at the location" - }, - "products": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Products produced at the location" - } - }, - "required": [ - "guid", - "location-type", - "location-identifier" - ] + "$ref": "components/location.json" } }, "organizations": { "type": "array", "items": { - "type": "object", - "properties": { - "guid": { - "type": "string", - "format": "uuid", - "description": "Unique identifier for the organization" - }, - "language": { - "type": "string", - "description": "Language code in ISO 639-1 or ISO 639-2 format", - "pattern": "^[a-z]{2,3}$" - }, - "organization-type": { - "type": "string", - "description": "Type of organization (e.g., supplier, retailer)" - }, - "name": { - "type": "string", - "description": "Name of the organization, if available" - }, - "address": { - "type": "object", - "description": "Address of the location in ISO 20022 format", - "properties": { - "street-name": { - "type": "string", - "description": "Street name of the address" - }, - "building-number": { - "type": "string", - "description": "Building number of the address" - }, - "post-code": { - "type": "string", - "description": "Postal code of the address" - }, - "town-name": { - "type": "string", - "description": "Town or city name of the address" - }, - "country-sub-division": { - "type": "string", - "description": "Subdivision of the country (e.g., state, province)" - }, - "country": { - "type": "string", - "description": "Country in ISO 3166 Alpha-2 format", - "pattern": "^[A-Z]{2,3}$" - }, - "address-line": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Address lines for unstructured address information" - } - }, - "required": [ - "country" - ] - }, - "organizational-identifier": { - "description": "Accepted organizational identifier (e.g., LEI, GLN, CRN)", - "anyOf": [ - { - "type": "string", - "pattern": "^(lei|gln|crn):[a-zA-Z0-9]+$", - "description": "A single location identifier string with a specific prefix (e.g., 'lei:', 'gln:', 'crn:')" - }, - { - "type": "array", - "items": { - "type": "string", - "pattern": "^(lei|gln|crn):[a-zA-Z0-9]+$", - "description": "Each item in the array must be a organization identifier string with a specific prefix" - }, - "minItems": 1, - "description": "An array of organization identifier strings" - } - ] - } - }, - "required": [ - "guid", - "organization-type", - "organizational-identifier" - ] + "$ref": "components/organization.json" } }, "affiliations": { "type": "array", "items": { - "type": "object", - "properties": { - "from-guid": { - "type": "string", - "format": "uuid", - "description": "Unique identifier for the source entity" - }, - "to-guid": { - "type": "string", - "format": "uuid", - "description": "Unique identifier for the target entity" - }, - "affiliation-type": { - "type": "string", - "description": "Type of relationship between the entities" - } - }, - "required": [ - "from-guid", - "to-guid", - "affiliation-type" - ] + "$ref": "components/affiliation.json" } } }, "required": [ "locations" ], - "definitions": { - "location-identifier": { - "type": "string", - "pattern": "^(osid|gfid):[A-Za-z0-9]+$", - "description": "A location identifier string with a specific prefix (e.g., 'osid:', 'gfid:')" - } - } + "additionalProperties": true } \ No newline at end of file diff --git a/schema/definitions/address.json b/schema/definitions/address.json new file mode 100644 index 0000000..31b1de6 --- /dev/null +++ b/schema/definitions/address.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Address Schema", + "type": "object", + "description": "Address of the location in ISO 20022 format", + "properties": { + "street-name": { + "type": "string", + "description": "Street name of the address" + }, + "building-number": { + "type": "string", + "description": "Building number of the address" + }, + "post-code": { + "type": "string", + "description": "Postal code of the address" + }, + "town-name": { + "type": "string", + "description": "Town or city name of the address" + }, + "country-sub-division": { + "type": "string", + "description": "Subdivision of the country (e.g., state, province)" + }, + "country": { + "type": "string", + "description": "Country in ISO 3166 Alpha-2 format", + "pattern": "^[A-Z]{2}$" + }, + "address-line": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Address lines for unstructured address information" + } + }, + "required": [ + "country" + ] +} \ No newline at end of file diff --git a/schema/definitions/language.json b/schema/definitions/language.json new file mode 100644 index 0000000..1628de7 --- /dev/null +++ b/schema/definitions/language.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Language Schema", + "type": "string", + "description": "Language code in ISO 639-1 or ISO 639-2 format", + "pattern": "^[a-z]{2,3}$" +} \ No newline at end of file diff --git a/schema/definitions/location-identifier b/schema/definitions/location-identifier new file mode 100644 index 0000000..975564a --- /dev/null +++ b/schema/definitions/location-identifier @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Location Identifier Schema", + "type": "string", + "pattern": "^(osid|gfid):[A-Za-z0-9]+$", + "description": "A location identifier string with a specific prefix (e.g., 'osid:', 'gfid:')" +} \ No newline at end of file diff --git a/schema/definitions/organization-identifier b/schema/definitions/organization-identifier new file mode 100644 index 0000000..28df938 --- /dev/null +++ b/schema/definitions/organization-identifier @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Organization Identifier Schema", + "type": "string", + "pattern": "^(lei|gln|crn):[a-zA-Z0-9]+$", + "description": "An organization identifier string with a specific prefix (e.g., 'lei:', 'gln:', 'crn:')" +} \ No newline at end of file diff --git a/schema/extensions/location-extensions.json b/schema/extensions/location-extensions.json new file mode 100644 index 0000000..1b8838a --- /dev/null +++ b/schema/extensions/location-extensions.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Location Extensions Schema", + "description": "Consolidated schema for all location extensions.", + "type": "object", + "properties": { + "responsible-recruiting": { + "$ref": "location-extensions/responsible-recruiting.json" + }, + "sea-level-risk": { + "$ref": "location-extensions/sea-level-risk.json" + } + } +} \ No newline at end of file diff --git a/schema/extensions/location-extensions/responsible-recruiting.json b/schema/extensions/location-extensions/responsible-recruiting.json new file mode 100644 index 0000000..8766d87 --- /dev/null +++ b/schema/extensions/location-extensions/responsible-recruiting.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Extension Schema for Responsible Recruiting", + "description": "Schema for responsible recruiting information at a location.", + "type": "object", + "properties": { + "recruitment-fee": { + "type": "number", + "description": "The recruitment fee charged to the worker, expressed as a monetary amount.", + "minimum": 0 + }, + "employer-coverage": { + "type": "number", + "description": "The amount covered by the employer for recruitment costs.", + "minimum": 0 + }, + "initial-contact-date": { + "type": "string", + "format": "date", + "description": "The date when the initial contact with the worker was made." + }, + "contract-signing-date": { + "type": "string", + "format": "date", + "description": "The date when the worker signed the employment contract." + }, + "first-paycheck-date": { + "type": "string", + "format": "date", + "description": "The date when the worker received their first paycheck." + } + }, + "required": [ + "recruitment-fee", + "employer-coverage", + "initial-contact-date", + "contract-signing-date", + "first-paycheck-date" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/schema/extensions/location-extensions/sea-level-risk.json b/schema/extensions/location-extensions/sea-level-risk.json new file mode 100644 index 0000000..a4f08f3 --- /dev/null +++ b/schema/extensions/location-extensions/sea-level-risk.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Extension Schema for Sea Level Risk", + "description": "Schema for sea level risk information related to a specific region.", + "type": "object", + "properties": { + "region": { + "type": "string", + "description": "The name of the region under sea level risk." + }, + "projected-underwater-date": { + "type": "string", + "format": "date", + "description": "The projected date by which the region is expected to be underwater." + }, + "yearly-impact": { + "type": "array", + "description": "An array of yearly impacts showing the percentage affected per year.", + "items": { + "type": "object", + "properties": { + "year": { + "type": "integer", + "description": "The year for which the impact is estimated." + }, + "percentage-affected": { + "type": "number", + "description": "The percentage of the region expected to be affected in the given year.", + "minimum": 0 + } + }, + "required": [ + "year", + "percentage-affected" + ], + "additionalProperties": true + } + } + }, + "required": [ + "region", + "projected-underwater-date", + "yearly-impact" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/tests/scdex/__init__.py b/tests/scdex/__init__.py index 8fba526..e089186 100644 --- a/tests/scdex/__init__.py +++ b/tests/scdex/__init__.py @@ -1,12 +1,22 @@ """ This module simply exposes the SCDEX JSON schema as a Python constant for use in testing. +It also provides a JSON schema validator with reference resolution, configured to work with the SCDEX core schema and its dependencies. """ from pathlib import Path import json +from jsonschema import Draft202012Validator, RefResolver + SCDEX_SCHEMA_REPO_ROOT = Path(__file__).parent.parent.parent with open(SCDEX_SCHEMA_REPO_ROOT.joinpath("schema", "core-schema.json"), 'r') as f: _schema_content = f.read() SCDEX_JSON_SCHEMA = json.loads(_schema_content) + +resolver = RefResolver( + base_uri=f'{SCDEX_SCHEMA_REPO_ROOT.joinpath("schema").resolve().as_uri()}/', # base directory for schema files + referrer=SCDEX_JSON_SCHEMA +) + +validator = Draft202012Validator(schema=SCDEX_JSON_SCHEMA, resolver=resolver) \ No newline at end of file diff --git a/tests/tests/test_scdex_schema.py b/tests/tests/test_scdex_schema.py index 5bfe5ea..b2ffa63 100644 --- a/tests/tests/test_scdex_schema.py +++ b/tests/tests/test_scdex_schema.py @@ -2,7 +2,7 @@ import jsonschema import pytest from jsonschema import validate, Draft202012Validator -from scdex import SCDEX_JSON_SCHEMA, SCDEX_SCHEMA_REPO_ROOT +from scdex import validator, SCDEX_JSON_SCHEMA, SCDEX_SCHEMA_REPO_ROOT examples_path = SCDEX_SCHEMA_REPO_ROOT.joinpath("examples", "full") @@ -21,7 +21,7 @@ def test_valid_examples(path_to_validate): with open(path_to_validate, 'r') as f: data = f.read() data = json.loads(data) - validate(data, SCDEX_JSON_SCHEMA, cls=Draft202012Validator) + validator.validate(data) @pytest.mark.parametrize("path_to_validate", examples_path.glob("invalid/*.json")) @@ -31,4 +31,4 @@ def test_invalid_examples(path_to_validate): data = f.read() data = json.loads(data) with pytest.raises(jsonschema.exceptions.ValidationError): - validate(data, SCDEX_JSON_SCHEMA, cls=Draft202012Validator) + validator.validate(data)