diff --git a/.gitmodules b/.gitmodules index c01a66aba4..a52edf3c1e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "interfaces/automated_testing"] path = interfaces/automated_testing url = https://github.com/interuss/automated_testing_interfaces +[submodule "schemas/ed318"] + path = schemas/ed318 + url = git@github.com:UASGeoZones/ED-318.git diff --git a/monitoring/Dockerfile b/monitoring/Dockerfile index 9f7f89dcd4..c935488b8b 100644 --- a/monitoring/Dockerfile +++ b/monitoring/Dockerfile @@ -52,6 +52,7 @@ WORKDIR /app/monitoring # Add core content from repo ADD ./interfaces /app/interfaces +ADD ./schemas /app/schemas ADD ./monitoring /app/monitoring # Add health check to the /app root and make it executable diff --git a/monitoring/uss_qualifier/configurations/dev/geoawareness_cis.yaml b/monitoring/uss_qualifier/configurations/dev/geoawareness_cis.yaml index 25b30da1f3..ac405b7fb7 100644 --- a/monitoring/uss_qualifier/configurations/dev/geoawareness_cis.yaml +++ b/monitoring/uss_qualifier/configurations/dev/geoawareness_cis.yaml @@ -3,15 +3,20 @@ v1: test_run: resources: resource_declarations: - source_document: + source_document_ed269: resource_type: resources.eurocae.ed269.source_document.SourceDocument specification: - url: file://./test_data/che/geoawareness/cis_source_sample.json + url: file://./test_data/che/geoawareness/cis_source_sample_ed269.json + source_document_ed318: + resource_type: resources.eurocae.ed318.source_document.SourceDocument + specification: + url: file://./test_data/che/geoawareness/cis_source_sample_ed318.json action: test_suite: suite_type: suites.uspace.geo_awareness_cis resources: - source_document: source_document + source_document_ed269: source_document_ed269 + source_document_ed318: source_document_ed318 execution: stop_fast: true artifacts: diff --git a/monitoring/uss_qualifier/resources/eurocae/ed318/__init__.py b/monitoring/uss_qualifier/resources/eurocae/ed318/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/uss_qualifier/resources/eurocae/ed318/source_document.py b/monitoring/uss_qualifier/resources/eurocae/ed318/source_document.py new file mode 100644 index 0000000000..234f4990c7 --- /dev/null +++ b/monitoring/uss_qualifier/resources/eurocae/ed318/source_document.py @@ -0,0 +1,23 @@ +from implicitdict import ImplicitDict + +from monitoring.uss_qualifier import fileio +from monitoring.uss_qualifier.resources.resource import Resource + + +class SourceDocumentSpecification(ImplicitDict): + url: str + """Url of the ED-318 document to verify""" + + +class SourceDocument(Resource[SourceDocumentSpecification]): + specification: SourceDocumentSpecification + + raw_document: str + """Content of the document""" + + def __init__( + self, specification: SourceDocumentSpecification, resource_origin: str + ): + super().__init__(specification, resource_origin) + self.specification = specification + self.raw_document = fileio.load_content(specification.url) diff --git a/monitoring/uss_qualifier/scenarios/eurocae/ed318/__init__.py b/monitoring/uss_qualifier/scenarios/eurocae/ed318/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/uss_qualifier/scenarios/eurocae/ed318/source_data_model.md b/monitoring/uss_qualifier/scenarios/eurocae/ed318/source_data_model.md new file mode 100644 index 0000000000..89efb5fb21 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/eurocae/ed318/source_data_model.md @@ -0,0 +1,23 @@ +# EUROCAE ED-318 UAS geographical zone model test scenario + +## Overview + +This scenario verifies that a JSON document complies with the ED-318 UAS Geographical Zone Model for Geo-Awareness purpose. + +## Resources + +### source_document + +The file or url of the document to be tested. + +## ED-318 data model compliance test case + +### Valid source test step + +#### 🛑 Valid JSON check + +The JSON file is properly formatted and can be read successfully. + +#### 🛑 Valid schema and values check + +The file respects the ED-318 schema and values are valid. diff --git a/monitoring/uss_qualifier/scenarios/eurocae/ed318/source_data_model.py b/monitoring/uss_qualifier/scenarios/eurocae/ed318/source_data_model.py new file mode 100644 index 0000000000..9c597556b3 --- /dev/null +++ b/monitoring/uss_qualifier/scenarios/eurocae/ed318/source_data_model.py @@ -0,0 +1,80 @@ +import json +import pathlib + +import referencing +from jsonschema import ValidationError, validate +from referencing.exceptions import NoSuchResource + +import monitoring +from monitoring.uss_qualifier.resources.eurocae.ed318.source_document import ( + SourceDocument, +) +from monitoring.uss_qualifier.scenarios.scenario import TestScenario +from monitoring.uss_qualifier.suites.suite import ExecutionContext + + +class SourceDataModelValidation(TestScenario): + source_document: SourceDocument + schema_registry: referencing.Registry = referencing.Registry() + schema: dict + + def __init__(self, source_document: SourceDocument): + super().__init__() + self.source_document = source_document + + # create a JSON schema registry for ED318 schemas + def retrieve_schema(uri: str) -> referencing.Resource: + # the $ref in the schemas are relative paths, this function allows resolving them + repo_root = pathlib.Path(str(monitoring.__file__)).parent.parent + schema_path = repo_root / "schemas" / "ed318" / "schema" / uri + if not schema_path.exists(): + raise NoSuchResource(ref=str(schema_path)) + with open(schema_path) as schema: + return referencing.Resource.from_contents(json.load(schema)) + + self.schema_registry = referencing.Registry(retrieve=retrieve_schema) + self.schema = self.schema_registry.get_or_retrieve( + "Schema_GeoZones.json" + ).value.contents + + def run(self, context: ExecutionContext): + self.begin_test_scenario(context) + + self.record_note( + "Document", + f"Ready at {self.source_document.specification.url}", + ) + + self.begin_test_case("ED-318 data model compliance") + self.begin_test_step("Valid source") + + data = None + with self.check( + "Valid JSON", + [self.source_document.specification.url], + ) as check: + try: + data = json.loads(self.source_document.raw_document) + except json.decoder.JSONDecodeError as e: + check.record_failed( + summary="Unable to deserialize the document as JSON", + details=str(e), + ) + + if data: + with self.check( + "Valid schema and values", [self.source_document.specification.url] + ) as check: + try: + validate( + instance=data, schema=self.schema, registry=self.schema_registry + ) + except ValidationError as e: + check.record_failed( + summary="Invalid format error", + details=str(e), + ) + + self.end_test_step() + self.end_test_case() + self.end_test_scenario() diff --git a/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.md b/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.md index 2c5098b044..bed04b50b7 100644 --- a/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.md +++ b/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.md @@ -5,6 +5,7 @@ ## [Actions](../README.md#actions) 1. Scenario: [EUROCAE ED-269 UAS geographical zone model](../../scenarios/eurocae/ed269/source_data_model.md) ([`scenarios.eurocae.ed269.source_data_model.SourceDataModelValidation`](../../scenarios/eurocae/ed269/source_data_model.py)) +2. Scenario: [EUROCAE ED-318 UAS geographical zone model](../../scenarios/eurocae/ed318/source_data_model.md) ([`scenarios.eurocae.ed318.source_data_model.SourceDataModelValidation`](../../scenarios/eurocae/ed318/source_data_model.py)) ## [Checked requirements](../README.md#checked-requirements) diff --git a/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.yaml b/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.yaml index c4b9a0d435..47c9c5e334 100644 --- a/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.yaml +++ b/monitoring/uss_qualifier/suites/uspace/geo_awareness_cis.yaml @@ -1,9 +1,15 @@ name: U-Space Common Information Service resources: - source_document: resources.eurocae.ed269.source_document.SourceDocument + source_document_ed269: resources.eurocae.ed269.source_document.SourceDocument + source_document_ed318: resources.eurocae.ed318.source_document.SourceDocument actions: - test_scenario: scenario_type: scenarios.eurocae.ed269.source_data_model.SourceDataModelValidation resources: - source_document: source_document + source_document: source_document_ed269 + on_failure: Abort + - test_scenario: + scenario_type: scenarios.eurocae.ed318.source_data_model.SourceDataModelValidation + resources: + source_document: source_document_ed318 on_failure: Abort diff --git a/monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample.json b/monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample_ed269.json similarity index 100% rename from monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample.json rename to monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample_ed269.json diff --git a/monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample_ed318.json b/monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample_ed318.json new file mode 100644 index 0000000000..42ebb1268c --- /dev/null +++ b/monitoring/uss_qualifier/test_data/che/geoawareness/cis_source_sample_ed318.json @@ -0,0 +1,432 @@ +{ + "type" : "FeatureCollection", + "name" : "Test data of the Swiss UAS Geozones according to ED-318 converted from the ED-269 data model", + "bbox" : [ 5.9643748, 45.8193544, 10.5591277, 47.7720945 ], + "description" : "Test data of the Swiss UAS Geozones according to ED-318 - format version 2.0", + "metadata" : { + "validFrom" : "2016-09-15T00:00:00+02:00", + "provider" : [ + { + "text" : "BAZL", + "lang" : "de-CH" + }, + { + "text" : "OFAC", + "lang" : "fr-CH" + }, + { + "text" : "UFAC", + "lang" : "it-CH" + }, + { + "text" : "FOCA", + "lang" : "en-GB" + } + ], + "issued" : "2025-03-13T13:58:00+01:00", + "description" : [ + { + "text" : "Testdaten der Schweizerischen UAS Geozones, herausgegeben vom Bundesamt für Zivilluftfahrt (BAZL). Der Datensatz hat keine Rechtskraft. Umwandlung aus dem Modell ED-269", + "lang" : "de-CH" + }, + { + "text" : "Données de test des UAS Geozones suisses publiées par l'Office fédéral de l'aviation civile (OFAC). L'ensemble de données n'a pas de valeur juridique. Conversion à partir du modèle ED-269", + "lang" : "fr-CH" + }, + { + "text" : "Dati di prova delle Geozones UAS svizzere emesse dall'Ufficio federale dell'aviazione civile (UFAC). Il dataset non ha valore legale. Conversione dal modello ED-269", + "lang" : "it-CH" + }, + { + "text" : "Test data of the Swiss UAS Geozones issued by the Federal Office of Civil Aviation (FOCA). The dataset has no legal validity. Conversion from the ED-269 model", + "lang" : "en-GB" + } + ], + "otherGeoid" : "CHGeo2004", + "technicalLimitation" : [ + { + "text" : "Der Datensatz entsteht durch die Umwandlung der Originaldaten des ED-269-Modells ins neue ED-318. Für die Umwandlung sind einige Datenänderungen nötig", + "lang" : "de-CH" + }, + { + "text" : "Le fichier a été créé en convertissant les données originales du modèle ED-269 dans le nouveau ED-318. La conversion nécessite des modifications des données", + "lang" : "fr-CH" + }, + { + "text" : "Il dataset è stato creato convertendo i dati originali del modello ED-269 nel nuovo ED-318. Per la conversione alcune modifiche dei dati sono necessarie", + "lang" : "it-CH" + }, + { + "text" : "The dataset was created by converting the original data from the ED-269 model to the new ED-318. Some data modifications are necessary for conversion", + "lang" : "en-GB" + } + ] + }, + "features" : [ + { + "type" : "Feature", + "geometry" : { + "type" : "Polygon", + "coordinates" : [ + [ + [ 7.4355068, 46.9402526 ], + [ 7.4368695, 46.9418327 ], + [ 7.4387706, 46.9437947 ], + [ 7.4408192, 46.9456857 ], + [ 7.4430096, 46.9475007 ], + [ 7.4453359, 46.9492347 ], + [ 7.4477917, 46.9508829 ], + [ 7.4503702, 46.9524407 ], + [ 7.4530644, 46.9539039 ], + [ 7.4558669, 46.9552685 ], + [ 7.45877, 46.9565308 ], + [ 7.4617657, 46.9576872 ], + [ 7.4648458, 46.9587345 ], + [ 7.4680019, 46.95967 ], + [ 7.4712253, 46.9604911 ], + [ 7.4745071, 46.9611954 ], + [ 7.4778383, 46.961781 ], + [ 7.4812098, 46.9622464 ], + [ 7.4846123, 46.9625903 ], + [ 7.4880365, 46.9628117 ], + [ 7.4914728, 46.9629099 ], + [ 7.494912, 46.9628848 ], + [ 7.4983446, 46.9627365 ], + [ 7.5017610999999995, 46.9624652 ], + [ 7.5051521, 46.9620718 ], + [ 7.5085083, 46.9615573 ], + [ 7.5093889, 46.9613887 ], + [ 7.4355068, 46.9402526 ] + ] + ], + "layer" : { + "upper" : 99999, + "upperReference" : "AGL", + "lower" : 60, + "lowerReference" : "AGL", + "uom" : "m" + } + }, + "properties" : { + "identifier" : "LSZB003", + "country" : "CHE", + "name" : [ + { + "text" : "LSZB Bern-Belp 2", + "lang" : "de-CH" + }, + { + "text" : "LSZB Bern-Belp 2", + "lang" : "fr-CH" + }, + { + "text" : "LSZB Bern-Belp 2", + "lang" : "it-CH" + }, + { + "text" : "LSZB Bern-Belp 2", + "lang" : "en-GB" + } + ], + "type" : "REQ_AUTHORIZATION", + "variant" : "COMMON", + "restrictionConditions" : "The operation of unmanned aircraft weighing more than 250 g is only allowed with exemption permit.", + "region" : 0, + "reason" : [ "AIR_TRAFFIC" ], + "extendedProperties" : { + "addInfo" : "Exemption permits may be applied for at the competent authority." + }, + "zoneAuthority" : [ + { + "name" : [ + { + "text" : "Skyguide", + "lang" : "de-CH" + }, + { + "text" : "Skyguide", + "lang" : "fr-CH" + }, + { + "text" : "Skyguide", + "lang" : "it-CH" + }, + { + "text" : "Skyguide", + "lang" : "en-GB" + } + ], + "service" : [ + { + "text" : "Special Flight Office (SFO)", + "lang" : "de-CH" + }, + { + "text" : "Special Flight Office (SFO)", + "lang" : "fr-CH" + }, + { + "text" : "Special Flight Office (SFO)", + "lang" : "it-CH" + }, + { + "text" : "Special Flight Office (SFO)", + "lang" : "en-GB" + } + ], + "siteURL" : "https://skyguide.ch/services/special-flights", + "purpose" : "AUTHORIZATION", + "intervalBefore" : "P10DT00H" + } + ] + } + }, + { + "type" : "Feature", + "geometry" : { + "type" : "Polygon", + "coordinates" : [ + [ + [ 7.5380771, 47.4931128 ], + [ 7.5380571, 47.4931546 ], + [ 7.538315, 47.4932532 ], + [ 7.5385897, 47.493358 ], + [ 7.5386419, 47.4933807 ], + [ 7.5387059, 47.4933811 ], + [ 7.538958, 47.493485 ], + [ 7.5390456, 47.4935621 ], + [ 7.5391291, 47.4935929 ], + [ 7.5391899, 47.493658 ], + [ 7.5392657, 47.4937026 ], + [ 7.5394173, 47.493705 ], + [ 7.5394293, 47.49368 ], + [ 7.5394407, 47.4936561 ], + [ 7.5396604, 47.4931961 ], + [ 7.538932, 47.4930931 ], + [ 7.5381406, 47.4929805 ], + [ 7.5380771, 47.4931128 ] + ] + ], + "layer" : { + "upper" : 99999, + "upperReference" : "AGL", + "lower" : 0, + "lowerReference" : "AGL", + "uom" : "m" + } + }, + "properties" : { + "identifier" : "BLns058", + "country" : "CHE", + "name" : [ + { + "text" : "Geschütztes Naturobjekt Langmatt", + "lang" : "de-CH" + }, + { + "text" : "Objet naturel protégé Langmatt", + "lang" : "fr-CH" + }, + { + "text" : "Oggetto naturale protetto Langmatt", + "lang" : "it-CH" + }, + { + "text" : "Protected natural object Langmatt", + "lang" : "en-GB" + } + ], + "type" : "REQ_AUTHORIZATION", + "variant" : "COMMON", + "restrictionConditions" : "The operation of unmanned aircraft is only allowed with exemption permit.", + "region" : 0, + "reason" : [ "NATURE" ], + "extendedProperties" : { + "addInfo" : "Exemption permits may be applied for at the competent authority." + }, + "zoneAuthority" : [ + { + "name" : [ + { + "text" : "Ebenrain-Zentrum für Landwirtschaft, Natur und Ernährung", + "lang" : "de-CH" + }, + { + "text" : "Centre Ebenrain pour l’Agriculture, la Nature et l’Alimentation", + "lang" : "fr-CH" + }, + { + "text" : "Centro Ebenrain per l’Agricoltura, la Natura e l’Alimentazione", + "lang" : "it-CH" + }, + { + "text" : "Ebenrain Centre for Agriculture, Nature and Food", + "lang" : "en-GB" + } + ], + "service" : [ + { + "text" : "Abteilung Natur und Landschaft", + "lang" : "de-CH" + }, + { + "text" : "Département Nature et Paysage", + "lang" : "fr-CH" + }, + { + "text" : "Dipartimento Natura e Paesaggio", + "lang" : "it-CH" + }, + { + "text" : "Department for Nature and Landscape", + "lang" : "en-GB" + } + ], + "contactName" : [ + { + "text" : "Abteilungsleiter", + "lang" : "de-CH" + }, + { + "text" : "Abteilungsleiter", + "lang" : "fr-CH" + }, + { + "text" : "Abteilungsleiter", + "lang" : "it-CH" + }, + { + "text" : "Abteilungsleiter", + "lang" : "en-GB" + } + ], + "siteURL" : "http://www.natur-und-landschaft.bl.ch", + "email" : "naturundlandschaft@bl.ch", + "phone" : "+41 61 552 21 21", + "purpose" : "AUTHORIZATION", + "intervalBefore" : "P07DT00H" + } + ] + } + }, + { + "type" : "Feature", + "geometry" : { + "type" : "Polygon", + "coordinates" : [ + [ + [ 7.1289534, 46.9783049 ], + [ 7.1285816, 46.9789731 ], + [ 7.136025, 46.9802911 ], + [ 7.136396, 46.9795329 ], + [ 7.130964, 46.9786 ], + [ 7.1311429, 46.9781174 ], + [ 7.1295214, 46.9778234 ], + [ 7.1292357, 46.9783542 ], + [ 7.1289534, 46.9783049 ] + ] + ], + "layer" : { + "upper" : 99999, + "upperReference" : "AGL", + "lower" : 0, + "lowerReference" : "AGL", + "uom" : "m" + } + }, + "properties" : { + "identifier" : "LSTB002", + "country" : "CHE", + "name" : [ + { + "text" : "LSTB Bellechasse (Flugplatzperimeter)", + "lang" : "de-CH" + }, + { + "text" : "LSTB Bellechasse (Périmètre d'aérodrome)", + "lang" : "fr-CH" + }, + { + "text" : "LSTB Bellechasse (Perimetro dell'aerodromo)", + "lang" : "it-CH" + }, + { + "text" : "LSTB Bellechasse (Airport perimeter)", + "lang" : "en-GB" + } + ], + "type" : "REQ_AUTHORIZATION", + "variant" : "COMMON", + "restrictionConditions" : "The operation of unmanned aircraft is only allowed with exemption permit.", + "region" : 0, + "reason" : [ "AIR_TRAFFIC" ], + "extendedProperties" : { + "addInfo" : "Exemption permits may be applied for at the competent authority." + }, + "zoneAuthority" : [ + { + "name" : [ + { + "text" : "Segelfluggruppe Freiburg", + "lang" : "de-CH" + }, + { + "text" : "Segelfluggruppe Freiburg", + "lang" : "fr-CH" + }, + { + "text" : "Segelfluggruppe Freiburg", + "lang" : "it-CH" + }, + { + "text" : "Segelfluggruppe Freiburg", + "lang" : "en-GB" + } + ], + "service" : [ + { + "text" : "Flugplatzleitung", + "lang" : "de-CH" + }, + { + "text" : "Flugplatzleitung", + "lang" : "fr-CH" + }, + { + "text" : "Flugplatzleitung", + "lang" : "it-CH" + }, + { + "text" : "Flugplatzleitung", + "lang" : "en-GB" + } + ], + "contactName" : [ + { + "text" : "Reto Petri", + "lang" : "de-CH" + }, + { + "text" : "Reto Petri", + "lang" : "fr-CH" + }, + { + "text" : "Reto Petri", + "lang" : "it-CH" + }, + { + "text" : "Reto Petri", + "lang" : "en-GB" + } + ], + "siteURL" : "https://www.sg-freiburg.com/kontakt", + "email" : "flugplatzleiter@sg-freiburg.ch", + "phone" : "0041266731933", + "purpose" : "AUTHORIZATION", + "intervalBefore" : "P02DT00H" + } + ] + } + } + ] +} diff --git a/pyproject.toml b/pyproject.toml index ddc58b71b2..1ede3f9eba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ exclude-newer = "P7D" target-version = "py313" extend-exclude = [ "interfaces/*", + "schemas/ed318/*", "monitoring/prober/output/*", "monitoring/mock_uss/output/*", "monitoring/uss_qualifier/output/*", @@ -89,6 +90,7 @@ exclude = [ "**/__pycache__", "**/.*", "interfaces/*", + "schemas/ed318/*", "monitoring/prober/output/*", "monitoring/mock_uss/output/*", "monitoring/uss_qualifier/output/*", diff --git a/schemas/ed318 b/schemas/ed318 new file mode 160000 index 0000000000..e98b292c56 --- /dev/null +++ b/schemas/ed318 @@ -0,0 +1 @@ +Subproject commit e98b292c5665a04989d62e32fd93829f161a89a9 diff --git a/schemas/manage_type_schemas.py b/schemas/manage_type_schemas.py index 7fbbde3073..abe019ffff 100644 --- a/schemas/manage_type_schemas.py +++ b/schemas/manage_type_schemas.py @@ -178,7 +178,7 @@ def path_to(t_dest: type, t_src: type) -> str: changes = 0 # Check for non-current schemas that need to be removed - for dirpath, _, filenames in os.walk(os.path.join(repo_root, "schemas")): + for dirpath, _, filenames in os.walk(os.path.join(repo_root, "schemas/monitoring")): for filename in filenames: rel_filename = os.path.relpath( os.path.join(dirpath, filename), start=repo_root diff --git a/schemas/monitoring/uss_qualifier/resources/eurocae/ed318/source_document/SourceDocumentSpecification.json b/schemas/monitoring/uss_qualifier/resources/eurocae/ed318/source_document/SourceDocumentSpecification.json new file mode 100644 index 0000000000..ce138f657d --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/eurocae/ed318/source_document/SourceDocumentSpecification.json @@ -0,0 +1,19 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/eurocae/ed318/source_document/SourceDocumentSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.eurocae.ed318.source_document.SourceDocumentSpecification, as defined in monitoring/uss_qualifier/resources/eurocae/ed318/source_document.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "url": { + "description": "Url of the ED-318 document to verify", + "type": "string" + } + }, + "required": [ + "url" + ], + "type": "object" +} \ No newline at end of file