From 4119de516f7783c74816b07cd676372bab5f5785 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 18 Jun 2020 16:16:47 +0100 Subject: [PATCH 1/3] Added W3C schema --- tests/schemas/w3c_td_schema.json | 1035 ++++++++++++++++++++++++++++++ 1 file changed, 1035 insertions(+) create mode 100644 tests/schemas/w3c_td_schema.json diff --git a/tests/schemas/w3c_td_schema.json b/tests/schemas/w3c_td_schema.json new file mode 100644 index 00000000..62194083 --- /dev/null +++ b/tests/schemas/w3c_td_schema.json @@ -0,0 +1,1035 @@ +{ + "title": "WoT TD Schema - 16 October 2019", + "description": "JSON Schema for validating TD instances against the TD model. TD instances can be with or without terms that have default values", + "$schema ": "http://json-schema.org/draft-07/schema#", + "definitions": { + "anyUri": { + "type": "string", + "format": "iri-reference" + }, + "description": { + "type": "string" + }, + "descriptions": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "title": { + "type": "string" + }, + "titles": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "security": { + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "scopes": { + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "subprotocol": { + "type": "string", + "enum": [ + "longpoll", + "websub", + "sse" + ] + }, + "thing-context-w3c-uri": { + "type": "string", + "enum": [ + "https://www.w3.org/2019/wot/td/v1" + ] + }, + "thing-context": { + "oneOf": [{ + "type": "array", + "items": [{ + "$ref": "#/definitions/thing-context-w3c-uri" + }], + "additionalItems": { + "anyOf": [{ + "$ref": "#/definitions/anyUri" + }, + { + "type": "object" + } + ] + } + }, + { + "$ref": "#/definitions/thing-context-w3c-uri" + } + ] + }, + "type_declaration": { + "oneOf": [{ + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "dataSchema": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "title": { + "$ref": "#/definitions/title" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "writeOnly": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + }, + "unit": { + "type": "string" + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "format": { + "type": "string" + }, + "const": {}, + "type": { + "type": "string", + "enum": [ + "boolean", + "integer", + "number", + "string", + "object", + "array", + "null" + ] + }, + "items": { + "oneOf": [{ + "$ref": "#/definitions/dataSchema" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + } + ] + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "properties": { + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "form_element_property": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "readproperty", + "writeproperty", + "observeproperty", + "unobserveproperty" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "readproperty", + "writeproperty", + "observeproperty", + "unobserveproperty" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "form_element_action": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "invokeaction" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "invokeaction" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "form_element_event": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "subscribeevent", + "unsubscribeevent" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "subscribeevent", + "unsubscribeevent" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "form_element_root": { + "type": "object", + "properties": { + "op": { + "oneOf": [{ + "type": "string", + "enum": [ + "readallproperties", + "writeallproperties", + "readmultipleproperties", + "writemultipleproperties" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "readallproperties", + "writeallproperties", + "readmultipleproperties", + "writemultipleproperties" + ] + } + } + ] + }, + "href": { + "$ref": "#/definitions/anyUri" + }, + "contentType": { + "type": "string" + }, + "contentCoding": { + "type": "string" + }, + "subprotocol": { + "$ref": "#/definitions/subprotocol" + }, + "security": { + "$ref": "#/definitions/security" + }, + "scopes": { + "$ref": "#/definitions/scopes" + }, + "response": { + "type": "object", + "properties": { + "contentType": { + "type": "string" + } + } + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "property_element": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_property" + } + }, + "uriVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "observable": { + "type": "boolean" + }, + "writeOnly": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + }, + "oneOf": { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + }, + "unit": { + "type": "string" + }, + "enum": { + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "format": { + "type": "string" + }, + "const": {}, + "type": { + "type": "string", + "enum": [ + "boolean", + "integer", + "number", + "string", + "object", + "array", + "null" + ] + }, + "items": { + "oneOf": [{ + "$ref": "#/definitions/dataSchema" + }, + { + "type": "array", + "items": { + "$ref": "#/definitions/dataSchema" + } + } + ] + }, + "maxItems": { + "type": "integer", + "minimum": 0 + }, + "minItems": { + "type": "integer", + "minimum": 0 + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + }, + "properties": { + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "required": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "forms" + ], + "additionalProperties": true + }, + "action_element": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_action" + } + }, + "uriVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "input": { + "$ref": "#/definitions/dataSchema" + }, + "output": { + "$ref": "#/definitions/dataSchema" + }, + "safe": { + "type": "boolean" + }, + "idempotent": { + "type": "boolean" + } + }, + "required": [ + "forms" + ], + "additionalProperties": true + }, + "event_element": { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_event" + } + }, + "uriVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/dataSchema" + } + }, + "subscription": { + "$ref": "#/definitions/dataSchema" + }, + "data": { + "$ref": "#/definitions/dataSchema" + }, + "cancellation": { + "$ref": "#/definitions/dataSchema" + } + }, + "required": [ + "forms" + ], + "additionalProperties": true + }, + "link_element": { + "type": "object", + "properties": { + "href": { + "$ref": "#/definitions/anyUri" + }, + "type": { + "type": "string" + }, + "rel": { + "type": "string" + }, + "anchor": { + "$ref": "#/definitions/anyUri" + } + }, + "required": [ + "href" + ], + "additionalProperties": true + }, + "securityScheme": { + "oneOf": [{ + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "nosec" + ] + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "basic" + ] + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "digest" + ] + }, + "qop": { + "type": "string", + "enum": [ + "auth", + "auth-int" + ] + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "apikey" + ] + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "bearer" + ] + }, + "authorization": { + "$ref": "#/definitions/anyUri" + }, + "alg": { + "type": "string" + }, + "format": { + "type": "string" + }, + "in": { + "type": "string", + "enum": [ + "header", + "query", + "body", + "cookie" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "psk" + ] + }, + "identity": { + "type": "string" + } + }, + "required": [ + "scheme" + ] + }, + { + "type": "object", + "properties": { + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "proxy": { + "$ref": "#/definitions/anyUri" + }, + "scheme": { + "type": "string", + "enum": [ + "oauth2" + ] + }, + "authorization": { + "$ref": "#/definitions/anyUri" + }, + "token": { + "$ref": "#/definitions/anyUri" + }, + "refresh": { + "$ref": "#/definitions/anyUri" + }, + "scopes": { + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "flow": { + "type": "string", + "enum": [ + "code" + ] + } + }, + "required": [ + "scheme" + ] + } + ] + } + }, + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uri" + }, + "title": { + "$ref": "#/definitions/title" + }, + "titles": { + "$ref": "#/definitions/titles" + }, + "properties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/property_element" + } + }, + "actions": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/action_element" + } + }, + "events": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/event_element" + } + }, + "description": { + "$ref": "#/definitions/description" + }, + "descriptions": { + "$ref": "#/definitions/descriptions" + }, + "version": { + "type": "object", + "properties": { + "instance": { + "type": "string" + } + }, + "required": [ + "instance" + ] + }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/link_element" + } + }, + "forms": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/form_element_root" + } + }, + "base": { + "$ref": "#/definitions/anyUri" + }, + "securityDefinitions": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/definitions/securityScheme" + } + }, + "support": { + "$ref": "#/definitions/anyUri" + }, + "created": { + "type": "string", + "format": "date-time" + }, + "modified": { + "type": "string", + "format": "date-time" + }, + "security": { + "oneOf": [{ + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ] + }, + "@type": { + "$ref": "#/definitions/type_declaration" + }, + "@context": { + "$ref": "#/definitions/thing-context" + } + }, + "required": [ + "title", + "security", + "securityDefinitions", + "@context" + ], + "additionalProperties": true +} \ No newline at end of file From 5c0ca64c2178ae20baa6e950c42b46db66fa0cc6 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 18 Jun 2020 16:32:33 +0100 Subject: [PATCH 2/3] Allow boolean values for "required". May be reverted at some point --- tests/schemas/w3c_td_schema.json | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/tests/schemas/w3c_td_schema.json b/tests/schemas/w3c_td_schema.json index 62194083..56abb5cb 100644 --- a/tests/schemas/w3c_td_schema.json +++ b/tests/schemas/w3c_td_schema.json @@ -182,10 +182,16 @@ } }, "required": { - "type": "array", - "items": { - "type": "string" - } + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "boolean" + } + ] } } }, @@ -517,10 +523,16 @@ } }, "required": { - "type": "array", - "items": { - "type": "string" - } + "oneOf": [{ + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "boolean" + } + ] } }, "required": [ From e173b6104fab45c74927cbcc727f495e8f62fbee Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 18 Jun 2020 16:33:30 +0100 Subject: [PATCH 3/3] Restored W3C TD compatibility --- src/labthings/server/spec/td.py | 73 +++++++++++++++++++++++++++++++-- tests/conftest.py | 2 +- tests/test_server_spec_td.py | 1 - 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/labthings/server/spec/td.py b/src/labthings/server/spec/td.py index b9191593..620ce1cc 100644 --- a/src/labthings/server/spec/td.py +++ b/src/labthings/server/spec/td.py @@ -50,6 +50,65 @@ def find_schema_for_view(view: View): return prop_schema +def build_forms_for_view(rules: list, view: View, op: list): + """Build a W3C form description for a particular View + + Args: + rules (list): List of Flask rules + view (View): View class + op (list): List of Form operations + + Returns: + [dict]: Form description + """ + forms = [] + prop_urls = [rule_to_path(rule) for rule in rules] + + content_type = get_topmost_spec_attr(view, "_content_type") or "application/json" + + for url in prop_urls: + forms.append({"op": op, "href": url, "contentType": content_type}) + + return forms + + +def view_to_thing_property_forms(rules: list, view: View): + """Build a W3C form description for a PropertyView + + Args: + rules (list): List of Flask rules + view (View): View class + op (list): List of Form operations + + Returns: + [dict]: Form description + """ + readable = hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete") + writeable = hasattr(view, "get") + + op = [] + if readable: + op.append("readproperty") + if writeable: + op.append("writeproperty") + + return build_forms_for_view(rules, view, op=op) + + +def view_to_thing_action_forms(rules: list, view: View): + """Build a W3C form description for an ActionView + + Args: + rules (list): List of Flask rules + view (View): View class + op (list): List of Form operations + + Returns: + [dict]: Form description + """ + return build_forms_for_view(rules, view, op=["invokeaction"]) + + class ThingDescription: def __init__(self, apispec: APISpec): self._apispec = weakref.ref(apispec) @@ -91,7 +150,10 @@ def add_link(self, view, rel, kwargs=None, params=None): def to_dict(self): return { - "@context": ["https://iot.mozilla.org/schemas/"], + "@context": [ + "https://www.w3.org/2019/wot/td/v1", + "https://iot.mozilla.org/schemas/", + ], "@type": current_labthing().types, "id": url_for("root", _external=True), "base": request.host_url, @@ -99,13 +161,15 @@ def to_dict(self): "description": current_labthing().description, "properties": self.properties, "actions": self.actions, - "events": self.events, # TODO: Enable once properly populated + # "events": self.events, # TODO: Enable once properly populated "links": self.links, + "securityDefinitions": {"nosec_sc": {"scheme": "nosec"}}, + "security": "nosec_sc", } def event_to_thing_event(self, event: Event): # TODO: Include event schema - return {} + return {"forms": []} def view_to_thing_property(self, rules: list, view: View): prop_urls = [rule_to_path(rule) for rule in rules] @@ -122,8 +186,8 @@ def view_to_thing_property(self, rules: list, view: View): hasattr(view, "post") or hasattr(view, "put") or hasattr(view, "delete") ), "writeOnly": not hasattr(view, "get"), - # TODO: Make URLs absolute "links": [{"href": f"{url}"} for url in prop_urls], + "forms": view_to_thing_property_forms(rules, view), "uriVariables": {}, **get_semantic_type(view), } @@ -181,6 +245,7 @@ def view_to_thing_action(self, rules: list, view: View): or (get_docstring(view.post) if hasattr(view, "post") else ""), # TODO: Make URLs absolute "links": [{"href": f"{url}"} for url in action_urls], + "forms": view_to_thing_action_forms(rules, view), "safe": is_safe, "idempotent": is_idempotent, } diff --git a/tests/conftest.py b/tests/conftest.py index 3d43f4e4..95c2f496 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ class Helpers: @staticmethod def validate_thing_description(thing_description, app_ctx, schemas_path): - schema = json.load(open(os.path.join(schemas_path, "td_schema.json"), "r")) + schema = json.load(open(os.path.join(schemas_path, "w3c_td_schema.json"), "r")) jsonschema.Draft7Validator.check_schema(schema) with app_ctx.test_request_context(): diff --git a/tests/test_server_spec_td.py b/tests/test_server_spec_td.py index d263add4..0b4acfcb 100644 --- a/tests/test_server_spec_td.py +++ b/tests/test_server_spec_td.py @@ -46,7 +46,6 @@ class ViewClass: def test_td_init(helpers, thing_description, app_ctx, schemas_path): assert thing_description - helpers.validate_thing_description(thing_description, app_ctx, schemas_path)