diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 22d6e621..eb95982a 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/api_downgrade.cpp b/Development/nmos/api_downgrade.cpp index 1047d78f..f516686f 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,228 @@ 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; + bst::optional default_value; + + property(utility::string_t name, bst::optional default_value = web::json::value::null()) + : name(std::move(name)) + , default_value(std::move(default_value)) + {} + }; + + static const std::map>>& resources_versions() { - static const std::map>> resources_versions + 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 { { 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() }, // schema changed + { U("interfaces"), value::array() } // schema changed + } } } }, { 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")) }, // 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")) }, // schema changed + { U("controls"), value::array() } // schema changed + } }, } }, { 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")) }, // schema chnaged + { U("grain_rate"), bst::nullopt }, // optional + { U("clock_name") }, + { U("channels"), value_of({ + value_of({ + {U("label"), U("")} + }) + }) } + } }, + { nmos::is04_versions::v1_3, { + { U("event_type"), bst::nullopt } // optional + } } } }, { 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")) }, // schema changed + { 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"), bst::nullopt }, // optional + { U("components") } + } }, + { nmos::is04_versions::v1_3, { + { U("colorspace"), value::string(U("BT709")) }, // schema changed + { U("transfer_characteristic"), bst::nullopt }, // schema changed, field optional + { U("event_type"), bst::nullopt } // optional + } } } }, { 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"), bst::nullopt }, // optional + { U("device_id") }, + { U("manifest_href") } + } }, + { nmos::is04_versions::v1_1, { + { 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"), 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")) } // schema changed + } } } }, { 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")) }, // 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")) }, // 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({ // schema changed + { 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 +1260,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. @@ -152,17 +1273,53 @@ 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); + 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 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)) + { + 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(...) + { + // is the property required + if (property.default_value) + { + result[property.name] = *property.default_value; + } + else + { + // ensure the optional field is removed, if it was set + if (result.has_field(property.name)) + { + result.erase(property.name); + } + } + } + } + else + { + // no schema validation required, just copy the resource property to the result + result[property.name] = resource_data.at(property.name); + } } } } diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index 2a5bb89a..d02c7304 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 4c8c7b60..b05dbd1c 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); diff --git a/Development/nmos/test/api_downgrade_test.cpp b/Development/nmos/test/api_downgrade_test.cpp new file mode 100644 index 00000000..eb2c53ae --- /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 std::vector sender_ids = { sender_id }; + const std::vector 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); + } + } +}