From 99d92751ef65f178c3751e5d4cc20ad8a2dc6fd6 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 7 Sep 2023 00:45:25 +0100 Subject: [PATCH 1/6] Fix IS-04 downgrade to skip optional property for invalid downgrade value or set to predefined default value for the required property --- Development/nmos/api_downgrade.cpp | 1191 +++++++++++++++++++++++++++- 1 file changed, 1168 insertions(+), 23 deletions(-) diff --git a/Development/nmos/api_downgrade.cpp b/Development/nmos/api_downgrade.cpp index 1047d78fd..5f80ad67e 100644 --- a/Development/nmos/api_downgrade.cpp +++ b/Development/nmos/api_downgrade.cpp @@ -1,11 +1,973 @@ #include "nmos/api_downgrade.h" #include +#include "cpprest/json_validator.h" +#include "cpprest/basic_utils.h" #include "nmos/is04_versions.h" #include "nmos/resource.h" +#include "nmos/type.h" namespace nmos { + namespace details + { + static web::json::value make_schema(const char* schema) + { + return web::json::value::parse(utility::s2us(schema)); + } + + static web::uri make_schema_id(const api_version& version, const nmos::type& type, const utility::string_t& property) + { + return U("/downgrade") + make_api_version(version) + U("-") + type.name + U("-") + property; + } + + // property schemas are extracted from the resource schemas + const std::map property_schemas = + { + // node + // services + { make_schema_id(is04_versions::v1_0, nmos::types::node, U("services")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "services": { + "description": "Array of objects containing a URN format type and href", + "type": "array", + "items": { + "type": "object", + "required": ["href", "type"], + "properties": { + "href": { + "type": "string", + "description": "URL to reach a service running on the Node", + "format": "uri" + }, + "type": { + "type": "string", + "description": "URN identifying the type of service", + "format": "uri" + } + } + } + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::node, U("services")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "services": { + "description": "Array of objects containing a URN format type and href", + "type": "array", + "items": { + "type": "object", + "required": ["href", "type"], + "properties": { + "href": { + "type": "string", + "description": "URL to reach a service running on the Node", + "format": "uri" + }, + "type": { + "type": "string", + "description": "URN identifying the type of service", + "format": "uri" + }, + "authorization": { + "type": "boolean", + "description": "This endpoint requires authorization", + "default": false + } + } + } + } + } + })") + }, + // interfaces + { make_schema_id(is04_versions::v1_2, nmos::types::node, U("interfaces")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "interfaces": { + "description":"Network interfaces made available to devices owned by this Node. Port IDs and Chassis IDs are used to inform topology discovery via IS-06, and require that interfaces implement ARP at a minimum, and ideally LLDP.", + "type": "array", + "items": { + "type": "object", + "required": ["chassis_id", "port_id", "name"], + "properties": { + "chassis_id": { + "description": "Chassis ID of the interface, as signalled in LLDP from this node. Set to null where LLDP is unsuitable for use (ie. virtualised environments) ", + "anyOf": [ + { + "type": "string", + "pattern" : "^([0-9a-f]{2}-){5}([0-9a-f]{2})$", + "description" : "When the Chassis ID is a MAC address, use this format" + }, + { + "type": "string", + "pattern" : "^.+$", + "description" : "When the Chassis ID is anything other than a MAC address, a freeform string may be used" + }, + { + "type": "null", + "description" : "When the Chassis ID is unavailable it should be set to null" + } + ] + }, + "port_id": { + "description": "Port ID of the interface, as signalled in LLDP or via ARP responses from this node. Must be a MAC address", + "type" : "string", + "pattern" : "^([0-9a-f]{2}-){5}([0-9a-f]{2})$" + }, + "name" : { + "description": "Name of the interface (unique in scope of this node). This attribute is used by sub-resources of this node such as senders and receivers to refer to interfaces to which they are bound.", + "type" : "string" + } + } + } + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::node, U("interfaces")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "interfaces": { + "description":"Network interfaces made available to devices owned by this Node. Port IDs and Chassis IDs are used to inform topology discovery via IS-06, and require that interfaces implement ARP at a minimum, and ideally LLDP.", + "type": "array", + "items": { + "type": "object", + "required": ["chassis_id", "port_id", "name"], + "properties": { + "chassis_id": { + "description": "Chassis ID of the interface, as signalled in LLDP from this node. Set to null where LLDP is unsuitable for use (ie. virtualised environments) ", + "anyOf": [ + { + "type": "string", + "pattern" : "^([0-9a-f]{2}-){5}([0-9a-f]{2})$", + "description" : "When the Chassis ID is a MAC address, use this format" + }, + { + "type": "string", + "pattern" : "^.+$", + "description" : "When the Chassis ID is anything other than a MAC address, a freeform string may be used" + }, + { + "type": "null", + "description" : "When the Chassis ID is unavailable it should be set to null" + } + ] + }, + "port_id": { + "description": "Port ID of the interface, as signalled in LLDP or via ARP responses from this node. Must be a MAC address", + "type" : "string", + "pattern" : "^([0-9a-f]{2}-){5}([0-9a-f]{2})$" + }, + "name" : { + "description": "Name of the interface (unique in scope of this node). This attribute is used by sub-resources of this node such as senders and receivers to refer to interfaces to which they are bound.", + "type" : "string" + }, + "attached_network_device" : { + "type": "object", + "required" : ["chassis_id", "port_id"] , + "properties" : { + "chassis_id": { + "description": "Chassis ID of the attached network device, as signalled in LLDP received by this Node.", + "anyOf" : [ + { + "type": "string", + "pattern" : "^([0-9a-f]{2}-){5}([0-9a-f]{2})$", + "description" : "When the Chassis ID is a MAC address, use this format" + }, + { + "type": "string", + "pattern" : "^.+$", + "description" : "When the Chassis ID is anything other than a MAC address, a freeform string may be used" + } + ] + }, + "port_id": { + "description": "Port ID of the attached network device, as signalled in LLDP received by this Node.", + "anyOf" : [ + { + "type": "string", + "pattern" : "^([0-9a-f]{2}-){5}([0-9a-f]{2})$", + "description" : "When the Port ID is a MAC address, use this format" + }, + { + "type": "string", + "pattern" : "^.+$", + "description" : "When the Port ID is anything other than a MAC address, a freeform string may be used" + } + ] + } + } + } + } + } + } + } + })") + }, + // device + // type + { make_schema_id(is04_versions::v1_0, nmos::types::device, U("type")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "type": { + "description": "Device type URN", + "type": "string", + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::device, U("type")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "type": { + "description": "Device type URN", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:x-nmos:device:generic", + "urn:x-nmos:device:pipeline" + ] + }, + { + "not": { + "pattern": "^urn:x-nmos:" + } + } + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::device, U("type")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "type": { + "description": "Device type URN", + "type": "string", + "oneOf": [ + { + "pattern": "^urn:x-nmos:device:" + }, + { + "not": { + "pattern": "^urn:x-nmos:" + } + } + ], + "format": "uri" + } + } + })") + }, + // controls + { make_schema_id(is04_versions::v1_1, nmos::types::device, U("controls")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "controls": { + "description": "Control endpoints exposed for the Device", + "type": "array", + "items": { + "type": "object", + "required": ["href", "type"], + "properties": { + "href": { + "type": "string", + "description": "URL to reach a control endpoint, whether http or otherwise", + "format": "uri" + }, + "type": { + "type": "string", + "description": "URN identifying the control format", + "format": "uri" + } + } + } + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::device, U("controls")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "controls": { + "description": "Control endpoints exposed for the Device", + "type": "array", + "items": { + "type": "object", + "required": ["href", "type"], + "properties": { + "href": { + "type": "string", + "description": "URL to reach a control endpoint, whether http or otherwise", + "format": "uri" + }, + "type": { + "type": "string", + "description": "URN identifying the control format", + "format": "uri" + }, + "authorization": { + "type": "boolean", + "description": "This endpoint requires authorization", + "default": false + } + } + } + } + } + })") + }, + // source + // format + { make_schema_id(is04_versions::v1_0, nmos::types::source, U("format")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "format": { + "description": "Format of the data coming from the Source as a URN", + "type": "string", + "enum": [ + "urn:x-nmos:format:video", + "urn:x-nmos:format:audio", + "urn:x-nmos:format:data" + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::source, U("format")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "format": { + "description": "Format of the data coming from the Source as a URN", + "type": "string", + "enum": [ + "urn:x-nmos:format:video", + "urn:x-nmos:format:data", + "urn:x-nmos:format:mux", + "urn:x-nmos:format:audio" + ], + "format": "uri" + } + } + })") + }, + // grain_rate + { make_schema_id(is04_versions::v1_1, nmos::types::source, U("grain_rate")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "grain_rate" : { + "description": "Maximum number of Grains per second for Flows derived from this Source. Corresponding Flow Grain rates may override this attribute. Grain rate matches the frame rate for video (see NMOS Content Model). Specified for periodic Sources only.", + "type": "object", + "required" : [ + "numerator" + ], + "properties" : { + "numerator" : { + "description" : "Numerator", + "type" : "integer" + }, + "denominator" : { + "description" : "Denominator", + "type" : "integer", + "default" : 1 + } + } + } + } + })") + }, + // flow + // format + { make_schema_id(is04_versions::v1_0, nmos::types::flow, U("format")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "format": { + "description": "Format of the data coming from the Flow as a URN", + "type": "string", + "enum": [ + "urn:x-nmos:format:video", + "urn:x-nmos:format:audio", + "urn:x-nmos:format:data" + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::flow, U("format")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "format": { + "description": "Format of the data coming from the Flow as a URN", + "type": "string", + "enum": [ + "urn:x-nmos:format:video", + "urn:x-nmos:format:audio", + "urn:x-nmos:format:data", + "urn:x-nmos:format:mux" + ], + "format": "uri" + } + } + })") + }, + // colorspace + { make_schema_id(is04_versions::v1_1, nmos::types::flow, U("colorspace")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "colorspace" : { + "description" : "Colorspace used for the video", + "type" : "string", + "enum" : [ + "BT601", + "BT709", + "BT2020", + "BT2100" + ] + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::flow, U("colorspace")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "colorspace": { + "description": "Colorspace used for the video. Any values not defined in the enum should be defined in the NMOS Parameter Registers", + "type": "string", + "anyOf": [ + { + "enum": [ + "BT601", + "BT709", + "BT2020", + "BT2100" + ] + }, + { + "pattern": "^\\S+$" + } + ] + } + } + })") + }, + // transfer_characteristic + { make_schema_id(is04_versions::v1_1, nmos::types::flow, U("transfer_characteristic")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transfer_characteristic": { + "description": "Transfer characteristic", + "type": "string", + "default": "SDR", + "enum": [ + "SDR", + "HLG", + "PQ" + ] + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::flow, U("transfer_characteristic")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transfer_characteristic": { + "description": "Transfer characteristic. Any values not defined in the enum should be defined in the NMOS Parameter Registers", + "type": "string", + "default": "SDR", + "anyOf": [ + { + "enum": [ + "SDR", + "HLG", + "PQ" + ] + }, + { + "pattern": "^\\S+$" + } + ] + } + } + })") + }, + // sender + // tags + { make_schema_id(is04_versions::v1_0, nmos::types::sender, U("tags")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "tags": { + "description": "Key value set of freeform string tags to aid in filtering Senders. Values should be represented as an array of strings. Can be empty.", + "type": "object", + "patternProperties": { + "": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::sender, U("tags")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "tags": { + "description": "Key value set of freeform string tags to aid in filtering Senders. Values should be represented as an array of strings. Can be empty.", + "type": "object", + "patternProperties": { + "": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + })") + }, + // transport + { make_schema_id(is04_versions::v1_0, nmos::types::sender, U("transport")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transport": { + "description": "Transport type used by the Sender in URN format", + "type": "string", + "enum": [ + "urn:x-nmos:transport:rtp", + "urn:x-nmos:transport:rtp.ucast", + "urn:x-nmos:transport:rtp.mcast", + "urn:x-nmos:transport:dash" + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::sender, U("transport")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transport": { + "description": "Transport type used by the Sender in URN format", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:x-nmos:transport:rtp", + "urn:x-nmos:transport:rtp.ucast", + "urn:x-nmos:transport:rtp.mcast", + "urn:x-nmos:transport:dash" + ] + }, + { + "not": { + "pattern": "^urn:x-nmos:" + } + } + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_3, nmos::types::sender, U("transport")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transport": { + "description": "Transport type used by the Sender in URN format", + "type": "string", + "oneOf": [ + { + "pattern": "^urn:x-nmos:transport:" + }, + { + "not": { + "pattern": "^urn:x-nmos:" + } + } + ], + "format": "uri" + } + } + })") + }, + // interface_bindings + { make_schema_id(is04_versions::v1_2, nmos::types::sender, U("interface_bindings")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "interface_bindings": { + "description": "Binding of Sender egress ports to interfaces on the parent Node. Should contain a single network interface unless a redundancy mechanism such as ST.2022-7 is in use, in which case each 'leg' should have its matching interface listed. Where the redundancy mechanism sends more than one copy of the stream via the same interface, that interface should be listed a corresponding number of times.", + "type": "array", + "items": { + "type":"string" + } + } + } + })") + }, + // subscription + { make_schema_id(is04_versions::v1_2, nmos::types::sender, U("subscription")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "subscription": { + "description": "Object containing the 'receiver_id' currently subscribed to (unicast only). Receiver_id should be null on initialisation, or when connected to a non-NMOS unicast Receiver.", + "type": "object", + "required": ["receiver_id", "active"], + "properties": { + "receiver_id": { + "type": ["string", "null"], + "description": "UUID of the Receiver that this Sender is currently subscribed to", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "default": null + }, + "active": { + "type": "boolean", + "description": "Sender is enabled and configured to stream data to a single Receiver (unicast), or to the network via multicast or a pull-based mechanism", + "default": false + } + } + } + } + })") + }, + // caps + { make_schema_id(is04_versions::v1_2, nmos::types::sender, U("caps")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "caps": { + "description": "Capabilities of this sender", + "type": "object", + "properties":{ + } + } + } + })") + }, + // receiver + // transport + { make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("transport")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transport": { + "description": "Transport type used by the Sender in URN format", + "type": "string", + "enum": [ + "urn:x-nmos:transport:rtp", + "urn:x-nmos:transport:rtp.ucast", + "urn:x-nmos:transport:rtp.mcast", + "urn:x-nmos:transport:dash" + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("transport")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "transport": { + "description": "Transport type used by the Sender in URN format", + "type": "string", + "oneOf": [ + { + "enum": [ + "urn:x-nmos:transport:rtp", + "urn:x-nmos:transport:rtp.ucast", + "urn:x-nmos:transport:rtp.mcast", + "urn:x-nmos:transport:dash" + ] + }, + { + "not": { + "pattern": "^urn:x-nmos:" + } + } + ], + "format": "uri" + } + } + })") + }, + // format + { make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("format")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "format": { + "description": "Type of Flow accepted by the Receiver as a URN", + "type": "string", + "enum": [ + "urn:x-nmos:format:video", + "urn:x-nmos:format:audio", + "urn:x-nmos:format:data" + ], + "format": "uri" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("format")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "format": { + "description": "Type of Flow accepted by the Receiver as a URN", + "type": "string", + "enum": [ + "urn:x-nmos:format:video", + "urn:x-nmos:format:audio", + "urn:x-nmos:format:data", + "urn:x-nmos:format:mux" + ], + "format": "uri" + } + } + })") + }, + // caps + { make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("caps")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "caps": { + "description": "Capabilities (not yet defined) ", + "type": "object" + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("caps")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "caps": { + "description": "Capabilities", + "type": "object", + "properties": { + "media_types": { + "description": "Subclassification of the formats accepted using IANA assigned media types", + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "anyOf": [ + { + "enum": [ + "video/raw", + "video/H264", + "video/vc2" + ] + }, + { + "pattern": "^video\\/[^\\s\\/]+$" + }, + { + "enum": [ + "audio/L24", + "audio/L20", + "audio/L16", + "audio/L8" + ] + }, + { + "pattern": "^audio\\/[^\\s\\/]+$" + }, + { + "enum": [ + "video/smpte291" + ] + }, + { + "pattern": "^[^\\s\\/]+\\/[^\\s\\/]+$" + }, + { + "enum": [ + "video/SMPTE2022-6" + ] + }, + { + "pattern": "^[^\\s\\/]+\\/[^\\s\\/]+$" + } + ] + } + } + } + } + } + })") + }, + // subscription + { make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("subscription")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "subscription": { + "description": "Object containing the 'sender_id' currently subscribed to. Sender_id should be null on initialisation.", + "type": "object", + "properties": { + "sender_id": { + "type": ["string", "null"], + "description": "UUID of the Sender that this Receiver is currently subscribed to", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "default": null + } + } + } + } + })") + }, + { make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("subscription")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "subscription": { + "description": "Object containing the 'sender_id' currently subscribed to. Sender_id should be null on initialisation.", + "type": "object", + "required": ["sender_id"], + "properties": { + "sender_id": { + "type": ["string", "null"], + "description": "UUID of the Sender that this Receiver is currently subscribed to", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "default": null + } + } + } + } + })") + }, + { make_schema_id(is04_versions::v1_2, nmos::types::receiver, U("subscription")), make_schema(R"({ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "subscription": { + "description": "Object containing the 'sender_id' currently subscribed to. Sender_id should be null on initialisation, or when connected to a non-NMOS Sender.", + "type": "object", + "required": ["sender_id", "active"], + "properties": { + "sender_id": { + "type": ["string", "null"], + "description": "UUID of the Sender that this Receiver is currently subscribed to", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "default": null + }, + "active": { + "type": "boolean", + "description": "Receiver is enabled and configured with a Sender's connection parameters", + "default": false + } + } + } + } + })") + } + }; + + web::json::value load_json_schema(const web::uri& id) + { + auto found = property_schemas.find(id); + + if (property_schemas.end() == found) + { + throw web::json::json_exception((_XPLATSTR("schema not found for ") + id.to_string()).c_str()); + } + + return found->second; + } + + // list of property validators to identify the associated property has been changed since the previous version + static const web::json::experimental::json_validator& property_changed_validator() + { + static const web::json::experimental::json_validator validator + { + load_json_schema, + { + // node + make_schema_id(is04_versions::v1_0, nmos::types::node, U("services")), + make_schema_id(is04_versions::v1_3, nmos::types::node, U("services")), + make_schema_id(is04_versions::v1_2, nmos::types::node, U("interfaces")), + make_schema_id(is04_versions::v1_3, nmos::types::node, U("interfaces")), + // device + make_schema_id(is04_versions::v1_0, nmos::types::device, U("type")), + make_schema_id(is04_versions::v1_1, nmos::types::device, U("type")), + make_schema_id(is04_versions::v1_3, nmos::types::device, U("type")), + make_schema_id(is04_versions::v1_1, nmos::types::device, U("controls")), + make_schema_id(is04_versions::v1_3, nmos::types::device, U("controls")), + // source + make_schema_id(is04_versions::v1_0, nmos::types::source, U("format")), + make_schema_id(is04_versions::v1_1, nmos::types::source, U("format")), + make_schema_id(is04_versions::v1_1, nmos::types::source, U("grain_rate")), + // flow + make_schema_id(is04_versions::v1_0, nmos::types::flow, U("format")), + make_schema_id(is04_versions::v1_1, nmos::types::flow, U("format")), + make_schema_id(is04_versions::v1_1, nmos::types::flow, U("colorspace")), + make_schema_id(is04_versions::v1_3, nmos::types::flow, U("colorspace")), + make_schema_id(is04_versions::v1_1, nmos::types::flow, U("transfer_characteristic")), + make_schema_id(is04_versions::v1_3, nmos::types::flow, U("transfer_characteristic")), + // sender + make_schema_id(is04_versions::v1_0, nmos::types::sender, U("tags")), + make_schema_id(is04_versions::v1_1, nmos::types::sender, U("tags")), + make_schema_id(is04_versions::v1_0, nmos::types::sender, U("transport")), + make_schema_id(is04_versions::v1_1, nmos::types::sender, U("transport")), + make_schema_id(is04_versions::v1_3, nmos::types::sender, U("transport")), + make_schema_id(is04_versions::v1_2, nmos::types::sender, U("caps")), + make_schema_id(is04_versions::v1_2, nmos::types::sender, U("interface_bindings")), + make_schema_id(is04_versions::v1_2, nmos::types::sender, U("subscription")), + // receiver + make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("transport")), + make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("transport")), + make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("format")), + make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("format")), + make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("caps")), + make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("caps")), + make_schema_id(is04_versions::v1_0, nmos::types::receiver, U("subscription")), + make_schema_id(is04_versions::v1_1, nmos::types::receiver, U("subscription")), + make_schema_id(is04_versions::v1_2, nmos::types::receiver, U("subscription")) + } + }; + return validator; + } + } + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#downgrade-queries bool is_permitted_downgrade(const nmos::resource& resource, const nmos::api_version& version) @@ -68,69 +1030,219 @@ namespace nmos return downgrade(resource.version, resource.downgrade_version, resource.type, resource.data, version, downgrade_version); } - static const std::map>>& resources_versions() + struct property + { + utility::string_t name; + web::json::value default; + bool required; // is required property + + property(utility::string_t name, web::json::value default = web::json::value::null(), bool required = true) + : name(std::move(name)) + , default(std::move(default)) + , required(std::move(required)) + {} + }; + + static const std::map>>& resources_versions() { - static const std::map>> resources_versions + using web::json::value; + using web::json::value_of; + + static const std::map>> resources_versions { { nmos::types::node, { - { nmos::is04_versions::v1_0, { U("id"), U("version"), U("label"), U("href"), U("hostname"), U("caps"), U("services") } }, - { nmos::is04_versions::v1_1, { U("description"), U("tags"), U("api"), U("clocks") } }, - { nmos::is04_versions::v1_2, { U("interfaces") } } + { nmos::is04_versions::v1_0, { + { U("id") }, + { U("version") }, + { U("label") }, + { U("href") }, + { U("hostname") }, + { U("caps") }, + { U("services"), value::array() } + } }, + { nmos::is04_versions::v1_1, { + { U("description") }, + { U("tags") }, + { U("api") }, + { U("clocks") } + } }, + { nmos::is04_versions::v1_2, { + { U("interfaces"), value::array() } + } }, + { nmos::is04_versions::v1_3, { + { U("services"), value::array() }, + { U("interfaces"), value::array() } + } } } }, { nmos::types::device, { - { nmos::is04_versions::v1_0, { U("id"), U("version"), U("label"), U("type"), U("node_id"), U("senders"), U("receivers") } }, - { nmos::is04_versions::v1_1, { U("description"), U("tags"), U("controls") } } + { nmos::is04_versions::v1_0, { + { U("id") }, + { U("version") }, + { U("label") }, + { U("type"), value::string(U("urn:x-nmos:device:generic")) }, + { U("node_id") }, + { U("senders") }, + { U("receivers") } + } }, + { nmos::is04_versions::v1_1, { + { U("type"), value::string(U("urn:x-nmos:device:generic")) }, + { U("description") }, + { U("tags") }, + { U("controls"), value::array() } + } }, + { nmos::is04_versions::v1_3, { + { U("type"), value::string(U("urn:x-nmos:device:generic")) }, + { U("controls"), value::array() } + } }, } }, { nmos::types::source, { - { nmos::is04_versions::v1_0, { U("id"), U("version"), U("label"), U("description"), U("format"), U("caps"), U("tags"), U("device_id"), U("parents") } }, - { nmos::is04_versions::v1_1, { U("grain_rate"), U("clock_name"), U("channels") } }, - { nmos::is04_versions::v1_3, { U("event_type") } } + { nmos::is04_versions::v1_0, { + { U("id") }, + { U("version") }, + { U("label") }, + { U("description") }, + { U("format"), value::string(U("urn:x-nmos:format:data")) }, + { U("caps") }, + { U("tags") }, + { U("device_id") }, + { U("parents") } + } }, + { nmos::is04_versions::v1_1, { + { U("format"), value::string(U("urn:x-nmos:format:video")) }, + { U("grain_rate"), value_of({ + { nmos::fields::numerator, 0 }, + { nmos::fields::denominator, 1 } }), false }, + { U("clock_name") }, + { U("channels") } + } }, + { nmos::is04_versions::v1_3, { + { U("event_type") } } } } }, { nmos::types::flow, { - { nmos::is04_versions::v1_0, { U("id"), U("version"), U("label"), U("description"), U("format"), U("tags"), U("source_id"), U("parents") } }, - { nmos::is04_versions::v1_1, { U("grain_rate"), U("device_id"), U("media_type"), U("sample_rate"), U("bit_depth"), U("DID_SDID"), U("frame_width"), U("frame_height"), U("interlace_mode"), U("colorspace"), U("transfer_characteristic"), U("components") } }, - { nmos::is04_versions::v1_3, { U("event_type") } } + { nmos::is04_versions::v1_0, { + { U("id") }, + { U("version") }, + { U("label") }, + { U("description") }, + { U("format"), value::string(U("urn:x-nmos:format:data")) }, + { U("tags") }, + { U("source_id") }, + { U("parents") } + } }, + { nmos::is04_versions::v1_1, { + { U("format"), value::string(U("urn:x-nmos:format:video")) }, + { U("grain_rate") }, + { U("device_id") }, + { U("media_type") }, + { U("sample_rate") }, + { U("bit_depth") }, + { U("DID_SDID") }, + { U("frame_width") }, + { U("frame_height") }, + { U("interlace_mode") }, + { U("colorspace"), value::string(U("BT709")) }, + { U("transfer_characteristic"), value::string(U("SDR")), false }, + { U("components") } + } }, + { nmos::is04_versions::v1_3, { + { U("colorspace"), value::string(U("BT709")) }, + { U("transfer_characteristic"), value::string(U("SDR")), false }, + { U("event_type") } + } } } }, { nmos::types::sender, { - { nmos::is04_versions::v1_0, { U("id"), U("version"), U("label"), U("description"), U("flow_id"), U("transport"), U("tags"), U("device_id"), U("manifest_href") } }, - { nmos::is04_versions::v1_2, { U("caps"), U("interface_bindings"), U("subscription") } } + { nmos::is04_versions::v1_0, { + { U("id") }, + { U("version") }, + { U("label") }, + { U("description") }, + { U("flow_id") }, + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, + { U("tags"), value::array(), false }, + { U("device_id") }, + { U("manifest_href") } + } }, + { nmos::is04_versions::v1_1, { + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, + { U("tags"), value::array() } + } }, + { nmos::is04_versions::v1_2, { + { U("caps"), value::object(), false }, + { U("interface_bindings"), value::array() }, + { U("subscription"), value_of({ + { nmos::fields::active, value::boolean(true) }, + { nmos::fields::receiver_id, value::null() } }) } + } }, + { nmos::is04_versions::v1_3, { + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) } + } } } }, { nmos::types::receiver, { - { nmos::is04_versions::v1_0, { U("id"), U("version"), U("label"), U("description"), U("format"), U("caps"), U("tags"), U("device_id"), U("transport"), U("subscription") } }, - { nmos::is04_versions::v1_2, { U("interface_bindings") } } + { nmos::is04_versions::v1_0, { + { U("id") }, + { U("version") }, + { U("label") }, + { U("description") }, + { U("format"), value::string(U("urn:x-nmos:format:data")) }, + { U("caps"), value::object() }, + { U("tags") }, + { U("device_id") }, + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, + { U("subscription"), value_of({ + { nmos::fields::sender_id, value::null() } }) } + } }, + { nmos::is04_versions::v1_1, { + { U("format"), value::string(U("urn:x-nmos:format:video")) }, + { U("caps"), value_of({ + { nmos::fields::media_types, value_of({ + { U("video/raw") } }) }}) + }, + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, + { U("subscription"), value_of({ + { nmos::fields::sender_id, value::null() } }) } + } }, + { nmos::is04_versions::v1_2, { {U("interface_bindings")}, + { U("subscription"), value_of({ + { nmos::fields::active, value::boolean(false) }, + { nmos::fields::sender_id, value::null() } }) } + } } } }, { nmos::types::subscription, { - { nmos::is04_versions::v1_0, { U("id"), U("ws_href"), U("max_update_rate_ms"), U("persist"), U("resource_path"), U("params") } }, - { nmos::is04_versions::v1_1, { U("secure") } }, - { nmos::is04_versions::v1_3, { U("authorization") } } + { nmos::is04_versions::v1_0, { {U("id")}, {U("ws_href")}, {U("max_update_rate_ms")}, {U("persist")}, {U("resource_path")}, {U("params")} } }, + { nmos::is04_versions::v1_1, { {U("secure")} } }, + { nmos::is04_versions::v1_3, { {U("authorization")} } } } } }; + return resources_versions; } web::json::value downgrade(const nmos::api_version& resource_version, const nmos::api_version& resource_downgrade_version, const nmos::type& resource_type, const web::json::value& resource_data, const nmos::api_version& version, const nmos::api_version& downgrade_version) { + using web::json::value; + using web::json::value_of; + if (!is_permitted_downgrade(resource_version, resource_downgrade_version, resource_type, version, downgrade_version)) return web::json::value::null(); // optimisation for no resource data (special case) @@ -139,7 +1251,7 @@ namespace nmos // optimisation for the common case (old-versioned resources, if being permitted, do not get upgraded) if (resource_version <= version) return resource_data; - web::json::value result; + value result; // This is a simple representation of the backwards-compatible changes that have been made between minor versions // of the specification. It just describes in which version each top-level property of each resource type was added. @@ -154,15 +1266,48 @@ namespace nmos // See https://github.com/AMWA-TV/is-04/pull/109/files#diff-251d9acc57a6ffaeed673153c6409f5f auto& resource_versions = resources_versions().at(resource_type); + auto version_first = resource_versions.cbegin(); auto version_last = resource_versions.upper_bound(version); for (auto version_properties = version_first; version_last != version_properties; ++version_properties) { for (auto& property : version_properties->second) { - if (resource_data.has_field(property)) + if (resource_data.has_field(property.name)) { - result[property] = resource_data.at(property); + // do schema validation on their property which has schema associated to it + const auto& schema_id = details::make_schema_id(version_properties->first, resource_type, property.name); + if (details::property_schemas.end() != details::property_schemas.find(schema_id)) + { + auto& value = resource_data.at(property.name); + + // validate property + try + { + details::property_changed_validator().validate(value_of({ { property.name, value } }), details::make_schema_id(version_properties->first, resource_type, property.name)); + result[property.name] = value; + } + catch(...) + { + // if property required, i.e. obligatory + if (property.required) + { + result[property.name] = property.default; + } + else + { + // ensure the optional field is removed, if it was set + if (result.has_field(property.name)) + { + result.erase(property.name); + } + } + } + } + else + { + result[property.name] = resource_data.at(property.name); + } } } } From 38f4ef91e479286860219c5b538be12d9d75c1b2 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Thu, 7 Sep 2023 01:51:49 +0100 Subject: [PATCH 2/6] Fix linux comiple error, unqualified-id --- Development/nmos/api_downgrade.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Development/nmos/api_downgrade.cpp b/Development/nmos/api_downgrade.cpp index 5f80ad67e..47ecd779e 100644 --- a/Development/nmos/api_downgrade.cpp +++ b/Development/nmos/api_downgrade.cpp @@ -1033,12 +1033,12 @@ namespace nmos struct property { utility::string_t name; - web::json::value default; + web::json::value default_value; bool required; // is required property - property(utility::string_t name, web::json::value default = web::json::value::null(), bool required = true) + property(utility::string_t name, web::json::value default_value = web::json::value::null(), bool required = true) : name(std::move(name)) - , default(std::move(default)) + , default_value(std::move(default_value)) , required(std::move(required)) {} }; @@ -1292,7 +1292,7 @@ namespace nmos // if property required, i.e. obligatory if (property.required) { - result[property.name] = property.default; + result[property.name] = property.default_value; } else { From 0d30ba1fc80b9c5dabed942cf288a92f3e17c079 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 Jan 2024 12:22:00 +0000 Subject: [PATCH 3/6] Add IS-04 Node, Device, Source, Flow, Sender and Receiver schemas --- Development/nmos/json_schema.cpp | 72 ++++++++++++++++++++++++++++++++ Development/nmos/json_schema.h | 7 ++++ 2 files changed, 79 insertions(+) diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index 2a5bb89ae..d02c73046 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -30,6 +30,12 @@ namespace nmos const web::uri registrationapi_resource_post_request_uri = make_schema_uri(tag, _XPLATSTR("registrationapi-resource-post-request.json")); const web::uri queryapi_subscriptions_post_request_uri = make_schema_uri(tag, _XPLATSTR("queryapi-subscriptions-post-request.json")); const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); + const web::uri node_uri = make_schema_uri(tag, _XPLATSTR("node.json")); + const web::uri device_uri = make_schema_uri(tag, _XPLATSTR("device.json")); + const web::uri source_uri = make_schema_uri(tag, _XPLATSTR("source.json")); + const web::uri flow_uri = make_schema_uri(tag, _XPLATSTR("flow.json")); + const web::uri sender_uri = make_schema_uri(tag, _XPLATSTR("sender.json")); + const web::uri receiver_uri = make_schema_uri(tag, _XPLATSTR("receiver.json")); } // See https://github.com/AMWA-TV/is-04/blob/v1.2.x/APIs/schemas/ @@ -41,6 +47,12 @@ namespace nmos const web::uri registrationapi_resource_post_request_uri = make_schema_uri(tag, _XPLATSTR("registrationapi-resource-post-request.json")); const web::uri queryapi_subscriptions_post_request_uri = make_schema_uri(tag, _XPLATSTR("queryapi-subscriptions-post-request.json")); const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); + const web::uri node_uri = make_schema_uri(tag, _XPLATSTR("node.json")); + const web::uri device_uri = make_schema_uri(tag, _XPLATSTR("device.json")); + const web::uri source_uri = make_schema_uri(tag, _XPLATSTR("source.json")); + const web::uri flow_uri = make_schema_uri(tag, _XPLATSTR("flow.json")); + const web::uri sender_uri = make_schema_uri(tag, _XPLATSTR("sender.json")); + const web::uri receiver_uri = make_schema_uri(tag, _XPLATSTR("receiver.json")); } // See https://github.com/AMWA-TV/is-04/blob/v1.1.x/APIs/schemas/ @@ -52,6 +64,12 @@ namespace nmos const web::uri registrationapi_resource_post_request_uri = make_schema_uri(tag, _XPLATSTR("registrationapi-resource-post-request.json")); const web::uri queryapi_subscriptions_post_request_uri = make_schema_uri(tag, _XPLATSTR("queryapi-subscriptions-post-request.json")); const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); + const web::uri node_uri = make_schema_uri(tag, _XPLATSTR("node.json")); + const web::uri device_uri = make_schema_uri(tag, _XPLATSTR("device.json")); + const web::uri source_uri = make_schema_uri(tag, _XPLATSTR("source.json")); + const web::uri flow_uri = make_schema_uri(tag, _XPLATSTR("flow.json")); + const web::uri sender_uri = make_schema_uri(tag, _XPLATSTR("sender.json")); + const web::uri receiver_uri = make_schema_uri(tag, _XPLATSTR("receiver.json")); } // See https://github.com/AMWA-TV/is-04/blob/v1.0.x/APIs/schemas/ @@ -63,6 +81,12 @@ namespace nmos const web::uri registrationapi_resource_post_request_uri = make_schema_uri(tag, _XPLATSTR("registrationapi-v1.0-resource-post-request.json")); const web::uri queryapi_subscriptions_post_request_uri = make_schema_uri(tag, _XPLATSTR("queryapi-v1.0-subscriptions-post-request.json")); const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); + const web::uri node_uri = make_schema_uri(tag, _XPLATSTR("node.json")); + const web::uri device_uri = make_schema_uri(tag, _XPLATSTR("device.json")); + const web::uri source_uri = make_schema_uri(tag, _XPLATSTR("source.json")); + const web::uri flow_uri = make_schema_uri(tag, _XPLATSTR("flow.json")); + const web::uri sender_uri = make_schema_uri(tag, _XPLATSTR("sender.json")); + const web::uri receiver_uri = make_schema_uri(tag, _XPLATSTR("receiver.json")); } } @@ -400,6 +424,54 @@ namespace nmos return is04_schemas::v1_0::nodeapi_receiver_target_put_request_uri; } + web::uri make_node_schema_uri(const nmos::api_version& version) + { + if (is04_versions::v1_3 <= version) return is04_schemas::v1_3::node_uri; + if (is04_versions::v1_2 <= version) return is04_schemas::v1_2::node_uri; + if (is04_versions::v1_1 <= version) return is04_schemas::v1_1::node_uri; + return is04_schemas::v1_0::node_uri; + } + + web::uri make_device_schema_uri(const nmos::api_version& version) + { + if (is04_versions::v1_3 <= version) return is04_schemas::v1_3::device_uri; + if (is04_versions::v1_2 <= version) return is04_schemas::v1_2::device_uri; + if (is04_versions::v1_1 <= version) return is04_schemas::v1_1::device_uri; + return is04_schemas::v1_0::device_uri; + } + + web::uri make_source_schema_uri(const nmos::api_version& version) + { + if (is04_versions::v1_3 <= version) return is04_schemas::v1_3::source_uri; + if (is04_versions::v1_2 <= version) return is04_schemas::v1_2::source_uri; + if (is04_versions::v1_1 <= version) return is04_schemas::v1_1::source_uri; + return is04_schemas::v1_0::source_uri; + } + + web::uri make_flow_schema_uri(const nmos::api_version& version) + { + if (is04_versions::v1_3 <= version) return is04_schemas::v1_3::flow_uri; + if (is04_versions::v1_2 <= version) return is04_schemas::v1_2::flow_uri; + if (is04_versions::v1_1 <= version) return is04_schemas::v1_1::flow_uri; + return is04_schemas::v1_0::flow_uri; + } + + web::uri make_sender_schema_uri(const nmos::api_version& version) + { + if (is04_versions::v1_3 <= version) return is04_schemas::v1_3::sender_uri; + if (is04_versions::v1_2 <= version) return is04_schemas::v1_2::sender_uri; + if (is04_versions::v1_1 <= version) return is04_schemas::v1_1::sender_uri; + return is04_schemas::v1_0::sender_uri; + } + + web::uri make_receiver_schema_uri(const nmos::api_version& version) + { + if (is04_versions::v1_3 <= version) return is04_schemas::v1_3::receiver_uri; + if (is04_versions::v1_2 <= version) return is04_schemas::v1_2::receiver_uri; + if (is04_versions::v1_1 <= version) return is04_schemas::v1_1::receiver_uri; + return is04_schemas::v1_0::receiver_uri; + } + web::uri make_connectionapi_staged_patch_request_schema_uri(const nmos::api_version& version, const nmos::type& type) { return nmos::types::sender == type diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index 4c8c7b60a..b05dbd1c3 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -23,6 +23,13 @@ namespace nmos web::uri make_nodeapi_receiver_target_put_request_schema_uri(const nmos::api_version& version); + web::uri make_node_schema_uri(const nmos::api_version& version); + web::uri make_device_schema_uri(const nmos::api_version& version); + web::uri make_source_schema_uri(const nmos::api_version& version); + web::uri make_flow_schema_uri(const nmos::api_version& version); + web::uri make_sender_schema_uri(const nmos::api_version& version); + web::uri make_receiver_schema_uri(const nmos::api_version& version); + web::uri make_connectionapi_staged_patch_request_schema_uri(const nmos::api_version& version, const nmos::type& type); web::uri make_connectionapi_sender_staged_patch_request_schema_uri(const nmos::api_version& version); web::uri make_connectionapi_receiver_staged_patch_request_schema_uri(const nmos::api_version& version); From eeaa19502cb820aeb637daee7dcc3e62906203b5 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 Jan 2024 12:26:50 +0000 Subject: [PATCH 4/6] Use of bst::optional for the optional properties --- Development/nmos/api_downgrade.cpp | 82 +++++++++++++++++------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/Development/nmos/api_downgrade.cpp b/Development/nmos/api_downgrade.cpp index 47ecd779e..f516686f8 100644 --- a/Development/nmos/api_downgrade.cpp +++ b/Development/nmos/api_downgrade.cpp @@ -1033,13 +1033,11 @@ namespace nmos struct property { utility::string_t name; - web::json::value default_value; - bool required; // is required property + bst::optional default_value; - property(utility::string_t name, web::json::value default_value = web::json::value::null(), bool required = true) + property(utility::string_t name, bst::optional default_value = web::json::value::null()) : name(std::move(name)) , default_value(std::move(default_value)) - , required(std::move(required)) {} }; @@ -1048,6 +1046,13 @@ namespace nmos using web::json::value; using web::json::value_of; + // The downgrade function uses the following map to identify which properties are required on the relevant version + // + // 1. work from the minimum version to the required downgrade version + // 2. find the property in the resource + // 3. do property schema validation on schema changed properties + // 4. if schema validation failed, remove optional property or set required property to default + // 5. if all okay, copy the resource property to the downgrade result static const std::map>> resources_versions { { @@ -1072,8 +1077,8 @@ namespace nmos { U("interfaces"), value::array() } } }, { nmos::is04_versions::v1_3, { - { U("services"), value::array() }, - { U("interfaces"), value::array() } + { U("services"), value::array() }, // schema changed + { U("interfaces"), value::array() } // schema changed } } } }, @@ -1090,14 +1095,14 @@ namespace nmos { U("receivers") } } }, { nmos::is04_versions::v1_1, { - { U("type"), value::string(U("urn:x-nmos:device:generic")) }, + { U("type"), value::string(U("urn:x-nmos:device:generic")) }, // schema changed { U("description") }, { U("tags") }, { U("controls"), value::array() } } }, { nmos::is04_versions::v1_3, { - { U("type"), value::string(U("urn:x-nmos:device:generic")) }, - { U("controls"), value::array() } + { U("type"), value::string(U("urn:x-nmos:device:generic")) }, // schema changed + { U("controls"), value::array() } // schema changed } }, } }, @@ -1116,15 +1121,18 @@ namespace nmos { U("parents") } } }, { nmos::is04_versions::v1_1, { - { U("format"), value::string(U("urn:x-nmos:format:video")) }, - { U("grain_rate"), value_of({ - { nmos::fields::numerator, 0 }, - { nmos::fields::denominator, 1 } }), false }, + { U("format"), value::string(U("urn:x-nmos:format:video")) }, // schema chnaged + { U("grain_rate"), bst::nullopt }, // optional { U("clock_name") }, - { U("channels") } + { U("channels"), value_of({ + value_of({ + {U("label"), U("")} + }) + }) } } }, { nmos::is04_versions::v1_3, { - { U("event_type") } } } + { U("event_type"), bst::nullopt } // optional + } } } }, { @@ -1141,7 +1149,7 @@ namespace nmos { U("parents") } } }, { nmos::is04_versions::v1_1, { - { U("format"), value::string(U("urn:x-nmos:format:video")) }, + { U("format"), value::string(U("urn:x-nmos:format:video")) }, // schema changed { U("grain_rate") }, { U("device_id") }, { U("media_type") }, @@ -1152,13 +1160,13 @@ namespace nmos { U("frame_height") }, { U("interlace_mode") }, { U("colorspace"), value::string(U("BT709")) }, - { U("transfer_characteristic"), value::string(U("SDR")), false }, + { U("transfer_characteristic"), bst::nullopt }, // optional { U("components") } } }, { nmos::is04_versions::v1_3, { - { U("colorspace"), value::string(U("BT709")) }, - { U("transfer_characteristic"), value::string(U("SDR")), false }, - { U("event_type") } + { U("colorspace"), value::string(U("BT709")) }, // schema changed + { U("transfer_characteristic"), bst::nullopt }, // schema changed, field optional + { U("event_type"), bst::nullopt } // optional } } } }, @@ -1172,23 +1180,23 @@ namespace nmos { U("description") }, { U("flow_id") }, { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, - { U("tags"), value::array(), false }, + { U("tags"), bst::nullopt }, // optional { U("device_id") }, { U("manifest_href") } } }, { nmos::is04_versions::v1_1, { - { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, - { U("tags"), value::array() } + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, // schema changed + { U("tags"), value::array() } // schema changed, field required } }, { nmos::is04_versions::v1_2, { - { U("caps"), value::object(), false }, + { U("caps"), bst::nullopt }, // optional { U("interface_bindings"), value::array() }, { U("subscription"), value_of({ { nmos::fields::active, value::boolean(true) }, { nmos::fields::receiver_id, value::null() } }) } } }, { nmos::is04_versions::v1_3, { - { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) } + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) } // schema changed } } } }, @@ -1209,17 +1217,18 @@ namespace nmos { nmos::fields::sender_id, value::null() } }) } } }, { nmos::is04_versions::v1_1, { - { U("format"), value::string(U("urn:x-nmos:format:video")) }, - { U("caps"), value_of({ + { U("format"), value::string(U("urn:x-nmos:format:video")) }, // schema changed + { U("caps"), value_of({ // schema changed, select one of the media types (video/audio/data/mux) for the default { nmos::fields::media_types, value_of({ { U("video/raw") } }) }}) }, - { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, - { U("subscription"), value_of({ + { U("transport"), value::string(U("urn:x-nmos:transport:rtp")) }, // schema changed + { U("subscription"), value_of({ // schema changed { nmos::fields::sender_id, value::null() } }) } } }, - { nmos::is04_versions::v1_2, { {U("interface_bindings")}, - { U("subscription"), value_of({ + { nmos::is04_versions::v1_2, { + { U("interface_bindings") }, + { U("subscription"), value_of({ // schema changed { nmos::fields::active, value::boolean(false) }, { nmos::fields::sender_id, value::null() } }) } } } @@ -1264,6 +1273,8 @@ namespace nmos // Further examples of this are the proposed addition in v1.3 of "attached_network_device" in the "interfaces" // sub-objects of a node and of "authorization" in node "api.endpoints" and "services" and device "controls". // See https://github.com/AMWA-TV/is-04/pull/109/files#diff-251d9acc57a6ffaeed673153c6409f5f + // Further examples of this are the enhancement of "colorspace" and "transfer_characteristic" in the video flow + // See https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/flow_video.html auto& resource_versions = resources_versions().at(resource_type); @@ -1275,7 +1286,7 @@ namespace nmos { if (resource_data.has_field(property.name)) { - // do schema validation on their property which has schema associated to it + // do schema validation on those properties which have schema associated with them const auto& schema_id = details::make_schema_id(version_properties->first, resource_type, property.name); if (details::property_schemas.end() != details::property_schemas.find(schema_id)) { @@ -1289,10 +1300,10 @@ namespace nmos } catch(...) { - // if property required, i.e. obligatory - if (property.required) + // is the property required + if (property.default_value) { - result[property.name] = property.default_value; + result[property.name] = *property.default_value; } else { @@ -1306,6 +1317,7 @@ namespace nmos } else { + // no schema validation required, just copy the resource property to the result result[property.name] = resource_data.at(property.name); } } From 079b4a8ab7f347643df2916ef64cebe4afe8ab82 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 Jan 2024 12:28:43 +0000 Subject: [PATCH 5/6] Add downgrade unit tests --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/test/api_downgrade_test.cpp | 249 +++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 Development/nmos/test/api_downgrade_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 22d6e6214..eb95982aa 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -40,6 +40,7 @@ set(NMOS_CPP_TEST_MDNS_TEST_HEADERS ) set(NMOS_CPP_TEST_NMOS_TEST_SOURCES + nmos/test/api_downgrade_test.cpp nmos/test/api_utils_test.cpp nmos/test/capabilities_test.cpp nmos/test/channels_test.cpp diff --git a/Development/nmos/test/api_downgrade_test.cpp b/Development/nmos/test/api_downgrade_test.cpp new file mode 100644 index 000000000..0af298e3a --- /dev/null +++ b/Development/nmos/test/api_downgrade_test.cpp @@ -0,0 +1,249 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/api_downgrade.h" + +#include +#include +#include "bst/test/test.h" +#include "cpprest/json_validator.h" // for web::json::experimental::json_validator +#include "nmos/clock_name.h" // for nmos::make_internal_clock +#include "nmos/channels.h" // for nmos::channel +#include "nmos/colorspace.h" // for nmos::colorspace +#include "nmos/components.h" // for nmos::chroma_subsampling +#include "nmos/interlace_mode.h" // for nmos::interlace_mode +#include "nmos/is04_versions.h" // for nmos::is04_versions +#include "nmos/json_schema.h" // for nmos::experimental::load_json_schema +#include "nmos/format.h" // for nmos::format +#include "nmos/node_resource.h" // for nmos::make_node +#include "nmos/node_resources.h" // for nmos::make_device +#include "nmos/resource.h" // for nmos::resource +#include "nmos/transfer_characteristic.h" // for nmos::transfer_characteristic +#include "nmos/transport.h" // for nmos::transport +#include "nmos/video_jxsv.h" // for nmos::profile +#include "sdp/json.h" // for sdp::sampling + +namespace +{ + using web::json::value_of; + + const auto lower_version = nmos::is04_versions::all.cbegin(); + const auto higher_version = --nmos::is04_versions::all.end(); + + const auto node_id = nmos::make_id(); + const auto device_id = nmos::make_id(); + const auto source_id = nmos::make_id(); + const auto flow_id = nmos::make_id(); + const auto sender_id = nmos::make_id(); + const auto receiver_id = nmos::make_id(); + const auto sender_ids = { sender_id }; + const auto receiver_ids = { receiver_id }; +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testNodeDowngrade) +{ + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_node_schema_uri)) + }; + + const auto settings = value_of({ + {U("label"), U("Test node")}, + {U("description"), U("This is a test node")}, + {U("host_name"), U("127.0.0.1")} + }); + const auto clocks = value_of({ + nmos::make_internal_clock(nmos::clock_names::clk0) + }); + const auto interfaces = value_of({ + value_of({ + {U("name"), U("{0BAFC19E-7B18-4C51-A7B1-9670E0B1CFE1}")}, + {U("chassis_id"), U("3c-52-82-6f-c2-3e")}, + {U("port_id"), U("3c-52-82-6f-c2-3e")} + }, true) + }); + auto resource = nmos::make_node(node_id, clocks, interfaces, settings); + + // tesing downgrade on all supported versions from the lower version to 1 less than the highest version (the current resource version) + for (auto version = lower_version; higher_version != version; ++version) + { + const auto& downgraded = nmos::downgrade(resource, *version); + validator.validate(downgraded, nmos::experimental::make_node_schema_uri(*version)); + BST_REQUIRE(true); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testDeviceDowngrade) +{ + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_device_schema_uri)) + }; + + const auto settings = value_of({ + {U("label"), U("Test device")}, + {U("description"), U("This is a test device")}, + {U("host_name"), U("127.0.0.1")} + }); + + auto resource = nmos::make_device(device_id, node_id, sender_ids, receiver_ids, settings); + + // tesing downgrade on all supported versions from the lower version to 1 less than the highest version (the current resource version) + for (auto version = lower_version; higher_version != version; ++version) + { + const auto& downgraded = nmos::downgrade(resource, *version); + validator.validate(downgraded, nmos::experimental::make_device_schema_uri(*version)); + BST_REQUIRE(true); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSourceDowngrade) +{ + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_source_schema_uri)) + }; + + const auto settings = value_of({ + {U("label"), U("Test source")}, + {U("description"), U("This is a test source")} + }); + + // list of different sources to test + std::vector sources = + { + nmos::make_video_source(source_id, device_id, nmos::clock_names::clk0, { 25, 1 }, settings), + nmos::make_audio_source(source_id, device_id, nmos::clock_names::clk0, { 25, 1 }, { {U("Mono"), nmos::channel_symbols::M1} }, settings), + nmos::make_data_source(source_id, device_id, nmos::clock_names::clk0, { 25, 1 }, settings), + nmos::make_mux_source(source_id, device_id, nmos::clock_names::clk0, { 25, 1 }, settings) + }; + + for (const auto& resource : sources) + { + // tesing downgrade on all supported versions from the lower version to 1 less than the highest version (the current resource version) + for (auto version = lower_version; higher_version != version; ++version) + { + const auto& downgraded = nmos::downgrade(resource, *version); + validator.validate(downgraded, nmos::experimental::make_source_schema_uri(*version)); + BST_REQUIRE(true); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testFlowDowngrade) +{ + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_flow_schema_uri)) + }; + + const auto settings = value_of({ + {U("label"), U("Test flow")}, + {U("description"), U("This is a test flow")} + }); + // test UNSPECIFIED using in transfer characteristic, it can only be used from v1.3 flow + const auto unspecified_transfer_characteristic = nmos::transfer_characteristic{ U("UNSPECIFIED")}; + + // list of different flows to test + std::vector flows = + { + nmos::make_raw_video_flow(flow_id, source_id, device_id, { 25, 1 }, 1920, 1080, nmos::interlace_modes::interlaced_tff, nmos::colorspaces::BT709, unspecified_transfer_characteristic, nmos::YCbCr422, 10, settings), + nmos::make_raw_video_flow(flow_id, source_id, device_id, { 25, 1 }, 1920, 1080, nmos::interlace_modes::interlaced_tff, nmos::colorspaces::BT709, nmos::transfer_characteristics::SDR, nmos::YCbCr422, 10, settings), + nmos::make_video_jxsv_flow(flow_id, source_id, device_id,{ 25, 1 }, 1920, 1080, nmos::interlace_modes::interlaced_tff,nmos::colorspaces::BT709, unspecified_transfer_characteristic, sdp::samplings::YCbCr_4_2_2, 10, nmos::profiles::High444_12, nmos::levels::Level1k_1, nmos::sublevels::Sublev3bpp, 2.0, settings), + nmos::make_coded_video_flow(flow_id, source_id, device_id,{ 25, 1 }, 1920, 1080, nmos::interlace_modes::interlaced_tff,nmos::colorspaces::BT709, unspecified_transfer_characteristic, sdp::samplings::YCbCr_4_2_2, 10, nmos::media_types::video_raw, settings), + nmos::make_raw_audio_flow(flow_id, source_id, device_id, 48000, 24, settings), + nmos::make_sdianc_data_flow(flow_id, source_id, device_id, { 0x60, 0x60 }, settings), + nmos::make_mux_flow(flow_id, source_id, device_id, settings) + }; + + for (const auto& resource : flows) + { + // tesing downgrade on all supported versions from the lower version to 1 less than the highest version (the current resource version) + for (auto version = lower_version; higher_version != version; ++version) + { + const auto& downgraded = nmos::downgrade(resource, *version); + validator.validate(downgraded, nmos::experimental::make_flow_schema_uri(*version)); + BST_REQUIRE(true); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSenderDowngrade) +{ + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_sender_schema_uri)) + }; + + const auto settings = value_of({ + {U("label"), U("Left Channel")}, + {U("description"), U("This is a test sender")} + }); + const std::vector interfaces = { U("{0BAFC19E-7B18-4C51-A7B1-9670E0B1CFE1}") }; + const nmos::transport rtp_transport{ nmos::transports::rtp }; + + // test sender downgrade + const auto manifest_href = nmos::experimental::make_manifest_api_manifest(sender_id, settings); + + // list of different downgradable senders to test + std::vector senders = + { + nmos::make_sender(sender_id, flow_id, rtp_transport, device_id, manifest_href.to_string(), interfaces, settings), + }; + + for (const auto& resource : senders) + { + // tesing downgrade on all supported versions from the lower version to 1 less than the highest version (the current resource version) + for (auto version = lower_version; higher_version != version; ++version) + { + const auto& downgraded = nmos::downgrade(resource, *version); + validator.validate(downgraded, nmos::experimental::make_sender_schema_uri(*version)); + BST_REQUIRE(true); + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testReceiverDowngrade) +{ + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_receiver_schema_uri)) + }; + + const auto settings = value_of({ + {U("label"), U("Test receiver")}, + {U("description"), U("This is a test receiver")} + }); + const nmos::transport transport{ nmos::transports::rtp }; + const std::vector interfaces = { U("{0BAFC19E-7B18-4C51-A7B1-9670E0B1CFE1}") }; + + // list of different receivers to test + std::vector receivers = + { + nmos::make_receiver(receiver_id, device_id, transport, interfaces, nmos::formats::video, { nmos::media_types::video_raw }, settings), + nmos::make_audio_receiver(receiver_id, device_id, transport, interfaces, 24, settings), + nmos::make_sdianc_data_receiver(receiver_id, device_id, transport, interfaces, settings), + nmos::make_mux_receiver(receiver_id, device_id, transport, interfaces, settings) + }; + + for (const auto& resource : receivers) + { + // tesing downgrade on all supported versions from the lower version to 1 less than the highest version (the current resource version) + for (auto version = lower_version; higher_version != version; ++version) + { + const auto& downgraded = nmos::downgrade(resource, *version); + validator.validate(downgraded, nmos::experimental::make_receiver_schema_uri(*version)); + BST_REQUIRE(true); + } + } +} From e217d711048e915ef42bc9592fd617ddc50dc984 Mon Sep 17 00:00:00 2001 From: lo-simon Date: Wed, 24 Jan 2024 13:03:12 +0000 Subject: [PATCH 6/6] Fix Ubuntu14.04 build using GCC 4.8.4 --- Development/nmos/test/api_downgrade_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Development/nmos/test/api_downgrade_test.cpp b/Development/nmos/test/api_downgrade_test.cpp index 0af298e3a..eb2c53ae8 100644 --- a/Development/nmos/test/api_downgrade_test.cpp +++ b/Development/nmos/test/api_downgrade_test.cpp @@ -34,8 +34,8 @@ namespace const auto flow_id = nmos::make_id(); const auto sender_id = nmos::make_id(); const auto receiver_id = nmos::make_id(); - const auto sender_ids = { sender_id }; - const auto receiver_ids = { receiver_id }; + const std::vector sender_ids = { sender_id }; + const std::vector receiver_ids = { receiver_id }; } ////////////////////////////////////////////////////////////////////////////////////////////