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 944eb990..5f266c03 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 5bf63a9f..8ebe3740 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 0db8898b..0b5b2d1e 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 00000000..e6ff520f --- /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 00000000..3c33a506 --- /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 fdd70581..405e6d24 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 e938a513..e95348e0 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 ecc75c46..94558f82 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 00000000..0314aea7 --- /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 00000000..1ba1775f --- /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 cf55267f..3287ede1 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 00000000..1d0aa096 --- /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 00000000..402147b5 --- /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 00000000..4bb69f8d --- /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 00000000..721527f8 --- /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 00000000..e7127169 --- /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 00000000..ca31ed51 --- /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 405e6d24..4038f284 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 1ba1775f..6b8e247e 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 721527f8..c587640b 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 94558f82..b4ad0b6b 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 dc8f4efa..379c9e3f 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 0314aea7..afacf73f 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 6b8e247e..acbe72b8 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 02db1490..6141ea40 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 afacf73f..1c3aaef0 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 acbe72b8..c863cda5 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 00000000..b0c160dc --- /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 2788ca9e..15742001 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 05039492..191d421c 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 b0c160dc..739b9a37 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 dc3ee9d2..6104944f 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 1c3aaef0..10b4793f 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 c863cda5..5b57a6ff 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 8ebe3740..715b7238 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 7b65b25b..77f2ddea 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 b4ad0b6b..97848306 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 c8943c93..1b546cfd 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 3287ede1..02dae4d4 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 77f2ddea..d68b8406 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 7c75cf01..69056819 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 3c33a506..29e1a30f 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 10b4793f..42018f0c 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 5f266c03..ecf845a1 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 6141ea40..f33f98e4 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 715b7238..f9a8b481 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 6104944f..85df5bca 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 42018f0c..0cf63867 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 5b57a6ff..c6586a8f 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 0b5b2d1e..976d7f31 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 e6ff520f..85396e8f 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 4038f284..2a4e6bbe 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 e95348e0..7eda1598 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 d68b8406..85f78efc 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 97848306..7c92f38e 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 379c9e3f..bace1479 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 1b546cfd..9c0fe67c 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 02dae4d4..37229e97 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 739b9a37..f471fd34 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 15742001..fe42ecd6 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 1d0aa096..d6fa7c7b 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 00000000..2c846e14 --- /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 ca31ed51..9d89a639 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 c587640b..e49745c7 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 191d421c..214e2f45 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 85df5bca..b1c25289 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 0cf63867..0f71a3e4 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 c6586a8f..53c88190 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 ecf845a1..c255d6c1 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 0f71a3e4..9857cff9 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 85396e8f..6a3f7663 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 00000000..7239fc40 --- /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 e7127169..00000000 --- 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 53c88190..9986bd07 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;