From 3b7f736605c2af5d35f3b2257206c91dfd6c3b79 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Thu, 11 May 2023 12:25:54 +0100 Subject: [PATCH 01/15] Initial draft NMOS Read/Write Node API implementation --- Development/cmake/NmosCppLibraries.cmake | 74 ++++++ Development/nmos-cpp-node/config.json | 3 + Development/nmos/api_utils.h | 2 + Development/nmos/is13_schemas/is13_schemas.h | 21 ++ Development/nmos/is13_versions.h | 26 ++ Development/nmos/json_schema.cpp | 36 +++ Development/nmos/json_schema.h | 2 + Development/nmos/node_server.cpp | 2 + Development/nmos/rwnode_api.cpp | 242 ++++++++++++++++++ Development/nmos/rwnode_api.h | 20 ++ Development/nmos/settings.h | 3 + Development/third_party/is-13/README.md | 8 + .../is-13/v1.0-dev/APIs/schemas/error.json | 27 ++ .../v1.0-dev/APIs/schemas/resource_core.json | 45 ++++ .../APIs/schemas/resource_core_patch.json | 28 ++ .../v1.0-dev/APIs/schemas/resource_cores.json | 10 + .../v1.0-dev/APIs/schemas/rwnodeapi-base.json | 20 ++ 17 files changed, 569 insertions(+) create mode 100644 Development/nmos/is13_schemas/is13_schemas.h create mode 100644 Development/nmos/is13_versions.h create mode 100644 Development/nmos/rwnode_api.cpp create mode 100644 Development/nmos/rwnode_api.h create mode 100644 Development/third_party/is-13/README.md create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/error.json create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core.json create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 944eb9902..5f266c032 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -686,6 +686,76 @@ target_include_directories(nmos_is09_schemas PUBLIC list(APPEND NMOS_CPP_TARGETS nmos_is09_schemas) add_library(nmos-cpp::nmos_is09_schemas ALIAS nmos_is09_schemas) +# nmos_is13_schemas library + +set(NMOS_IS13_SCHEMAS_HEADERS + nmos/is13_schemas/is13_schemas.h + ) + +set(NMOS_IS13_V1_0_TAG v1.0-dev) + +set(NMOS_IS13_V1_0_SCHEMAS_JSON + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/error.json + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_core.json + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_core_patch.json + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_cores.json + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/rwnodeapi-base.json + ) + +set(NMOS_IS13_SCHEMAS_JSON_MATCH "third_party/is-13/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS13_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is13_schemas/\\1/\\2.cpp") +string(REGEX REPLACE "${NMOS_IS13_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS13_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS13_V1_0_SCHEMAS_SOURCES "${NMOS_IS13_V1_0_SCHEMAS_JSON}") + +foreach(JSON ${NMOS_IS13_V1_0_SCHEMAS_JSON}) + string(REGEX REPLACE "${NMOS_IS13_SCHEMAS_JSON_MATCH}" "${NMOS_IS13_SCHEMAS_SOURCE_REPLACE}" SOURCE "${JSON}") + string(REGEX REPLACE "${NMOS_IS13_SCHEMAS_JSON_MATCH}" "\\1" NS "${JSON}") + string(REGEX REPLACE "${NMOS_IS13_SCHEMAS_JSON_MATCH}" "\\2" VAR "${JSON}") + string(MAKE_C_IDENTIFIER "${NS}" NS) + string(MAKE_C_IDENTIFIER "${VAR}" VAR) + + file(WRITE "${SOURCE}.in" "\ +// Auto-generated from: ${JSON}\n\ +\n\ +namespace nmos\n\ +{\n\ + namespace is13_schemas\n\ + {\n\ + namespace ${NS}\n\ + {\n\ + const char* ${VAR} = R\"-auto-generated-(") + + file(READ "${JSON}" RAW) + file(APPEND "${SOURCE}.in" "${RAW}") + + file(APPEND "${SOURCE}.in" ")-auto-generated-\";\n\ + }\n\ + }\n\ +}\n") + + configure_file("${SOURCE}.in" "${SOURCE}" COPYONLY) +endforeach() + +add_library( + nmos_is13_schemas STATIC + ${NMOS_IS13_SCHEMAS_HEADERS} + ${NMOS_IS13_V1_0_SCHEMAS_SOURCES} + ) + +source_group("nmos\\is13_schemas\\Header Files" FILES ${NMOS_IS13_SCHEMAS_HEADERS}) +source_group("nmos\\is13_schemas\\${NMOS_IS13_V1_0_TAG}\\Source Files" FILES ${NMOS_IS13_V1_0_SCHEMAS_SOURCES}) + +target_link_libraries( + nmos_is13_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is13_schemas PUBLIC + $ + $ + ) + +list(APPEND NMOS_CPP_TARGETS nmos_is13_schemas) +add_library(nmos-cpp::nmos_is13_schemas ALIAS nmos_is13_schemas) + # nmos-cpp library set(NMOS_CPP_BST_SOURCES @@ -794,6 +864,7 @@ set(NMOS_CPP_NMOS_SOURCES nmos/registry_server.cpp nmos/resource.cpp nmos/resources.cpp + nmos/rwnode_api.cpp nmos/schemas_api.cpp nmos/sdp_utils.cpp nmos/server.cpp @@ -847,6 +918,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/is07_versions.h nmos/is08_versions.h nmos/is09_versions.h + nmos/is13_versions.h nmos/json_fields.h nmos/json_schema.h nmos/lldp_handler.h @@ -886,6 +958,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/registry_server.h nmos/resource.h nmos/resources.h + nmos/rwnode_api.h nmos/schemas_api.h nmos/sdp_utils.h nmos/server.h @@ -985,6 +1058,7 @@ target_link_libraries( nmos-cpp::nmos_is05_schemas nmos-cpp::nmos_is08_schemas nmos-cpp::nmos_is09_schemas + nmos-cpp::nmos_is13_schemas nmos-cpp::mdns nmos-cpp::slog nmos-cpp::OpenSSL diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 5bf63a9ff..8ebe3740f 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -101,6 +101,9 @@ // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration //"is09_versions": ["v1.0"], + // is13_versions [node]: used to specify the enabled API versions for a version-locked configuration + //"is13_versions": ["v1.0"], + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely //"pri": 100, diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 0db8898bb..0b5b2d1ea 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -50,6 +50,8 @@ namespace nmos const route_pattern channelmapping_api = make_route_pattern(U("api"), U("channelmapping")); // IS-09 System API (originally specified in JT-NM TR-1001-1:2018 Annex A) const route_pattern system_api = make_route_pattern(U("api"), U("system")); + // IS-13 Read/Write Node API + const route_pattern rwnode_api = make_route_pattern(U("api"), U("rwnode")); // API version pattern const route_pattern version = make_route_pattern(U("version"), U("v[0-9]+\\.[0-9]+")); diff --git a/Development/nmos/is13_schemas/is13_schemas.h b/Development/nmos/is13_schemas/is13_schemas.h new file mode 100644 index 000000000..e6ff520f1 --- /dev/null +++ b/Development/nmos/is13_schemas/is13_schemas.h @@ -0,0 +1,21 @@ +#ifndef NMOS_IS13_SCHEMAS_H +#define NMOS_IS13_SCHEMAS_H + +// Extern declarations for auto-generated constants +// could be auto-generated, but isn't currently! +namespace nmos +{ + namespace is13_schemas + { + namespace v1_0_dev + { + extern const char* error; + extern const char* resource_core; + extern const char* resource_core_patch; + extern const char* resource_cores; + extern const char* rwnodeapi_base; + } + } +} + +#endif diff --git a/Development/nmos/is13_versions.h b/Development/nmos/is13_versions.h new file mode 100644 index 000000000..3c33a506c --- /dev/null +++ b/Development/nmos/is13_versions.h @@ -0,0 +1,26 @@ +#ifndef NMOS_IS13_VERSIONS_H +#define NMOS_IS13_VERSIONS_H + +#include +#include +#include "nmos/api_version.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace is13_versions + { + const api_version v1_0{ 1, 0 }; + + const std::set all{ nmos::is13_versions::v1_0 }; + + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::fields::is13_versions) + ? boost::copy_range>(nmos::fields::is04_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + : nmos::is13_versions::all; + } + } +} + +#endif diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index fdd70581d..405e6d245 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -9,6 +9,8 @@ #include "nmos/is08_schemas/is08_schemas.h" #include "nmos/is09_versions.h" #include "nmos/is09_schemas/is09_schemas.h" +#include "nmos/is13_versions.h" +#include "nmos/is13_schemas/is13_schemas.h" #include "nmos/type.h" namespace nmos @@ -126,6 +128,23 @@ namespace nmos const web::uri systemapi_global_schema_uri = make_schema_uri(tag, _XPLATSTR("global.json")); } } + + namespace is13_schemas + { + web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) + { + return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-rwnode/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + } + + // See https://github.com/AMWA-TV/nmos-rwnode/blob/v1.0-dev/APIs/schemas/ + namespace v1_0 + { + using namespace nmos::is13_schemas::v1_0_dev; + const utility::string_t tag(_XPLATSTR("v1.0-dev")); + + const web::uri rwnodeapi_resource_core_patch_request_uri = make_schema_uri(tag, _XPLATSTR("resource_core_patch.json")); + } + } } namespace nmos @@ -310,6 +329,17 @@ namespace nmos }; } + static std::map make_is13_schemas() + { + using namespace nmos::is13_schemas; + + return + { + // v1.0 + { make_schema_uri(v1_0::tag, _XPLATSTR("resource_core_patch.json")), make_schema(v1_0::resource_core_patch) } + }; + } + inline void merge(std::map& to, std::map&& from) { to.insert(from.begin(), from.end()); // std::map::merge in C++17 @@ -321,6 +351,7 @@ namespace nmos merge(result, make_is05_schemas()); merge(result, make_is08_schemas()); merge(result, make_is09_schemas()); + merge(result, make_is13_schemas()); return result; } @@ -382,6 +413,11 @@ namespace nmos return is08_schemas::v1_0::map_activations_post_request_uri; } + web::uri make_rwnodeapi_resource_core_patch_request_schema_uri(const nmos::api_version& version) + { + return is13_schemas::v1_0::rwnodeapi_resource_core_patch_request_uri; + } + // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id) { diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index e938a513e..e95348e05 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -29,6 +29,8 @@ namespace nmos web::uri make_channelmappingapi_map_activations_post_request_schema_uri(const nmos::api_version& version); + web::uri make_rwnodeapi_resource_core_patch_request_schema_uri(const nmos::api_version& version); + // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id); } diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index ecc75c461..94558f821 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -10,6 +10,7 @@ #include "nmos/model.h" #include "nmos/node_api.h" #include "nmos/node_behaviour.h" +#include "nmos/rwnode_api.h" #include "nmos/server.h" #include "nmos/server_utils.h" #include "nmos/settings_api.h" @@ -51,6 +52,7 @@ namespace nmos nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged); node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, gate)); + node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_rwnode_api(node_model, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); // Configure the Connection API diff --git a/Development/nmos/rwnode_api.cpp b/Development/nmos/rwnode_api.cpp new file mode 100644 index 000000000..0314aea76 --- /dev/null +++ b/Development/nmos/rwnode_api.cpp @@ -0,0 +1,242 @@ +#include "nmos/rwnode_api.h" + +#include +#include "cpprest/json_validator.h" +#include "nmos/api_utils.h" +#include "nmos/is13_versions.h" +#include "nmos/json_schema.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, slog::base_gate& gate); + + web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, slog::base_gate& gate) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router rwnode_api; + + rwnode_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-nmos/") }, req, res)); + return pplx::task_from_result(true); + }); + + rwnode_api.support(U("/x-nmos/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("rwnode/") }, req, res)); + return pplx::task_from_result(true); + }); + + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is13_versions::from_settings(model.settings); }); + rwnode_api.support(U("/x-nmos/") + nmos::patterns::rwnode_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body(nmos::make_api_version_sub_routes(versions), req, res)); + return pplx::task_from_result(true); + }); + + rwnode_api.mount(U("/x-nmos/") + nmos::patterns::rwnode_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_rwnode_api(model, gate)); + + return rwnode_api; + } + + web::json::value make_rwnode_response(const nmos::resource& resource) + { + using web::json::value_of; + return value_of({ + { nmos::fields::id, resource.data.at(nmos::fields::id) }, + { nmos::fields::version, resource.data.at(nmos::fields::version) }, + { nmos::fields::label, resource.data.at(nmos::fields::label) }, + { nmos::fields::description, resource.data.at(nmos::fields::description) }, + { nmos::fields::tags, resource.data.at(nmos::fields::tags) } + }); + } + + void merge_rwnode_request(web::json::value& value, const web::json::value& patch) + { + web::json::merge_patch(value, patch, true); + web::json::insert(value, std::make_pair(nmos::fields::label, U(""))); + web::json::insert(value, std::make_pair(nmos::fields::description, U(""))); + web::json::insert(value, std::make_pair(nmos::fields::tags, web::json::value::object())); + } + + web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, slog::base_gate& gate_) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router rwnode_api; + + // check for supported API version + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is13_versions::from_settings(model.settings); }); + rwnode_api.support(U(".*"), details::make_api_version_handler(versions, gate_)); + + rwnode_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("self/"), U("devices/"), U("sources/"), U("flows/"), U("senders/"), U("receivers/") }, req, res)); + return pplx::task_from_result(true); + }); + + rwnode_api.support(U("/self/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.read_lock(); + auto& resources = model.node_resources; + + const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); + + auto resource = nmos::find_self_resource(resources); + if (resources.end() != resource) + { + slog::log(gate, SLOG_FLF) << "Returning self resource: " << resource->id; + set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + } + else + { + slog::log(gate, SLOG_FLF) << "Self resource not found!"; + set_reply(res, status_codes::InternalError); // rather than Not Found, since the Read/Write Node API doesn't allow a 404 response + } + + return pplx::task_from_result(true); + }); + + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(versions | boost::adaptors::transformed(experimental::make_rwnodeapi_resource_core_patch_request_schema_uri)) + }; + + rwnode_api.support(U("/self/?"), methods::PATCH, [&model, validator, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + return details::extract_json(req, gate).then([&model, &validator, req, res, parameters, gate](value body) mutable + { + const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); + + validator.validate(body, experimental::make_rwnodeapi_resource_core_patch_request_schema_uri(version)); + + auto lock = model.write_lock(); + auto& resources = model.node_resources; + + auto resource = nmos::find_self_resource(resources); + if (resources.end() != resource) + { + slog::log(gate, SLOG_FLF) << "Patching self resource: " << resource->id; + + modify_resource(resources, resource->id, [&body](nmos::resource& resource) + { + resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + nmos::merge_rwnode_request(resource.data, body); + }); + + set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + + model.notify(); + } + else + { + slog::log(gate, SLOG_FLF) << "Self resource not found!"; + set_reply(res, status_codes::InternalError); // rather than Not Found, since the Read/Write Node API doesn't allow a 404 response + } + + return true; + }); + }); + + rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.read_lock(); + auto& resources = model.node_resources; + + const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); + const string_t resourceType = parameters.at(nmos::patterns::subresourceType.name); + + const auto match = [&](const nmos::resources::value_type& resource) { return resource.type == nmos::type_from_resourceType(resourceType); }; + + size_t count = 0; + + set_reply(res, status_codes::OK, + web::json::serialize_array(resources + | boost::adaptors::filtered(match) + | boost::adaptors::transformed( + [&count, &version](const nmos::resources::value_type& resource) { ++count; return nmos::make_rwnode_response(resource); } + )), + web::http::details::mime_types::application_json); + + slog::log(gate, SLOG_FLF) << "Returning " << count << " matching " << resourceType; + + return pplx::task_from_result(true); + }); + + rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.read_lock(); + auto& resources = model.node_resources; + + const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); + const string_t resourceType = parameters.at(nmos::patterns::subresourceType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + slog::log(gate, SLOG_FLF) << "Returning " << id_type; + set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::PATCH, [&model, validator, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + return details::extract_json(req, gate).then([&model, &validator, req, res, parameters, gate](value body) mutable + { + const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); + + validator.validate(body, experimental::make_rwnodeapi_resource_core_patch_request_schema_uri(version)); + + auto lock = model.write_lock(); + auto& resources = model.node_resources; + + const string_t resourceType = parameters.at(nmos::patterns::connectorType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + slog::log(gate, SLOG_FLF) << "Patching " << id_type; + + modify_resource(resources, resource->id, [&body](nmos::resource& resource) + { + resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + nmos::merge_rwnode_request(resource.data, body); + }); + + set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + + model.notify(); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return true; + }); + }); + + return rwnode_api; + } +} diff --git a/Development/nmos/rwnode_api.h b/Development/nmos/rwnode_api.h new file mode 100644 index 000000000..1ba1775f4 --- /dev/null +++ b/Development/nmos/rwnode_api.h @@ -0,0 +1,20 @@ +#ifndef NMOS_RWNODE_API_H +#define NMOS_RWNODE_API_H + +#include "cpprest/api_router.h" + +namespace slog +{ + class base_gate; +} + +// Read/Write Node API implementation +// See https://specs.amwa.tv/is-rwnode/branches/v1.0-dev/APIs/ReadWriteNodeAPI.html +namespace nmos +{ + struct model; + + web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, slog::base_gate& gate); +} + +#endif diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index cf55267fd..3287ede18 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -101,6 +101,9 @@ namespace nmos // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is09_versions{ U("is09_versions") }; // when omitted, nmos::is09_versions::all is used + // is13_versions [node]: used to specify the enabled API versions for a version-locked configuration + const web::json::field_as_array is13_versions{ U("is13_versions") }; // when omitted, nmos::is13_versions::all is used + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely const web::json::field_as_integer_or pri{ U("pri"), 100 }; // default to highest_development_priority diff --git a/Development/third_party/is-13/README.md b/Development/third_party/is-13/README.md new file mode 100644 index 000000000..1d0aa096c --- /dev/null +++ b/Development/third_party/is-13/README.md @@ -0,0 +1,8 @@ +# AMWA IS-13 NMOS Read/Write Node Specification + +This directory contains files from the [AMWA IS-13 NMOS Read/Write Node Specification](https://github.com/AMWA-TV/is-rwnode), in particular tagged versions of the JSON schemas used by the API specifications. + +Original source code: + +- (c) AMWA 2023 +- Licensed under the Apache License, Version 2.0; http://www.apache.org/licenses/LICENSE-2.0 diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/error.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/error.json new file mode 100644 index 000000000..402147b52 --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/error.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Describes the standard error response which is returned with HTTP codes 400 and above", + "title": "Error response", + "required": [ + "code", + "error", + "debug" + ], + "properties": { + "code": { + "description": "HTTP error code", + "type": "integer", + "minimum": 400, + "maximum": 599 + }, + "error": { + "description": "Human readable message which is suitable for user interface display, and helpful to the user", + "type": "string" + }, + "debug": { + "description": "Debug information which may assist a programmer working with the API", + "type": ["null", "string"] + } + } +} diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core.json new file mode 100644 index 000000000..4bb69f8dc --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Describes the foundations of all NMOS resources", + "title": "Base resource", + "required": [ + "id", + "version", + "label", + "description", + "tags" + ], + "properties": { + "id": { + "description": "Globally unique identifier for the resource", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "version": { + "description": "String formatted TAI timestamp (:) indicating precisely when an attribute of the resource last changed", + "type": "string", + "pattern": "^[0-9]+:[0-9]+$" + }, + "label": { + "description": "Freeform string label for the resource", + "type": "string" + }, + "description": { + "description": "Detailed description of the resource", + "type": "string" + }, + "tags": { + "description": "Key value set of freeform string tags to aid in filtering resources. Values should be represented as an array of strings. Can be empty.", + "type": "object", + "patternProperties": { + "": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json new file mode 100644 index 000000000..721527f8d --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json @@ -0,0 +1,28 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Describes the foundations of all NMOS resources", + "title": "Base resource", + "properties": { + "label": { + "description": "Freeform string label for the resource. Set to null to restore default label.", + "type": ["null", "string"] + }, + "description": { + "description": "Detailed description of the resource. Set to null to restore default label.", + "type": ["null", "string"] + }, + "tags": { + "description": "Key value set of freeform string tags to aid in filtering resources. Values should be represented as an array of strings. Can be empty. Set to null to delete or restore default values for a tag.", + "type": "object", + "patternProperties": { + "": { + "type": ["null", "array"], + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json new file mode 100644 index 000000000..e71271693 --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array", + "description": "A list of resources", + "title": "Collection of resources", + "items": { + "$ref": "resource_core.json" + }, + "uniqueItems": true +} diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json new file mode 100644 index 000000000..ca31ed51f --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Describes the Read/Write Node API base resource", + "title": "Read/Write Node API base resource", + "items": { + "type": "string", + "enum": [ + "self/", + "sources/", + "flows/", + "devices/", + "senders/", + "receivers/" + ] + }, + "minItems": 6, + "maxItems": 6, + "uniqueItems": true +} From ccf3d5e26f298b55151d3dd902d9648fa2da5997 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Thu, 11 May 2023 14:20:22 +0100 Subject: [PATCH 02/15] Sync with new repo name and latest v1.0-dev --- Development/nmos/json_schema.cpp | 4 ++-- Development/nmos/rwnode_api.h | 2 +- .../is-13/v1.0-dev/APIs/schemas/resource_core_patch.json | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index 405e6d245..4038f284e 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -133,10 +133,10 @@ namespace nmos { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) { - return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-rwnode/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + return{ _XPLATSTR("https://github.com/AMWA-TV/is-13/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; } - // See https://github.com/AMWA-TV/nmos-rwnode/blob/v1.0-dev/APIs/schemas/ + // See https://github.com/AMWA-TV/is-13/blob/v1.0-dev/APIs/schemas/ namespace v1_0 { using namespace nmos::is13_schemas::v1_0_dev; diff --git a/Development/nmos/rwnode_api.h b/Development/nmos/rwnode_api.h index 1ba1775f4..6b8e247e5 100644 --- a/Development/nmos/rwnode_api.h +++ b/Development/nmos/rwnode_api.h @@ -9,7 +9,7 @@ namespace slog } // Read/Write Node API implementation -// See https://specs.amwa.tv/is-rwnode/branches/v1.0-dev/APIs/ReadWriteNodeAPI.html +// See https://specs.amwa.tv/is-13/branches/v1.0-dev/APIs/ReadWriteNodeAPI.html namespace nmos { struct model; diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json index 721527f8d..c587640bb 100644 --- a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json @@ -3,6 +3,7 @@ "type": "object", "description": "Describes the foundations of all NMOS resources", "title": "Base resource", + "additionalProperties": false, "properties": { "label": { "description": "Freeform string label for the resource. Set to null to restore default label.", @@ -13,8 +14,8 @@ "type": ["null", "string"] }, "tags": { - "description": "Key value set of freeform string tags to aid in filtering resources. Values should be represented as an array of strings. Can be empty. Set to null to delete or restore default values for a tag.", - "type": "object", + "description": "Key value set of freeform string tags to aid in filtering resources. Set to null to restore default tags. Values should be represented as an array of strings. Can be empty. Set to null to delete or restore default values for a tag.", + "type": ["null", "object"], "patternProperties": { "": { "type": ["null", "array"], From 5cb17b25fc13054450bcbc6700f0935e82eebbb4 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Fri, 12 May 2023 11:52:56 +0100 Subject: [PATCH 03/15] Allow custom patch validation, and by default reject changes to BCP-002-01 Group Hints and BCP-002-02 Asset Distinguishing Information but allow everything else --- Development/nmos/node_server.cpp | 2 +- Development/nmos/node_server.h | 4 ++ Development/nmos/rwnode_api.cpp | 118 +++++++++++++++++++++++++------ Development/nmos/rwnode_api.h | 20 +++++- 4 files changed, 122 insertions(+), 22 deletions(-) diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 94558f821..b4ad0b6b5 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -52,7 +52,7 @@ namespace nmos nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged); node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, gate)); - node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_rwnode_api(node_model, gate)); + node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_rwnode_api(node_model, node_implementation.merge_rwnode_patch, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); // Configure the Connection API diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index dc8f4efa8..379c9e3f0 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -9,6 +9,7 @@ #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" #include "nmos/ocsp_response_handler.h" +#include "nmos/rwnode_api.h" namespace nmos { @@ -56,6 +57,7 @@ namespace nmos node_implementation& on_connection_activated(nmos::connection_activation_handler connection_activated) { this->connection_activated = std::move(connection_activated); return *this; } node_implementation& on_validate_channelmapping_output_map(nmos::details::channelmapping_output_map_validator validate_map) { this->validate_map = std::move(validate_map); return *this; } node_implementation& on_channelmapping_activated(nmos::channelmapping_activation_handler channelmapping_activated) { this->channelmapping_activated = std::move(channelmapping_activated); return *this; } + node_implementation& on_merge_rwnode_patch(nmos::rwnode_patch_merger merge_rwnode_patch) { this->merge_rwnode_patch = std::move(merge_rwnode_patch); return *this; } node_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } // deprecated, use on_validate_connection_resource_patch @@ -85,6 +87,8 @@ namespace nmos nmos::channelmapping_activation_handler channelmapping_activated; + nmos::rwnode_patch_merger merge_rwnode_patch; + nmos::ocsp_response_handler get_ocsp_response; }; diff --git a/Development/nmos/rwnode_api.cpp b/Development/nmos/rwnode_api.cpp index 0314aea76..afacf73fe 100644 --- a/Development/nmos/rwnode_api.cpp +++ b/Development/nmos/rwnode_api.cpp @@ -1,5 +1,6 @@ #include "nmos/rwnode_api.h" +#include #include #include "cpprest/json_validator.h" #include "nmos/api_utils.h" @@ -10,9 +11,9 @@ namespace nmos { - web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, nmos::rwnode_patch_merger merge_patch, slog::base_gate& gate); - web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, nmos::rwnode_patch_merger merge_patch, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -37,11 +38,21 @@ namespace nmos return pplx::task_from_result(true); }); - rwnode_api.mount(U("/x-nmos/") + nmos::patterns::rwnode_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_rwnode_api(model, gate)); + rwnode_api.mount(U("/x-nmos/") + nmos::patterns::rwnode_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_rwnode_api(model, std::move(merge_patch), gate)); return rwnode_api; } + web::json::value make_rwnode_patch(const nmos::resource& resource) + { + using web::json::value_of; + return value_of({ + { nmos::fields::label, resource.data.at(nmos::fields::label) }, + { nmos::fields::description, resource.data.at(nmos::fields::description) }, + { nmos::fields::tags, resource.data.at(nmos::fields::tags) } + }); + } + web::json::value make_rwnode_response(const nmos::resource& resource) { using web::json::value_of; @@ -54,15 +65,90 @@ namespace nmos }); } - void merge_rwnode_request(web::json::value& value, const web::json::value& patch) + namespace details + { + bool is_read_only_tag(const utility::string_t& key) + { + return boost::algorithm::starts_with(key, U("urn:x-nmos:tag:asset:")) + || boost::algorithm::starts_with(key, U("urn:x-nmos:tag:grouphint/")); + } + } + + // this function merges the patch with few additional constraints, i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information + // when reset using null, tags are removed, and label and description are set to the empty string + // (this is the default patch merger) + void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch, slog::base_gate& gate) { + // reject changes to read-ony tags + + if (patch.has_object_field(nmos::fields::tags)) + { + const auto& tags = nmos::fields::tags(patch); + auto patch_readonly = std::find_if(tags.begin(), tags.end(), [](const std::pair& field) + { + return details::is_read_only_tag(field.first); + }); + if (tags.end() != patch_readonly) throw std::runtime_error("cannot patch read-only tag: " + utility::us2s(patch_readonly->first)); + } + + // save existing read-only tags + + auto readonly_tags = web::json::value_from_fields(nmos::fields::tags(value) | boost::adaptors::filtered([](const std::pair& field) + { + return details::is_read_only_tag(field.first); + })); + + // apply patch + web::json::merge_patch(value, patch, true); + + // apply defaults to properties that have been reset + web::json::insert(value, std::make_pair(nmos::fields::label, U(""))); web::json::insert(value, std::make_pair(nmos::fields::description, U(""))); - web::json::insert(value, std::make_pair(nmos::fields::tags, web::json::value::object())); + web::json::insert(value, std::make_pair(nmos::fields::tags, readonly_tags)); + } + + namespace details + { + void assign_rwnode_patch(web::json::value& value, web::json::value&& patch) + { + if (value.has_string_field(nmos::fields::label)) value[nmos::fields::label] = std::move(patch.at(nmos::fields::label)); + if (value.has_string_field(nmos::fields::description)) value[nmos::fields::description] = std::move(patch.at(nmos::fields::description)); + if (value.has_object_field(nmos::fields::tags)) value[nmos::fields::tags] = std::move(patch.at(nmos::fields::tags)); + } + + void handle_rwnode_patch(nmos::resources& resources, const nmos::resource& resource, const web::json::value& patch, const nmos::rwnode_patch_merger& merge_patch, slog::base_gate& gate) + { + auto merged = nmos::make_rwnode_patch(resource); + try + { + if (merge_patch) + { + merge_patch(resource, merged, patch, gate); + } + else + { + nmos::merge_rwnode_patch(resource, merged, patch, gate); + } + } + catch (const web::json::json_exception& e) + { + throw std::logic_error(e.what()); + } + catch (const std::runtime_error& e) + { + throw std::logic_error(e.what()); + } + modify_resource(resources, resource.id, [&merged](nmos::resource& resource) + { + resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + details::assign_rwnode_patch(resource.data, std::move(merged)); + }); + } } - web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, slog::base_gate& gate_) + web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, nmos::rwnode_patch_merger merge_patch, slog::base_gate& gate_) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -107,11 +193,11 @@ namespace nmos boost::copy_range>(versions | boost::adaptors::transformed(experimental::make_rwnodeapi_resource_core_patch_request_schema_uri)) }; - rwnode_api.support(U("/self/?"), methods::PATCH, [&model, validator, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + rwnode_api.support(U("/self/?"), methods::PATCH, [&model, validator, merge_patch, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); - return details::extract_json(req, gate).then([&model, &validator, req, res, parameters, gate](value body) mutable + return details::extract_json(req, gate).then([&model, &validator, merge_patch, req, res, parameters, gate](value body) mutable { const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); @@ -125,11 +211,7 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Patching self resource: " << resource->id; - modify_resource(resources, resource->id, [&body](nmos::resource& resource) - { - resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); - nmos::merge_rwnode_request(resource.data, body); - }); + details::handle_rwnode_patch(resources, *resource, body, merge_patch, gate); set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); @@ -196,11 +278,11 @@ namespace nmos return pplx::task_from_result(true); }); - rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::PATCH, [&model, validator, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::PATCH, [&model, validator, merge_patch, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); - return details::extract_json(req, gate).then([&model, &validator, req, res, parameters, gate](value body) mutable + return details::extract_json(req, gate).then([&model, &validator, merge_patch, req, res, parameters, gate](value body) mutable { const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); @@ -218,11 +300,7 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Patching " << id_type; - modify_resource(resources, resource->id, [&body](nmos::resource& resource) - { - resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); - nmos::merge_rwnode_request(resource.data, body); - }); + details::handle_rwnode_patch(resources, *resource, body, merge_patch, gate); set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); diff --git a/Development/nmos/rwnode_api.h b/Development/nmos/rwnode_api.h index 6b8e247e5..acbe72b8b 100644 --- a/Development/nmos/rwnode_api.h +++ b/Development/nmos/rwnode_api.h @@ -13,8 +13,26 @@ namespace slog namespace nmos { struct model; + struct resource; - web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, slog::base_gate& gate); + // Read/Write Node API callbacks + + // a rwnode_patch_merger validates the specified patch data for the specified IS-04 resource and updates the object to be merged + // or may throw std::runtime_error, which will be mapped to a 500 Internal Error status code with NMOS error "debug" information including the exception message + // (the default patch merger, nmos::merge_rwnode_patch, implements the minimum requirements) + typedef std::function rwnode_patch_merger; + + // Read/Write Node API factory functions + + // callbacks from this function are called with the model locked, and may read but should not write directly to the model + web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, rwnode_patch_merger merge_patch, slog::base_gate& gate); + + // Helper functions for the Read/Write Node API callbacks + + // this function merges the patch with few additional constraints, i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information + // when reset using null, tags are removed, and label and description are set to the empty string + // (this is the default patch merger) + void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch, slog::base_gate& gate); } #endif From 4528bce216bd9f63bf136c0640a51fcd33165698 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Fri, 12 May 2023 15:49:22 +0100 Subject: [PATCH 04/15] Refactor and add unit test --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/rwnode_api.cpp | 52 +++++------ Development/nmos/rwnode_api.h | 13 ++- Development/nmos/test/rwnode_api_test.cpp | 105 ++++++++++++++++++++++ 4 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 Development/nmos/test/rwnode_api_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 02db14903..6141ea40e 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -46,6 +46,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/event_type_test.cpp nmos/test/json_validator_test.cpp nmos/test/paging_utils_test.cpp + nmos/test/rwnode_api_test.cpp nmos/test/query_api_test.cpp nmos/test/sdp_utils_test.cpp nmos/test/system_resources_test.cpp diff --git a/Development/nmos/rwnode_api.cpp b/Development/nmos/rwnode_api.cpp index afacf73fe..1c3aaef0d 100644 --- a/Development/nmos/rwnode_api.cpp +++ b/Development/nmos/rwnode_api.cpp @@ -72,45 +72,39 @@ namespace nmos return boost::algorithm::starts_with(key, U("urn:x-nmos:tag:asset:")) || boost::algorithm::starts_with(key, U("urn:x-nmos:tag:grouphint/")); } - } - - // this function merges the patch with few additional constraints, i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information - // when reset using null, tags are removed, and label and description are set to the empty string - // (this is the default patch merger) - void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch, slog::base_gate& gate) - { - // reject changes to read-ony tags - if (patch.has_object_field(nmos::fields::tags)) + void merge_rwnode_patch(web::json::value& value, const web::json::value& patch) { - const auto& tags = nmos::fields::tags(patch); - auto patch_readonly = std::find_if(tags.begin(), tags.end(), [](const std::pair& field) + // reject changes to read-ony tags + + if (patch.has_object_field(nmos::fields::tags)) { - return details::is_read_only_tag(field.first); - }); - if (tags.end() != patch_readonly) throw std::runtime_error("cannot patch read-only tag: " + utility::us2s(patch_readonly->first)); - } + const auto& tags = nmos::fields::tags(patch); + auto patch_readonly = std::find_if(tags.begin(), tags.end(), [](const std::pair& field) + { + return is_read_only_tag(field.first); + }); + if (tags.end() != patch_readonly) throw std::runtime_error("cannot patch read-only tag: " + utility::us2s(patch_readonly->first)); + } - // save existing read-only tags + // save existing read-only tags - auto readonly_tags = web::json::value_from_fields(nmos::fields::tags(value) | boost::adaptors::filtered([](const std::pair& field) - { - return details::is_read_only_tag(field.first); - })); + auto readonly_tags = web::json::value_from_fields(nmos::fields::tags(value) | boost::adaptors::filtered([](const std::pair& field) + { + return is_read_only_tag(field.first); + })); - // apply patch + // apply patch - web::json::merge_patch(value, patch, true); + web::json::merge_patch(value, patch, true); - // apply defaults to properties that have been reset + // apply defaults to properties that have been reset - web::json::insert(value, std::make_pair(nmos::fields::label, U(""))); - web::json::insert(value, std::make_pair(nmos::fields::description, U(""))); - web::json::insert(value, std::make_pair(nmos::fields::tags, readonly_tags)); - } + web::json::insert(value, std::make_pair(nmos::fields::label, U(""))); + web::json::insert(value, std::make_pair(nmos::fields::description, U(""))); + web::json::insert(value, std::make_pair(nmos::fields::tags, readonly_tags)); + } - namespace details - { void assign_rwnode_patch(web::json::value& value, web::json::value&& patch) { if (value.has_string_field(nmos::fields::label)) value[nmos::fields::label] = std::move(patch.at(nmos::fields::label)); diff --git a/Development/nmos/rwnode_api.h b/Development/nmos/rwnode_api.h index acbe72b8b..c863cda50 100644 --- a/Development/nmos/rwnode_api.h +++ b/Development/nmos/rwnode_api.h @@ -29,10 +29,19 @@ namespace nmos // Helper functions for the Read/Write Node API callbacks - // this function merges the patch with few additional constraints, i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information + namespace details + { + void merge_rwnode_patch(web::json::value& value, const web::json::value& patch); + } + + // this function merges the patch into the value with few additional constraints + // i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information // when reset using null, tags are removed, and label and description are set to the empty string // (this is the default patch merger) - void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch, slog::base_gate& gate); + inline void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch, slog::base_gate& gate) + { + details::merge_rwnode_patch(value, patch); + } } #endif diff --git a/Development/nmos/test/rwnode_api_test.cpp b/Development/nmos/test/rwnode_api_test.cpp new file mode 100644 index 000000000..b0c160dcf --- /dev/null +++ b/Development/nmos/test/rwnode_api_test.cpp @@ -0,0 +1,105 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/rwnode_api.h" + +#include "bst/test/test.h" +#include "nmos/group_hint.h" +#include "nmos/json_fields.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testMergeRwnodePatch) +{ + using web::json::value; + using web::json::value_of; + + const auto source = value_of({ + { nmos::fields::label, U("meow") }, + { nmos::fields::description, U("purr") }, + { nmos::fields::tags, value_of({ + { U("foo"), value_of({ U("hiss"), U("yowl") }) }, + { nmos::fields::group_hint, value_of({ nmos::make_group_hint({ U("bar"), U("baz") }) }) } + }) } + }); + + // empty patch + { + auto merged{ source }; + nmos::details::merge_rwnode_patch(merged, value::object()); + BST_REQUIRE_EQUAL(source, merged); + } + + // reset everything + { + auto merged{ source }; + nmos::details::merge_rwnode_patch(merged, value_of({ + { nmos::fields::label, {} }, + { nmos::fields::description, {} }, + { nmos::fields::tags, {} } + })); + BST_REQUIRE(nmos::fields::label(merged).empty()); + BST_REQUIRE(nmos::fields::description(merged).empty()); + const auto& tags = merged.at(nmos::fields::tags); + BST_REQUIRE_EQUAL(1, tags.size()); + const auto& group_hint = nmos::fields::group_hint(tags); + BST_REQUIRE_EQUAL(1, group_hint.size()); + BST_REQUIRE_EQUAL(U("bar:baz"), group_hint.at(0).as_string()); + } + + // try to reset read-only tag + { + auto merged{ source }; + BST_REQUIRE_THROW(nmos::details::merge_rwnode_patch(merged, value_of({ + { nmos::fields::tags, value_of({ + { nmos::fields::group_hint, {} } + }) } + })), std::runtime_error); + } + + // try to update read-only tag + { + auto merged{ source }; + BST_REQUIRE_THROW(nmos::details::merge_rwnode_patch(merged, value_of({ + { nmos::fields::tags, value_of({ + { nmos::fields::group_hint, value_of({ nmos::make_group_hint({ U("qux"), U("quux") }) }) } + }) } + })), std::runtime_error); + } + + // add and remove tags + { + auto merged{ source }; + nmos::details::merge_rwnode_patch(merged, value_of({ + { nmos::fields::tags, value_of({ + { U("foo"), {} }, + { U("bar"), value_of({ U("woof"), U("bark") }) }, + { U("baz"), {} } + }) } + })); + const auto& tags = merged.at(nmos::fields::tags); + BST_REQUIRE_EQUAL(2, tags.size()); + BST_REQUIRE(!tags.has_field(U("foo"))); + BST_REQUIRE(tags.has_field(U("bar"))); + const auto& bar = tags.at(U("bar")); + BST_REQUIRE_EQUAL(2, bar.size()); + BST_REQUIRE_EQUAL(U("bark"), bar.at(1).as_string()); + } + + // change label, description and tags + { + auto merged{ source }; + nmos::details::merge_rwnode_patch(merged, value_of({ + { nmos::fields::label, U("woof") }, + { nmos::fields::description, U("bark") }, + { nmos::fields::tags, value_of({ + { U("foo"), value_of({ U("growl") })} + }) } + })); + BST_REQUIRE_EQUAL(U("woof"), nmos::fields::label(merged)); + BST_REQUIRE_EQUAL(U("bark"), nmos::fields::description(merged)); + const auto& tags = merged.at(nmos::fields::tags); + BST_REQUIRE_EQUAL(2, tags.size()); + BST_REQUIRE(tags.has_field(U("foo"))); + const auto& foo = tags.at(U("foo")); + BST_REQUIRE_EQUAL(1, foo.size()); + BST_REQUIRE_EQUAL(U("growl"), foo.at(0).as_string()); + } +} From 28ef83ded48433972f8ac5369c28765da2252130 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Fri, 12 May 2023 15:55:16 +0100 Subject: [PATCH 05/15] Add IS-13 to READMEs --- Development/third_party/README.md | 2 ++ README.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Development/third_party/README.md b/Development/third_party/README.md index 2788ca9e0..15742001a 100644 --- a/Development/third_party/README.md +++ b/Development/third_party/README.md @@ -18,5 +18,7 @@ Third-party source files used by the nmos-cpp libraries The JSON Schema files used for validation of Channel Mapping API requests and responses - [is-09](is-09) The JSON Schema files used for validation of System API requests and responses +- [is-13](is-13) + The JSON Schema files used for validation of Read/Write Node API requests and responses - [WpdPack](WpdPack) Libraries and header files from the [WinPcap](https://www.winpcap.org/) Developer's Pack diff --git a/README.md b/README.md index 050394924..191d421c8 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA IS-07 NMOS Event & Tally Specification](https://specs.amwa.tv/is-07/) - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) - [AMWA IS-09 NMOS System Parameters Specification](https://specs.amwa.tv/is-09/) (originally defined in JT-NM TR-1001-1:2018 Annex A) +- [AMWA IS-13 NMOS Read/Write Node Specification](https://specs.amwa.tv/is-13/) - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) - [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) @@ -112,6 +113,7 @@ The implementation is designed to be extended. Development is ongoing, following Recent activity on the project (newest first): +- Added support for IS-13 v1.0-dev - Added support for HSTS and OCSP stapling - Added support for BCP-006-01 v1.0-dev, which can be demonstrated with **nmos-cpp-node** by using `"video_type": "video/jxsv"` - Updates to the GitHub Actions build-test workflow for better coverage of platforms and to include unicast DNS-SD tests From 6927a98882a0ebbab954c627924f3c787457f4f7 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Fri, 12 May 2023 17:05:21 +0100 Subject: [PATCH 06/15] Fix for gcc pre-5 --- Development/nmos/test/rwnode_api_test.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Development/nmos/test/rwnode_api_test.cpp b/Development/nmos/test/rwnode_api_test.cpp index b0c160dcf..739b9a376 100644 --- a/Development/nmos/test/rwnode_api_test.cpp +++ b/Development/nmos/test/rwnode_api_test.cpp @@ -22,14 +22,14 @@ BST_TEST_CASE(testMergeRwnodePatch) // empty patch { - auto merged{ source }; + auto merged(source); nmos::details::merge_rwnode_patch(merged, value::object()); BST_REQUIRE_EQUAL(source, merged); } // reset everything { - auto merged{ source }; + auto merged(source); nmos::details::merge_rwnode_patch(merged, value_of({ { nmos::fields::label, {} }, { nmos::fields::description, {} }, @@ -46,7 +46,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // try to reset read-only tag { - auto merged{ source }; + auto merged(source); BST_REQUIRE_THROW(nmos::details::merge_rwnode_patch(merged, value_of({ { nmos::fields::tags, value_of({ { nmos::fields::group_hint, {} } @@ -56,7 +56,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // try to update read-only tag { - auto merged{ source }; + auto merged(source); BST_REQUIRE_THROW(nmos::details::merge_rwnode_patch(merged, value_of({ { nmos::fields::tags, value_of({ { nmos::fields::group_hint, value_of({ nmos::make_group_hint({ U("qux"), U("quux") }) }) } @@ -66,7 +66,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // add and remove tags { - auto merged{ source }; + auto merged(source); nmos::details::merge_rwnode_patch(merged, value_of({ { nmos::fields::tags, value_of({ { U("foo"), {} }, @@ -85,7 +85,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // change label, description and tags { - auto merged{ source }; + auto merged(source); nmos::details::merge_rwnode_patch(merged, value_of({ { nmos::fields::label, U("woof") }, { nmos::fields::description, U("bark") }, From 147018f0bff834462671f6e5ca677724ab3f90d3 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Sat, 13 May 2023 05:17:17 +0100 Subject: [PATCH 07/15] Add explicit implementation in nmos-cpp-node --- Development/nmos-cpp-node/node_implementation.cpp | 14 +++++++++++++- Development/nmos/rwnode_api.cpp | 4 ++-- Development/nmos/rwnode_api.h | 4 ++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index dc3ee9d26..6104944ff 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -1269,6 +1269,17 @@ nmos::channelmapping_activation_handler make_node_implementation_channelmapping_ }; } +// Example Read/Write Node API patch callback to update resource labels, descriptions and tags +nmos::rwnode_patch_merger make_node_implementation_rwnode_patch_merger(slog::base_gate& gate) +{ + return [&gate](const nmos::resource& resource, web::json::value& value, const web::json::value& patch) + { + const std::pair id_type{ resource.id, resource.type }; + slog::log(gate, SLOG_FLF) << nmos::stash_category(impl::categories::node_implementation) << "Updating " << id_type; + nmos::details::merge_rwnode_patch(value, patch); + }; +} + namespace impl { nmos::interlace_mode get_interlace_mode(const nmos::settings& settings) @@ -1422,5 +1433,6 @@ nmos::experimental::node_implementation make_node_implementation(nmos::node_mode .on_set_transportfile(make_node_implementation_transportfile_setter(model.node_resources, model.settings)) .on_connection_activated(make_node_implementation_connection_activation_handler(model, gate)) .on_validate_channelmapping_output_map(make_node_implementation_map_validator()) // may be omitted if not required - .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)); + .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)) + .on_merge_rwnode_patch(make_node_implementation_rwnode_patch_merger(gate)); // may be omitted if not required } diff --git a/Development/nmos/rwnode_api.cpp b/Development/nmos/rwnode_api.cpp index 1c3aaef0d..10b4793f5 100644 --- a/Development/nmos/rwnode_api.cpp +++ b/Development/nmos/rwnode_api.cpp @@ -119,11 +119,11 @@ namespace nmos { if (merge_patch) { - merge_patch(resource, merged, patch, gate); + merge_patch(resource, merged, patch); } else { - nmos::merge_rwnode_patch(resource, merged, patch, gate); + nmos::merge_rwnode_patch(resource, merged, patch); } } catch (const web::json::json_exception& e) diff --git a/Development/nmos/rwnode_api.h b/Development/nmos/rwnode_api.h index c863cda50..5b57a6ffe 100644 --- a/Development/nmos/rwnode_api.h +++ b/Development/nmos/rwnode_api.h @@ -20,7 +20,7 @@ namespace nmos // a rwnode_patch_merger validates the specified patch data for the specified IS-04 resource and updates the object to be merged // or may throw std::runtime_error, which will be mapped to a 500 Internal Error status code with NMOS error "debug" information including the exception message // (the default patch merger, nmos::merge_rwnode_patch, implements the minimum requirements) - typedef std::function rwnode_patch_merger; + typedef std::function rwnode_patch_merger; // Read/Write Node API factory functions @@ -38,7 +38,7 @@ namespace nmos // i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information // when reset using null, tags are removed, and label and description are set to the empty string // (this is the default patch merger) - inline void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch, slog::base_gate& gate) + inline void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch) { details::merge_rwnode_patch(value, patch); } From 031bd00be58f7d918b17330c7601422ce49cf900 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Sat, 13 May 2023 05:34:29 +0100 Subject: [PATCH 08/15] Add service advertisement --- Development/nmos-cpp-node/config.json | 1 + Development/nmos/node_resource.cpp | 21 ++++++++++++++++++++- Development/nmos/node_server.cpp | 2 +- Development/nmos/settings.cpp | 1 + Development/nmos/settings.h | 1 + 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 8ebe3740f..715b72386 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -135,6 +135,7 @@ //"events_port": 3216, //"events_ws_port": 3217, //"channelmapping_port": 3215, + //"rwnode_port": 3212, // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) //"system_port": 10641, diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index 7b65b25b2..77f2ddeae 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -5,6 +5,7 @@ #include "nmos/clock_name.h" #include "nmos/clock_ref_type.h" #include "nmos/is04_versions.h" +#include "nmos/is13_versions.h" #include "nmos/resource.h" namespace nmos @@ -41,7 +42,25 @@ namespace nmos data[U("caps")] = value::object(); - data[U("services")] = value::array(); + if (0 <= nmos::fields::rwnode_port(settings)) + { + for (const auto& version : nmos::is13_versions::from_settings(settings)) + { + auto rwnode_uri = web::uri_builder() + .set_scheme(nmos::http_scheme(settings)) + .set_port(nmos::fields::rwnode_port(settings)) + .set_path(U("/x-nmos/rwnode/") + make_api_version(version)); + auto type = U("urn:x-nmos:service:rw-node/") + make_api_version(version); + + for (const auto& host : hosts) + { + web::json::push_back(data[U("services")], value_of({ + { U("href"), rwnode_uri.set_host(host).to_uri().to_string() }, + { U("type"), type } + })); + } + } + } data[U("clocks")] = !web::json::empty(clocks) ? clocks : value::array(); diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index b4ad0b6b5..97848306c 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -52,7 +52,7 @@ namespace nmos nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged); node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, gate)); - node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_rwnode_api(node_model, node_implementation.merge_rwnode_patch, gate)); + node_server.api_routers[{ {}, nmos::fields::rwnode_port(node_model.settings) }].mount({}, nmos::make_rwnode_api(node_model, node_implementation.merge_rwnode_patch, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); // Configure the Connection API diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index c8943c93e..1b546cfd6 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -70,6 +70,7 @@ namespace nmos if (registry) web::json::insert(settings, std::make_pair(nmos::fields::query_ws_port, ws_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::fields::registration_port, http_port)); web::json::insert(settings, std::make_pair(nmos::fields::node_port, http_port)); + if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::rwnode_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::fields::system_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::connection_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_port, http_port)); diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 3287ede18..02dae4d43 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -137,6 +137,7 @@ namespace nmos const web::json::field_as_integer_or events_port{ U("events_port"), 3216 }; const web::json::field_as_integer_or events_ws_port{ U("events_ws_port"), 3217 }; const web::json::field_as_integer_or channelmapping_port{ U("channelmapping_port"), 3215 }; + const web::json::field_as_integer_or rwnode_port{ U("rwnode_port"), 3212 }; // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) const web::json::field_as_integer_or system_port{ U("system_port"), 10641 }; From 1a4a3ab885c97cebaae020754fb99102179c0b78 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Sat, 13 May 2023 05:44:32 +0100 Subject: [PATCH 09/15] Fix missing Node services and Device controls attributes when there aren't any to be advertised --- Development/nmos/node_resource.cpp | 2 ++ Development/nmos/node_resources.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index 77f2ddeae..d68b84067 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -42,6 +42,8 @@ namespace nmos data[U("caps")] = value::object(); + data[U("services")] = value::array(); + if (0 <= nmos::fields::rwnode_port(settings)) { for (const auto& version : nmos::is13_versions::from_settings(settings)) diff --git a/Development/nmos/node_resources.cpp b/Development/nmos/node_resources.cpp index 7c75cf01a..69056819d 100644 --- a/Development/nmos/node_resources.cpp +++ b/Development/nmos/node_resources.cpp @@ -41,6 +41,8 @@ namespace nmos const auto hosts = nmos::get_hosts(settings); + data[U("controls")] = value::array(); + if (0 <= nmos::fields::connection_port(settings)) { for (const auto& version : nmos::is05_versions::from_settings(settings)) From e7bd6efd90ed5e748f584ec25c471d7fdaeed2bd Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley <31761158+garethsb@users.noreply.github.com> Date: Sat, 13 May 2023 10:01:13 +0100 Subject: [PATCH 10/15] Fix copy-paste typo --- Development/nmos/is13_versions.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/is13_versions.h b/Development/nmos/is13_versions.h index 3c33a506c..29e1a30f5 100644 --- a/Development/nmos/is13_versions.h +++ b/Development/nmos/is13_versions.h @@ -17,7 +17,7 @@ namespace nmos inline std::set from_settings(const nmos::settings& settings) { return settings.has_field(nmos::fields::is13_versions) - ? boost::copy_range>(nmos::fields::is04_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + ? boost::copy_range>(nmos::fields::is13_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) : nmos::is13_versions::all; } } From bf67e66c4289872676b3b3a21129f3166de025a7 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Fri, 19 May 2023 09:14:35 +0100 Subject: [PATCH 11/15] Remove currently unused variables --- Development/nmos/rwnode_api.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Development/nmos/rwnode_api.cpp b/Development/nmos/rwnode_api.cpp index 10b4793f5..42018f0c7 100644 --- a/Development/nmos/rwnode_api.cpp +++ b/Development/nmos/rwnode_api.cpp @@ -164,8 +164,6 @@ namespace nmos auto lock = model.read_lock(); auto& resources = model.node_resources; - const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); - auto resource = nmos::find_self_resource(resources); if (resources.end() != resource) { @@ -227,7 +225,6 @@ namespace nmos auto lock = model.read_lock(); auto& resources = model.node_resources; - const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); const string_t resourceType = parameters.at(nmos::patterns::subresourceType.name); const auto match = [&](const nmos::resources::value_type& resource) { return resource.type == nmos::type_from_resourceType(resourceType); }; @@ -238,7 +235,7 @@ namespace nmos web::json::serialize_array(resources | boost::adaptors::filtered(match) | boost::adaptors::transformed( - [&count, &version](const nmos::resources::value_type& resource) { ++count; return nmos::make_rwnode_response(resource); } + [&count](const nmos::resources::value_type& resource) { ++count; return nmos::make_rwnode_response(resource); } )), web::http::details::mime_types::application_json); @@ -253,7 +250,6 @@ namespace nmos auto lock = model.read_lock(); auto& resources = model.node_resources; - const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); const string_t resourceType = parameters.at(nmos::patterns::subresourceType.name); const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); From 9cd89854d2e9d99b0ea7d35cd280cfe432c14d7c Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Fri, 19 May 2023 17:31:07 +0100 Subject: [PATCH 12/15] Rename Read/Write Node API to Annotation API (cf. https://github.com/AMWA-TV/is-13/pull/19) --- Development/cmake/NmosCppLibraries.cmake | 7 +- Development/cmake/NmosCppTest.cmake | 2 +- Development/nmos-cpp-node/config.json | 2 +- .../nmos-cpp-node/node_implementation.cpp | 8 +- .../{rwnode_api.cpp => annotation_api.cpp} | 86 ++++++++++--------- .../nmos/{rwnode_api.h => annotation_api.h} | 22 ++--- Development/nmos/api_utils.h | 4 +- Development/nmos/is13_schemas/is13_schemas.h | 3 +- Development/nmos/json_schema.cpp | 6 +- Development/nmos/json_schema.h | 2 +- Development/nmos/node_resource.cpp | 12 +-- Development/nmos/node_server.cpp | 4 +- Development/nmos/node_server.h | 6 +- Development/nmos/settings.cpp | 2 +- Development/nmos/settings.h | 2 +- ...e_api_test.cpp => annotation_api_test.cpp} | 16 ++-- Development/third_party/README.md | 2 +- Development/third_party/is-13/README.md | 4 +- .../APIs/schemas/annotationapi-base.json | 15 ++++ ...base.json => annotationapi-node-base.json} | 4 +- .../APIs/schemas/resource_core_patch.json | 2 +- README.md | 2 +- 22 files changed, 118 insertions(+), 95 deletions(-) rename Development/nmos/{rwnode_api.cpp => annotation_api.cpp} (69%) rename Development/nmos/{rwnode_api.h => annotation_api.h} (53%) rename Development/nmos/test/{rwnode_api_test.cpp => annotation_api_test.cpp} (86%) create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-base.json rename Development/third_party/is-13/v1.0-dev/APIs/schemas/{rwnodeapi-base.json => annotationapi-node-base.json} (72%) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 5f266c032..ecf845a14 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -695,11 +695,12 @@ set(NMOS_IS13_SCHEMAS_HEADERS set(NMOS_IS13_V1_0_TAG v1.0-dev) set(NMOS_IS13_V1_0_SCHEMAS_JSON + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/annotationapi-base.json + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/annotationapi-node-base.json third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/error.json third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_core.json third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_core_patch.json third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_cores.json - third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/rwnodeapi-base.json ) set(NMOS_IS13_SCHEMAS_JSON_MATCH "third_party/is-13/([^/]+)/APIs/schemas/([^;]+)\\.json") @@ -813,6 +814,7 @@ set(NMOS_CPP_CPPREST_DETAILS_HEADERS set(NMOS_CPP_NMOS_SOURCES nmos/activation_utils.cpp nmos/admin_ui.cpp + nmos/annotation_api.cpp nmos/api_downgrade.cpp nmos/api_utils.cpp nmos/capabilities.cpp @@ -864,7 +866,6 @@ set(NMOS_CPP_NMOS_SOURCES nmos/registry_server.cpp nmos/resource.cpp nmos/resources.cpp - nmos/rwnode_api.cpp nmos/schemas_api.cpp nmos/sdp_utils.cpp nmos/server.cpp @@ -879,6 +880,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/activation_mode.h nmos/activation_utils.h nmos/admin_ui.h + nmos/annotation_api.h nmos/api_downgrade.h nmos/api_utils.h nmos/api_version.h @@ -958,7 +960,6 @@ set(NMOS_CPP_NMOS_HEADERS nmos/registry_server.h nmos/resource.h nmos/resources.h - nmos/rwnode_api.h nmos/schemas_api.h nmos/sdp_utils.h nmos/server.h diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 6141ea40e..f33f98e40 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -39,6 +39,7 @@ set(NMOS_CPP_TEST_MDNS_TEST_HEADERS ) set(NMOS_CPP_TEST_NMOS_TEST_SOURCES + nmos/test/annotation_api_test.cpp nmos/test/api_utils_test.cpp nmos/test/capabilities_test.cpp nmos/test/channels_test.cpp @@ -46,7 +47,6 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/event_type_test.cpp nmos/test/json_validator_test.cpp nmos/test/paging_utils_test.cpp - nmos/test/rwnode_api_test.cpp nmos/test/query_api_test.cpp nmos/test/sdp_utils_test.cpp nmos/test/system_resources_test.cpp diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index 715b72386..f9a8b4814 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -135,7 +135,7 @@ //"events_port": 3216, //"events_ws_port": 3217, //"channelmapping_port": 3215, - //"rwnode_port": 3212, + //"annotation_port": 3212, // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) //"system_port": 10641, diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 6104944ff..85df5bca5 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -1269,14 +1269,14 @@ nmos::channelmapping_activation_handler make_node_implementation_channelmapping_ }; } -// Example Read/Write Node API patch callback to update resource labels, descriptions and tags -nmos::rwnode_patch_merger make_node_implementation_rwnode_patch_merger(slog::base_gate& gate) +// Example Annotation API patch callback to update resource labels, descriptions and tags +nmos::annotation_patch_merger make_node_implementation_annotation_patch_merger(slog::base_gate& gate) { return [&gate](const nmos::resource& resource, web::json::value& value, const web::json::value& patch) { const std::pair id_type{ resource.id, resource.type }; slog::log(gate, SLOG_FLF) << nmos::stash_category(impl::categories::node_implementation) << "Updating " << id_type; - nmos::details::merge_rwnode_patch(value, patch); + nmos::details::merge_annotation_patch(value, patch); }; } @@ -1434,5 +1434,5 @@ nmos::experimental::node_implementation make_node_implementation(nmos::node_mode .on_connection_activated(make_node_implementation_connection_activation_handler(model, gate)) .on_validate_channelmapping_output_map(make_node_implementation_map_validator()) // may be omitted if not required .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)) - .on_merge_rwnode_patch(make_node_implementation_rwnode_patch_merger(gate)); // may be omitted if not required + .on_merge_annotation_patch(make_node_implementation_annotation_patch_merger(gate)); // may be omitted if not required } diff --git a/Development/nmos/rwnode_api.cpp b/Development/nmos/annotation_api.cpp similarity index 69% rename from Development/nmos/rwnode_api.cpp rename to Development/nmos/annotation_api.cpp index 42018f0c7..0cf638672 100644 --- a/Development/nmos/rwnode_api.cpp +++ b/Development/nmos/annotation_api.cpp @@ -1,4 +1,4 @@ -#include "nmos/rwnode_api.h" +#include "nmos/annotation_api.h" #include #include @@ -11,39 +11,39 @@ namespace nmos { - web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, nmos::rwnode_patch_merger merge_patch, slog::base_gate& gate); + web::http::experimental::listener::api_router make_unmounted_annotation_api(nmos::model& model, nmos::annotation_patch_merger merge_patch, slog::base_gate& gate); - web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, nmos::rwnode_patch_merger merge_patch, slog::base_gate& gate) + web::http::experimental::listener::api_router make_annotation_api(nmos::model& model, nmos::annotation_patch_merger merge_patch, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; - api_router rwnode_api; + api_router annotation_api; - rwnode_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + annotation_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) { set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-nmos/") }, req, res)); return pplx::task_from_result(true); }); - rwnode_api.support(U("/x-nmos/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + annotation_api.support(U("/x-nmos/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) { - set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("rwnode/") }, req, res)); + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("annotation/") }, req, res)); return pplx::task_from_result(true); }); const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is13_versions::from_settings(model.settings); }); - rwnode_api.support(U("/x-nmos/") + nmos::patterns::rwnode_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) + annotation_api.support(U("/x-nmos/") + nmos::patterns::annotation_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { set_reply(res, status_codes::OK, nmos::make_sub_routes_body(nmos::make_api_version_sub_routes(versions), req, res)); return pplx::task_from_result(true); }); - rwnode_api.mount(U("/x-nmos/") + nmos::patterns::rwnode_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_rwnode_api(model, std::move(merge_patch), gate)); + annotation_api.mount(U("/x-nmos/") + nmos::patterns::annotation_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_annotation_api(model, std::move(merge_patch), gate)); - return rwnode_api; + return annotation_api; } - web::json::value make_rwnode_patch(const nmos::resource& resource) + web::json::value make_annotation_patch(const nmos::resource& resource) { using web::json::value_of; return value_of({ @@ -53,7 +53,7 @@ namespace nmos }); } - web::json::value make_rwnode_response(const nmos::resource& resource) + web::json::value make_annotation_response(const nmos::resource& resource) { using web::json::value_of; return value_of({ @@ -73,7 +73,7 @@ namespace nmos || boost::algorithm::starts_with(key, U("urn:x-nmos:tag:grouphint/")); } - void merge_rwnode_patch(web::json::value& value, const web::json::value& patch) + void merge_annotation_patch(web::json::value& value, const web::json::value& patch) { // reject changes to read-ony tags @@ -105,16 +105,16 @@ namespace nmos web::json::insert(value, std::make_pair(nmos::fields::tags, readonly_tags)); } - void assign_rwnode_patch(web::json::value& value, web::json::value&& patch) + void assign_annotation_patch(web::json::value& value, web::json::value&& patch) { if (value.has_string_field(nmos::fields::label)) value[nmos::fields::label] = std::move(patch.at(nmos::fields::label)); if (value.has_string_field(nmos::fields::description)) value[nmos::fields::description] = std::move(patch.at(nmos::fields::description)); if (value.has_object_field(nmos::fields::tags)) value[nmos::fields::tags] = std::move(patch.at(nmos::fields::tags)); } - void handle_rwnode_patch(nmos::resources& resources, const nmos::resource& resource, const web::json::value& patch, const nmos::rwnode_patch_merger& merge_patch, slog::base_gate& gate) + void handle_annotation_patch(nmos::resources& resources, const nmos::resource& resource, const web::json::value& patch, const nmos::annotation_patch_merger& merge_patch, slog::base_gate& gate) { - auto merged = nmos::make_rwnode_patch(resource); + auto merged = nmos::make_annotation_patch(resource); try { if (merge_patch) @@ -123,7 +123,7 @@ namespace nmos } else { - nmos::merge_rwnode_patch(resource, merged, patch); + nmos::merge_annotation_patch(resource, merged, patch); } } catch (const web::json::json_exception& e) @@ -137,28 +137,34 @@ namespace nmos modify_resource(resources, resource.id, [&merged](nmos::resource& resource) { resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); - details::assign_rwnode_patch(resource.data, std::move(merged)); + details::assign_annotation_patch(resource.data, std::move(merged)); }); } } - web::http::experimental::listener::api_router make_unmounted_rwnode_api(nmos::model& model, nmos::rwnode_patch_merger merge_patch, slog::base_gate& gate_) + web::http::experimental::listener::api_router make_unmounted_annotation_api(nmos::model& model, nmos::annotation_patch_merger merge_patch, slog::base_gate& gate_) { using namespace web::http::experimental::listener::api_router_using_declarations; - api_router rwnode_api; + api_router annotation_api; // check for supported API version const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is13_versions::from_settings(model.settings); }); - rwnode_api.support(U(".*"), details::make_api_version_handler(versions, gate_)); + annotation_api.support(U(".*"), details::make_api_version_handler(versions, gate_)); - rwnode_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + annotation_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("node/") }, req, res)); + return pplx::task_from_result(true); + }); + + annotation_api.support(U("/node/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) { set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("self/"), U("devices/"), U("sources/"), U("flows/"), U("senders/"), U("receivers/") }, req, res)); return pplx::task_from_result(true); }); - rwnode_api.support(U("/self/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + annotation_api.support(U("/node/self/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); auto lock = model.read_lock(); @@ -168,12 +174,12 @@ namespace nmos if (resources.end() != resource) { slog::log(gate, SLOG_FLF) << "Returning self resource: " << resource->id; - set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + set_reply(res, status_codes::OK, nmos::make_annotation_response(*resource)); } else { slog::log(gate, SLOG_FLF) << "Self resource not found!"; - set_reply(res, status_codes::InternalError); // rather than Not Found, since the Read/Write Node API doesn't allow a 404 response + set_reply(res, status_codes::InternalError); // rather than Not Found, since the Annotation API doesn't allow a 404 response } return pplx::task_from_result(true); @@ -182,10 +188,10 @@ namespace nmos const web::json::experimental::json_validator validator { nmos::experimental::load_json_schema, - boost::copy_range>(versions | boost::adaptors::transformed(experimental::make_rwnodeapi_resource_core_patch_request_schema_uri)) + boost::copy_range>(versions | boost::adaptors::transformed(experimental::make_annotationapi_resource_core_patch_request_schema_uri)) }; - rwnode_api.support(U("/self/?"), methods::PATCH, [&model, validator, merge_patch, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + annotation_api.support(U("/node/self/?"), methods::PATCH, [&model, validator, merge_patch, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); @@ -193,7 +199,7 @@ namespace nmos { const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); - validator.validate(body, experimental::make_rwnodeapi_resource_core_patch_request_schema_uri(version)); + validator.validate(body, experimental::make_annotationapi_resource_core_patch_request_schema_uri(version)); auto lock = model.write_lock(); auto& resources = model.node_resources; @@ -203,23 +209,23 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Patching self resource: " << resource->id; - details::handle_rwnode_patch(resources, *resource, body, merge_patch, gate); + details::handle_annotation_patch(resources, *resource, body, merge_patch, gate); - set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + set_reply(res, status_codes::OK, nmos::make_annotation_response(*resource)); model.notify(); } else { slog::log(gate, SLOG_FLF) << "Self resource not found!"; - set_reply(res, status_codes::InternalError); // rather than Not Found, since the Read/Write Node API doesn't allow a 404 response + set_reply(res, status_codes::InternalError); // rather than Not Found, since the Annotation API doesn't allow a 404 response } return true; }); }); - rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + annotation_api.support(U("/node/") + nmos::patterns::subresourceType.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); auto lock = model.read_lock(); @@ -235,7 +241,7 @@ namespace nmos web::json::serialize_array(resources | boost::adaptors::filtered(match) | boost::adaptors::transformed( - [&count](const nmos::resources::value_type& resource) { ++count; return nmos::make_rwnode_response(resource); } + [&count](const nmos::resources::value_type& resource) { ++count; return nmos::make_annotation_response(resource); } )), web::http::details::mime_types::application_json); @@ -244,7 +250,7 @@ namespace nmos return pplx::task_from_result(true); }); - rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + annotation_api.support(U("/node/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); auto lock = model.read_lock(); @@ -258,7 +264,7 @@ namespace nmos if (resources.end() != resource) { slog::log(gate, SLOG_FLF) << "Returning " << id_type; - set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + set_reply(res, status_codes::OK, nmos::make_annotation_response(*resource)); } else { @@ -268,7 +274,7 @@ namespace nmos return pplx::task_from_result(true); }); - rwnode_api.support(U("/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::PATCH, [&model, validator, merge_patch, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + annotation_api.support(U("/node/") + nmos::patterns::subresourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::PATCH, [&model, validator, merge_patch, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); @@ -276,7 +282,7 @@ namespace nmos { const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); - validator.validate(body, experimental::make_rwnodeapi_resource_core_patch_request_schema_uri(version)); + validator.validate(body, experimental::make_annotationapi_resource_core_patch_request_schema_uri(version)); auto lock = model.write_lock(); auto& resources = model.node_resources; @@ -290,9 +296,9 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Patching " << id_type; - details::handle_rwnode_patch(resources, *resource, body, merge_patch, gate); + details::handle_annotation_patch(resources, *resource, body, merge_patch, gate); - set_reply(res, status_codes::OK, nmos::make_rwnode_response(*resource)); + set_reply(res, status_codes::OK, nmos::make_annotation_response(*resource)); model.notify(); } @@ -305,6 +311,6 @@ namespace nmos }); }); - return rwnode_api; + return annotation_api; } } diff --git a/Development/nmos/rwnode_api.h b/Development/nmos/annotation_api.h similarity index 53% rename from Development/nmos/rwnode_api.h rename to Development/nmos/annotation_api.h index 5b57a6ffe..c6586a8f4 100644 --- a/Development/nmos/rwnode_api.h +++ b/Development/nmos/annotation_api.h @@ -8,39 +8,39 @@ namespace slog class base_gate; } -// Read/Write Node API implementation +// Annotation API implementation // See https://specs.amwa.tv/is-13/branches/v1.0-dev/APIs/ReadWriteNodeAPI.html namespace nmos { struct model; struct resource; - // Read/Write Node API callbacks + // Annotation API callbacks - // a rwnode_patch_merger validates the specified patch data for the specified IS-04 resource and updates the object to be merged + // an annotation_patch_merger validates the specified patch data for the specified IS-04 resource and updates the object to be merged // or may throw std::runtime_error, which will be mapped to a 500 Internal Error status code with NMOS error "debug" information including the exception message - // (the default patch merger, nmos::merge_rwnode_patch, implements the minimum requirements) - typedef std::function rwnode_patch_merger; + // (the default patch merger, nmos::merge_annotation_patch, implements the minimum requirements) + typedef std::function annotation_patch_merger; - // Read/Write Node API factory functions + // Annotation API factory functions // callbacks from this function are called with the model locked, and may read but should not write directly to the model - web::http::experimental::listener::api_router make_rwnode_api(nmos::model& model, rwnode_patch_merger merge_patch, slog::base_gate& gate); + web::http::experimental::listener::api_router make_annotation_api(nmos::model& model, annotation_patch_merger merge_patch, slog::base_gate& gate); - // Helper functions for the Read/Write Node API callbacks + // Helper functions for the Annotation API callbacks namespace details { - void merge_rwnode_patch(web::json::value& value, const web::json::value& patch); + void merge_annotation_patch(web::json::value& value, const web::json::value& patch); } // this function merges the patch into the value with few additional constraints // i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information // when reset using null, tags are removed, and label and description are set to the empty string // (this is the default patch merger) - inline void merge_rwnode_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch) + inline void merge_annotation_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch) { - details::merge_rwnode_patch(value, patch); + details::merge_annotation_patch(value, patch); } } diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 0b5b2d1ea..976d7f316 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -50,8 +50,8 @@ namespace nmos const route_pattern channelmapping_api = make_route_pattern(U("api"), U("channelmapping")); // IS-09 System API (originally specified in JT-NM TR-1001-1:2018 Annex A) const route_pattern system_api = make_route_pattern(U("api"), U("system")); - // IS-13 Read/Write Node API - const route_pattern rwnode_api = make_route_pattern(U("api"), U("rwnode")); + // IS-13 Annotation API + const route_pattern annotation_api = make_route_pattern(U("api"), U("annotation")); // API version pattern const route_pattern version = make_route_pattern(U("version"), U("v[0-9]+\\.[0-9]+")); diff --git a/Development/nmos/is13_schemas/is13_schemas.h b/Development/nmos/is13_schemas/is13_schemas.h index e6ff520f1..85396e8fb 100644 --- a/Development/nmos/is13_schemas/is13_schemas.h +++ b/Development/nmos/is13_schemas/is13_schemas.h @@ -9,11 +9,12 @@ namespace nmos { namespace v1_0_dev { + extern const char* annotationapi_base; + extern const char* annotationapi_node_base; extern const char* error; extern const char* resource_core; extern const char* resource_core_patch; extern const char* resource_cores; - extern const char* rwnodeapi_base; } } } diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index 4038f284e..2a4e6bbe0 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -142,7 +142,7 @@ namespace nmos using namespace nmos::is13_schemas::v1_0_dev; const utility::string_t tag(_XPLATSTR("v1.0-dev")); - const web::uri rwnodeapi_resource_core_patch_request_uri = make_schema_uri(tag, _XPLATSTR("resource_core_patch.json")); + const web::uri annotationapi_resource_core_patch_request_uri = make_schema_uri(tag, _XPLATSTR("resource_core_patch.json")); } } } @@ -413,9 +413,9 @@ namespace nmos return is08_schemas::v1_0::map_activations_post_request_uri; } - web::uri make_rwnodeapi_resource_core_patch_request_schema_uri(const nmos::api_version& version) + web::uri make_annotationapi_resource_core_patch_request_schema_uri(const nmos::api_version& version) { - return is13_schemas::v1_0::rwnodeapi_resource_core_patch_request_uri; + return is13_schemas::v1_0::annotationapi_resource_core_patch_request_uri; } // load the json schema for the specified base URI diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index e95348e05..7eda1598c 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -29,7 +29,7 @@ namespace nmos web::uri make_channelmappingapi_map_activations_post_request_schema_uri(const nmos::api_version& version); - web::uri make_rwnodeapi_resource_core_patch_request_schema_uri(const nmos::api_version& version); + web::uri make_annotationapi_resource_core_patch_request_schema_uri(const nmos::api_version& version); // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id); diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index d68b84067..85f78efc6 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -44,20 +44,20 @@ namespace nmos data[U("services")] = value::array(); - if (0 <= nmos::fields::rwnode_port(settings)) + if (0 <= nmos::fields::annotation_port(settings)) { for (const auto& version : nmos::is13_versions::from_settings(settings)) { - auto rwnode_uri = web::uri_builder() + auto annotation_uri = web::uri_builder() .set_scheme(nmos::http_scheme(settings)) - .set_port(nmos::fields::rwnode_port(settings)) - .set_path(U("/x-nmos/rwnode/") + make_api_version(version)); - auto type = U("urn:x-nmos:service:rw-node/") + make_api_version(version); + .set_port(nmos::fields::annotation_port(settings)) + .set_path(U("/x-nmos/annotation/") + make_api_version(version)); + auto type = U("urn:x-nmos:service:annotation/") + make_api_version(version); for (const auto& host : hosts) { web::json::push_back(data[U("services")], value_of({ - { U("href"), rwnode_uri.set_host(host).to_uri().to_string() }, + { U("href"), annotation_uri.set_host(host).to_uri().to_string() }, { U("type"), type } })); } diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 97848306c..7c92f38e8 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -10,7 +10,7 @@ #include "nmos/model.h" #include "nmos/node_api.h" #include "nmos/node_behaviour.h" -#include "nmos/rwnode_api.h" +#include "nmos/annotation_api.h" #include "nmos/server.h" #include "nmos/server_utils.h" #include "nmos/settings_api.h" @@ -52,7 +52,7 @@ namespace nmos nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged); node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, gate)); - node_server.api_routers[{ {}, nmos::fields::rwnode_port(node_model.settings) }].mount({}, nmos::make_rwnode_api(node_model, node_implementation.merge_rwnode_patch, gate)); + node_server.api_routers[{ {}, nmos::fields::annotation_port(node_model.settings) }].mount({}, nmos::make_annotation_api(node_model, node_implementation.merge_annotation_patch, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); // Configure the Connection API diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index 379c9e3f0..bace14793 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -9,7 +9,7 @@ #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" #include "nmos/ocsp_response_handler.h" -#include "nmos/rwnode_api.h" +#include "nmos/annotation_api.h" namespace nmos { @@ -57,7 +57,7 @@ namespace nmos node_implementation& on_connection_activated(nmos::connection_activation_handler connection_activated) { this->connection_activated = std::move(connection_activated); return *this; } node_implementation& on_validate_channelmapping_output_map(nmos::details::channelmapping_output_map_validator validate_map) { this->validate_map = std::move(validate_map); return *this; } node_implementation& on_channelmapping_activated(nmos::channelmapping_activation_handler channelmapping_activated) { this->channelmapping_activated = std::move(channelmapping_activated); return *this; } - node_implementation& on_merge_rwnode_patch(nmos::rwnode_patch_merger merge_rwnode_patch) { this->merge_rwnode_patch = std::move(merge_rwnode_patch); return *this; } + node_implementation& on_merge_annotation_patch(nmos::annotation_patch_merger merge_annotation_patch) { this->merge_annotation_patch = std::move(merge_annotation_patch); return *this; } node_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } // deprecated, use on_validate_connection_resource_patch @@ -87,7 +87,7 @@ namespace nmos nmos::channelmapping_activation_handler channelmapping_activated; - nmos::rwnode_patch_merger merge_rwnode_patch; + nmos::annotation_patch_merger merge_annotation_patch; nmos::ocsp_response_handler get_ocsp_response; }; diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index 1b546cfd6..9c0fe67cc 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -70,7 +70,7 @@ namespace nmos if (registry) web::json::insert(settings, std::make_pair(nmos::fields::query_ws_port, ws_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::fields::registration_port, http_port)); web::json::insert(settings, std::make_pair(nmos::fields::node_port, http_port)); - if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::rwnode_port, http_port)); + if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::annotation_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::fields::system_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::connection_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_port, http_port)); diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 02dae4d43..37229e978 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -137,7 +137,7 @@ namespace nmos const web::json::field_as_integer_or events_port{ U("events_port"), 3216 }; const web::json::field_as_integer_or events_ws_port{ U("events_ws_port"), 3217 }; const web::json::field_as_integer_or channelmapping_port{ U("channelmapping_port"), 3215 }; - const web::json::field_as_integer_or rwnode_port{ U("rwnode_port"), 3212 }; + const web::json::field_as_integer_or annotation_port{ U("annotation_port"), 3212 }; // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) const web::json::field_as_integer_or system_port{ U("system_port"), 10641 }; diff --git a/Development/nmos/test/rwnode_api_test.cpp b/Development/nmos/test/annotation_api_test.cpp similarity index 86% rename from Development/nmos/test/rwnode_api_test.cpp rename to Development/nmos/test/annotation_api_test.cpp index 739b9a376..f471fd34c 100644 --- a/Development/nmos/test/rwnode_api_test.cpp +++ b/Development/nmos/test/annotation_api_test.cpp @@ -1,12 +1,12 @@ // The first "test" is of course whether the header compiles standalone -#include "nmos/rwnode_api.h" +#include "nmos/annotation_api.h" #include "bst/test/test.h" #include "nmos/group_hint.h" #include "nmos/json_fields.h" //////////////////////////////////////////////////////////////////////////////////////////// -BST_TEST_CASE(testMergeRwnodePatch) +BST_TEST_CASE(testMergeAnnotationPatch) { using web::json::value; using web::json::value_of; @@ -23,14 +23,14 @@ BST_TEST_CASE(testMergeRwnodePatch) // empty patch { auto merged(source); - nmos::details::merge_rwnode_patch(merged, value::object()); + nmos::details::merge_annotation_patch(merged, value::object()); BST_REQUIRE_EQUAL(source, merged); } // reset everything { auto merged(source); - nmos::details::merge_rwnode_patch(merged, value_of({ + nmos::details::merge_annotation_patch(merged, value_of({ { nmos::fields::label, {} }, { nmos::fields::description, {} }, { nmos::fields::tags, {} } @@ -47,7 +47,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // try to reset read-only tag { auto merged(source); - BST_REQUIRE_THROW(nmos::details::merge_rwnode_patch(merged, value_of({ + BST_REQUIRE_THROW(nmos::details::merge_annotation_patch(merged, value_of({ { nmos::fields::tags, value_of({ { nmos::fields::group_hint, {} } }) } @@ -57,7 +57,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // try to update read-only tag { auto merged(source); - BST_REQUIRE_THROW(nmos::details::merge_rwnode_patch(merged, value_of({ + BST_REQUIRE_THROW(nmos::details::merge_annotation_patch(merged, value_of({ { nmos::fields::tags, value_of({ { nmos::fields::group_hint, value_of({ nmos::make_group_hint({ U("qux"), U("quux") }) }) } }) } @@ -67,7 +67,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // add and remove tags { auto merged(source); - nmos::details::merge_rwnode_patch(merged, value_of({ + nmos::details::merge_annotation_patch(merged, value_of({ { nmos::fields::tags, value_of({ { U("foo"), {} }, { U("bar"), value_of({ U("woof"), U("bark") }) }, @@ -86,7 +86,7 @@ BST_TEST_CASE(testMergeRwnodePatch) // change label, description and tags { auto merged(source); - nmos::details::merge_rwnode_patch(merged, value_of({ + nmos::details::merge_annotation_patch(merged, value_of({ { nmos::fields::label, U("woof") }, { nmos::fields::description, U("bark") }, { nmos::fields::tags, value_of({ diff --git a/Development/third_party/README.md b/Development/third_party/README.md index 15742001a..fe42ecd63 100644 --- a/Development/third_party/README.md +++ b/Development/third_party/README.md @@ -19,6 +19,6 @@ Third-party source files used by the nmos-cpp libraries - [is-09](is-09) The JSON Schema files used for validation of System API requests and responses - [is-13](is-13) - The JSON Schema files used for validation of Read/Write Node API requests and responses + The JSON Schema files used for validation of Annotation API requests and responses - [WpdPack](WpdPack) Libraries and header files from the [WinPcap](https://www.winpcap.org/) Developer's Pack diff --git a/Development/third_party/is-13/README.md b/Development/third_party/is-13/README.md index 1d0aa096c..d6fa7c7b2 100644 --- a/Development/third_party/is-13/README.md +++ b/Development/third_party/is-13/README.md @@ -1,6 +1,6 @@ -# AMWA IS-13 NMOS Read/Write Node Specification +# AMWA IS-13 NMOS Annotation Specification -This directory contains files from the [AMWA IS-13 NMOS Read/Write Node Specification](https://github.com/AMWA-TV/is-rwnode), in particular tagged versions of the JSON schemas used by the API specifications. +This directory contains files from the [AMWA IS-13 NMOS Annotation Specification](https://github.com/AMWA-TV/is-13), in particular tagged versions of the JSON schemas used by the API specifications. Original source code: diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-base.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-base.json new file mode 100644 index 000000000..2c846e14f --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-base.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Describes the Annotation API base resource", + "title": "Annotation API base resource", + "items": { + "type": "string", + "enum": [ + "node/" + ] + }, + "minItems": 1, + "maxItems": 1, + "uniqueItems": true +} diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-node-base.json similarity index 72% rename from Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json rename to Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-node-base.json index ca31ed51f..9d89a6399 100644 --- a/Development/third_party/is-13/v1.0-dev/APIs/schemas/rwnodeapi-base.json +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/annotationapi-node-base.json @@ -1,8 +1,8 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "description": "Describes the Read/Write Node API base resource", - "title": "Read/Write Node API base resource", + "description": "Describes the Annotation API node resource", + "title": "Annotation API node resource", "items": { "type": "string", "enum": [ diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json index c587640bb..e49745c76 100644 --- a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_core_patch.json @@ -10,7 +10,7 @@ "type": ["null", "string"] }, "description": { - "description": "Detailed description of the resource. Set to null to restore default label.", + "description": "Detailed description of the resource. Set to null to restore default description.", "type": ["null", "string"] }, "tags": { diff --git a/README.md b/README.md index 191d421c8..214e2f458 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA IS-07 NMOS Event & Tally Specification](https://specs.amwa.tv/is-07/) - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) - [AMWA IS-09 NMOS System Parameters Specification](https://specs.amwa.tv/is-09/) (originally defined in JT-NM TR-1001-1:2018 Annex A) -- [AMWA IS-13 NMOS Read/Write Node Specification](https://specs.amwa.tv/is-13/) +- [AMWA IS-13 NMOS Annotation Specification](https://specs.amwa.tv/is-13/) - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) - [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) From 674bb36329633245b35c7ccfd1dad97e337d4494 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Mon, 22 May 2023 12:24:38 +0100 Subject: [PATCH 13/15] More flexible default implementation of annotation_patch_merger --- .../nmos-cpp-node/node_implementation.cpp | 18 +++++++++--- Development/nmos/annotation_api.cpp | 29 +++++++++++++------ Development/nmos/annotation_api.h | 16 ++++++---- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 85df5bca5..b1c252891 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -1270,13 +1270,23 @@ nmos::channelmapping_activation_handler make_node_implementation_channelmapping_ } // Example Annotation API patch callback to update resource labels, descriptions and tags -nmos::annotation_patch_merger make_node_implementation_annotation_patch_merger(slog::base_gate& gate) +nmos::annotation_patch_merger make_node_implementation_annotation_patch_merger(const nmos::settings& settings, slog::base_gate& gate) { - return [&gate](const nmos::resource& resource, web::json::value& value, const web::json::value& patch) + using web::json::value; + using web::json::value_of; + + return [&settings, &gate](const nmos::resource& resource, web::json::value& value, const web::json::value& patch) { const std::pair id_type{ resource.id, resource.type }; slog::log(gate, SLOG_FLF) << nmos::stash_category(impl::categories::node_implementation) << "Updating " << id_type; - nmos::details::merge_annotation_patch(value, patch); + // this example uses the specified tags for node and device resources as defaults + const auto default_tags + = id_type.second == nmos::types::node ? impl::fields::node_tags(settings) + : id_type.second == nmos::types::device ? impl::fields::device_tags(settings) + : value::object(); + // and uses the default predicate for read-only tags + nmos::details::merge_annotation_patch(value, patch, &nmos::details::is_read_only_tag, value_of({ { nmos::fields::tags, default_tags } })); + // this example does not save the new values to persistent storage or e.g. reject values that are too large }; } @@ -1434,5 +1444,5 @@ nmos::experimental::node_implementation make_node_implementation(nmos::node_mode .on_connection_activated(make_node_implementation_connection_activation_handler(model, gate)) .on_validate_channelmapping_output_map(make_node_implementation_map_validator()) // may be omitted if not required .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)) - .on_merge_annotation_patch(make_node_implementation_annotation_patch_merger(gate)); // may be omitted if not required + .on_merge_annotation_patch(make_node_implementation_annotation_patch_merger(model.settings, gate)); // may be omitted if not required } diff --git a/Development/nmos/annotation_api.cpp b/Development/nmos/annotation_api.cpp index 0cf638672..0f71a3e45 100644 --- a/Development/nmos/annotation_api.cpp +++ b/Development/nmos/annotation_api.cpp @@ -67,29 +67,35 @@ namespace nmos namespace details { + // BCP-002-01 Group Hint tag and BCP-002-02 Asset Distinguishing Information tags are read-only + // all other tags are read/write bool is_read_only_tag(const utility::string_t& key) { return boost::algorithm::starts_with(key, U("urn:x-nmos:tag:asset:")) || boost::algorithm::starts_with(key, U("urn:x-nmos:tag:grouphint/")); } - void merge_annotation_patch(web::json::value& value, const web::json::value& patch) + const web::json::field_as_string_or default_label{ nmos::fields::label.key, U("") }; + const web::json::field_as_string_or default_description{ nmos::fields::description.key, U("") }; + const web::json::field_as_value_or default_tags{ nmos::fields::tags.key, web::json::value::object() }; + + void merge_annotation_patch(web::json::value& value, const web::json::value& patch, annotation_tag_predicate is_read_only_tag, const web::json::value& default_value) { // reject changes to read-ony tags if (patch.has_object_field(nmos::fields::tags)) { const auto& tags = nmos::fields::tags(patch); - auto patch_readonly = std::find_if(tags.begin(), tags.end(), [](const std::pair& field) + auto patch_readonly = std::find_if(tags.begin(), tags.end(), [&](const std::pair& field) { return is_read_only_tag(field.first); }); if (tags.end() != patch_readonly) throw std::runtime_error("cannot patch read-only tag: " + utility::us2s(patch_readonly->first)); } - // save existing read-only tags + // save existing read-only tags (so that read-only tags don't need to be included in default_value) - auto readonly_tags = web::json::value_from_fields(nmos::fields::tags(value) | boost::adaptors::filtered([](const std::pair& field) + auto readonly_tags = web::json::value_from_fields(nmos::fields::tags(value) | boost::adaptors::filtered([&](const std::pair& field) { return is_read_only_tag(field.first); })); @@ -100,16 +106,21 @@ namespace nmos // apply defaults to properties that have been reset - web::json::insert(value, std::make_pair(nmos::fields::label, U(""))); - web::json::insert(value, std::make_pair(nmos::fields::description, U(""))); + web::json::insert(value, std::make_pair(nmos::fields::label, details::default_label(default_value))); + web::json::insert(value, std::make_pair(nmos::fields::description, details::default_description(default_value))); web::json::insert(value, std::make_pair(nmos::fields::tags, readonly_tags)); + auto& tags = value.at(nmos::fields::tags); + for (const auto& default_tag : details::default_tags(default_value).as_object()) + { + web::json::insert(tags, default_tag); + } } void assign_annotation_patch(web::json::value& value, web::json::value&& patch) { - if (value.has_string_field(nmos::fields::label)) value[nmos::fields::label] = std::move(patch.at(nmos::fields::label)); - if (value.has_string_field(nmos::fields::description)) value[nmos::fields::description] = std::move(patch.at(nmos::fields::description)); - if (value.has_object_field(nmos::fields::tags)) value[nmos::fields::tags] = std::move(patch.at(nmos::fields::tags)); + if (patch.has_string_field(nmos::fields::label)) value[nmos::fields::label] = std::move(patch.at(nmos::fields::label)); + if (patch.has_string_field(nmos::fields::description)) value[nmos::fields::description] = std::move(patch.at(nmos::fields::description)); + if (patch.has_object_field(nmos::fields::tags)) value[nmos::fields::tags] = std::move(patch.at(nmos::fields::tags)); } void handle_annotation_patch(nmos::resources& resources, const nmos::resource& resource, const web::json::value& patch, const nmos::annotation_patch_merger& merge_patch, slog::base_gate& gate) diff --git a/Development/nmos/annotation_api.h b/Development/nmos/annotation_api.h index c6586a8f4..53c881907 100644 --- a/Development/nmos/annotation_api.h +++ b/Development/nmos/annotation_api.h @@ -31,13 +31,19 @@ namespace nmos namespace details { - void merge_annotation_patch(web::json::value& value, const web::json::value& patch); + typedef std::function annotation_tag_predicate; + + // BCP-002-01 Group Hint tag and BCP-002-02 Asset Distinguishing Information tags are read-only + // all other tags are read/write + bool is_read_only_tag(const utility::string_t& name); + + // this function merges the patch into the value with few additional constraints + // when any fields are reset using null, default values are applied if specified or + // read-write tags are removed, and label and description are set to the empty string + void merge_annotation_patch(web::json::value& value, const web::json::value& patch, annotation_tag_predicate is_read_only_tag = &nmos::details::is_read_only_tag, const web::json::value& default_value = {}); } - // this function merges the patch into the value with few additional constraints - // i.e. label, description and all tags are read/write except Group Hint and Asset Distinguishing Information - // when reset using null, tags are removed, and label and description are set to the empty string - // (this is the default patch merger) + // this is the default patch merger inline void merge_annotation_patch(const nmos::resource& resource, web::json::value& value, const web::json::value& patch) { details::merge_annotation_patch(value, patch); From b7c2e57a7fe04b3f47bd2f7b30f7ba392709f830 Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley Date: Tue, 23 May 2023 12:44:01 +0100 Subject: [PATCH 14/15] Update per https://github.com/AMWA-TV/is-13/pull/21 --- Development/cmake/NmosCppLibraries.cmake | 2 +- Development/nmos/annotation_api.cpp | 29 ++++++++++++++----- Development/nmos/is13_schemas/is13_schemas.h | 2 +- .../v1.0-dev/APIs/schemas/resource-list.json | 11 +++++++ .../v1.0-dev/APIs/schemas/resource_cores.json | 10 ------- 5 files changed, 35 insertions(+), 19 deletions(-) create mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/resource-list.json delete mode 100644 Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index ecf845a14..c255d6c15 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -700,7 +700,7 @@ set(NMOS_IS13_V1_0_SCHEMAS_JSON third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/error.json third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_core.json third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_core_patch.json - third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource_cores.json + third_party/is-13/${NMOS_IS13_V1_0_TAG}/APIs/schemas/resource-list.json ) set(NMOS_IS13_SCHEMAS_JSON_MATCH "third_party/is-13/([^/]+)/APIs/schemas/([^;]+)\\.json") diff --git a/Development/nmos/annotation_api.cpp b/Development/nmos/annotation_api.cpp index 0f71a3e45..9857cff90 100644 --- a/Development/nmos/annotation_api.cpp +++ b/Development/nmos/annotation_api.cpp @@ -248,13 +248,28 @@ namespace nmos size_t count = 0; - set_reply(res, status_codes::OK, - web::json::serialize_array(resources - | boost::adaptors::filtered(match) - | boost::adaptors::transformed( - [&count](const nmos::resources::value_type& resource) { ++count; return nmos::make_annotation_response(resource); } - )), - web::http::details::mime_types::application_json); + // experimental extension, to support human-readable HTML rendering of NMOS responses + if (experimental::details::is_html_response_preferred(req, web::http::details::mime_types::application_json)) + { + set_reply(res, status_codes::OK, + web::json::serialize_array(resources + | boost::adaptors::filtered(match) + | boost::adaptors::transformed( + [&count, &req](const nmos::resource& resource) { ++count; return experimental::details::make_html_response_a_tag(resource.id + U("/"), req); } + )), + web::http::details::mime_types::application_json); + } + else + { + set_reply(res, status_codes::OK, + web::json::serialize_array(resources + | boost::adaptors::filtered(match) + | boost::adaptors::transformed( + [&count](const nmos::resource& resource) { ++count; return value(resource.id + U("/")); } + ) + ), + web::http::details::mime_types::application_json); + } slog::log(gate, SLOG_FLF) << "Returning " << count << " matching " << resourceType; diff --git a/Development/nmos/is13_schemas/is13_schemas.h b/Development/nmos/is13_schemas/is13_schemas.h index 85396e8fb..6a3f76634 100644 --- a/Development/nmos/is13_schemas/is13_schemas.h +++ b/Development/nmos/is13_schemas/is13_schemas.h @@ -14,7 +14,7 @@ namespace nmos extern const char* error; extern const char* resource_core; extern const char* resource_core_patch; - extern const char* resource_cores; + extern const char* resource_list; } } } diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource-list.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource-list.json new file mode 100644 index 000000000..7239fc400 --- /dev/null +++ b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource-list.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "List of resource ID paths", + "title": "Resources base resource", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/$" + }, + "uniqueItems": true +} diff --git a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json b/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json deleted file mode 100644 index e71271693..000000000 --- a/Development/third_party/is-13/v1.0-dev/APIs/schemas/resource_cores.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "array", - "description": "A list of resources", - "title": "Collection of resources", - "items": { - "$ref": "resource_core.json" - }, - "uniqueItems": true -} From 04ac4fd0893828a3d197b1c6c12f7c70e8990dda Mon Sep 17 00:00:00 2001 From: Gareth Sylvester-Bradley <31761158+garethsb@users.noreply.github.com> Date: Wed, 2 Aug 2023 23:05:08 +0100 Subject: [PATCH 15/15] Fix comment --- Development/nmos/annotation_api.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Development/nmos/annotation_api.h b/Development/nmos/annotation_api.h index 53c881907..9986bd070 100644 --- a/Development/nmos/annotation_api.h +++ b/Development/nmos/annotation_api.h @@ -9,7 +9,7 @@ namespace slog } // Annotation API implementation -// See https://specs.amwa.tv/is-13/branches/v1.0-dev/APIs/ReadWriteNodeAPI.html +// See https://specs.amwa.tv/is-13/branches/v1.0-dev/APIs/AnnotationAPI.html namespace nmos { struct model;