From a57c26d9b50607b06441cbe55b431cf02b539bd1 Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Mon, 15 Sep 2025 17:11:05 +0800 Subject: [PATCH 01/10] feat: creates a general schema validator for vda5050 messages Signed-off-by: Shawn Chan --- CMakeLists.txt | 1 + include/vda5050_msgs/json_utils/schemas.hpp | 8 +++ .../vda5050_msgs/json_utils/validators.hpp | 34 +++++++++++ json_schemas/schemas.cpp | 57 +++++++++++++++++++ package.xml | 1 + 5 files changed, 101 insertions(+) create mode 100644 include/vda5050_msgs/json_utils/schemas.hpp create mode 100644 include/vda5050_msgs/json_utils/validators.hpp create mode 100644 json_schemas/schemas.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88b515b..c4d8250 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ endif() find_package(ament_cmake REQUIRED) find_package(rosidl_default_generators REQUIRED) find_package(nlohmann_json REQUIRED) +find_package(nlohmann_json_schema_validator REQUIRED) # Generate VDA5050 ROS 2 messages set(msg_files diff --git a/include/vda5050_msgs/json_utils/schemas.hpp b/include/vda5050_msgs/json_utils/schemas.hpp new file mode 100644 index 0000000..995f615 --- /dev/null +++ b/include/vda5050_msgs/json_utils/schemas.hpp @@ -0,0 +1,8 @@ +#ifndef VDA5050_MSGS__JSON_UTILS__SCHEMAS_HPP_ +#define VDA5050_MSGS__JSON_UTILS__SCHEMAS_HPP_ + +#include + +extern const char* connection_schema; + +#endif \ No newline at end of file diff --git a/include/vda5050_msgs/json_utils/validators.hpp b/include/vda5050_msgs/json_utils/validators.hpp new file mode 100644 index 0000000..4f9184a --- /dev/null +++ b/include/vda5050_msgs/json_utils/validators.hpp @@ -0,0 +1,34 @@ +#include +#include +#include +#include "include/vda5050_msgs/json_utils/schemas.hpp" + +/// @brief Checks that a JSON object is following the specified schema +/// +/// @param schema The schema that the JSON object is to follow +/// @param j Reference to the JSON object to be validated +/// @return +int validate_schema(nlohmann::json schema, nlohmann::json& j) +{ + nlohmann::json_schema::json_validator validator; + + try + { + validator.set_root_schema(schema); + } + catch (const std::exception &e) + { + std::cerr << "Validation of schema failed: " << e.what() << "\n"; + return EXIT_FAILURE; + } + + try + { + validator.validate(j); + } + catch(const std::exception& e) + { + std::cerr << e.what() << '\n'; + } + return EXIT_SUCCESS; +} diff --git a/json_schemas/schemas.cpp b/json_schemas/schemas.cpp new file mode 100644 index 0000000..d10becc --- /dev/null +++ b/json_schemas/schemas.cpp @@ -0,0 +1,57 @@ +#include "include/vda5050_msgs/json_utils/schemas.hpp" + +const char* connection_schema = R"( + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "connection", + "description": "The last will message of the AGV. Has to be sent with retain flag.\nOnce the AGV comes online, it has to send this message on its connect topic, with the connectionState enum set to \"ONLINE\".\n The last will message is to be configured with the connection state set to \"CONNECTIONBROKEN\".\nThus, if the AGV disconnects from the broker, master control gets notified via the topic \"connection\".\nIf the AGV is disconnecting in an orderly fashion (e.g. shutting down, sleeping), the AGV is to publish a message on this topic with the connectionState set to \"DISCONNECTED\".", + "subtopic": "/connection", + "type": "object", + "required": [ + "headerId", + "timestamp", + "version", + "manufacturer", + "serialNumber", + "connectionState" + ], + "properties": { + "headerId": { + "type": "integer", + "description": "Header ID of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ssZ).", + "examples": [ + "1991-03-11T11:40:03.12Z" + ] + }, + "version": { + "type": "string", + "description": "Version of the protocol [Major].[Minor].[Patch]", + "examples": [ + "1.3.2" + ] + }, + "manufacturer": { + "type": "string", + "description": "Manufacturer of the AGV." + }, + "serialNumber": { + "type": "string", + "description": "Serial number of the AGV." + }, + "connectionState": { + "type": "string", + "enum": [ + "ONLINE", + "OFFLINE", + "CONNECTIONBROKEN" + ], + "description": "ONLINE: connection between AGV and broker is active. OFFLINE: connection between AGV and broker has gone offline in a coordinated way. CONNECTIONBROKEN: The connection between AGV and broker has unexpectedly ended." + } + } + } +)"; \ No newline at end of file diff --git a/package.xml b/package.xml index 3bf6e8c..7aaf06d 100644 --- a/package.xml +++ b/package.xml @@ -11,6 +11,7 @@ rosidl_default_generators nlohmann-json-dev + nlohmann-json-schema-validator-dev rosidl_default_runtime From f8f38f3c77694c2b293b054c6ef1390241440d43 Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Thu, 18 Sep 2025 10:45:25 +0800 Subject: [PATCH 02/10] feat: creates validator function Signed-off-by: Shawn Chan --- include/vda5050_msgs/json_utils/validators.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/vda5050_msgs/json_utils/validators.hpp b/include/vda5050_msgs/json_utils/validators.hpp index 4f9184a..77dc1cc 100644 --- a/include/vda5050_msgs/json_utils/validators.hpp +++ b/include/vda5050_msgs/json_utils/validators.hpp @@ -5,9 +5,9 @@ /// @brief Checks that a JSON object is following the specified schema /// -/// @param schema The schema that the JSON object is to follow -/// @param j Reference to the JSON object to be validated -/// @return +/// @param schema The schema to validate against, as an nlohmann::json object +/// @param j Reference to the nlohmann::json object to be validated +/// @return EXIT_FAILURE or EXIT_SUCCESS int validate_schema(nlohmann::json schema, nlohmann::json& j) { nlohmann::json_schema::json_validator validator; From 87ca78db35c44445e9125c71a19887bb451099be Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Thu, 18 Sep 2025 12:08:56 +0800 Subject: [PATCH 03/10] feat: adds utility functions to generator.hpp Signed-off-by: Shawn Chan --- test/generator/generator.hpp | 70 ++++++++++++++++++++++++++++++++ test/generator/jsonGenerator.hpp | 15 +++++++ test/test_json_validators.cpp | 20 +++++++++ 3 files changed, 105 insertions(+) create mode 100644 test/generator/jsonGenerator.hpp create mode 100644 test/test_json_validators.cpp diff --git a/test/generator/generator.hpp b/test/generator/generator.hpp index 4ec7b06..585baa0 100644 --- a/test/generator/generator.hpp +++ b/test/generator/generator.hpp @@ -62,6 +62,18 @@ class RandomDataGenerator return uint_dist_(rng_); } + /// \brief Generate a random 64-bit floating-point number + double generate_random_float() + { + return float_dist_(rng_); + } + + /// \brief Generate a random boolean value + bool generate_random_bool() + { + return bool_dist_(rng_); + } + /// \brief Generate a random alphanumerical string with length upto 50 std::string generate_random_string() { @@ -97,6 +109,51 @@ class RandomDataGenerator return states[state_idx]; } + /// \brief Generate a random index for enum selection + uint8_t generate_random_index(size_t size) + { + std::uniform_int_distribution index_dist(0, size - 1); + return index_dist(rng_); + } + + /// \brief Generate a random vector of type float64 + std::vector generate_random_float_vector(const uint8_t size) + { + std::vector vec(size); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + *it = generate_random_float(); + } + return vec; + } + + /// \brief Generate a random vector of type T + template + std::vector generate_random_vector(const uint8_t size) + { + std::vector vec(size); + for (auto it = vec.begin(); it != vec.end(); ++it) + { + *it = generate(); + } + return vec; + } + + /// \brief + uint8_t generate_random_size() + { + return size_dist_(rng_); + } + + /// \brief Generate a random connection state value + std::string generate_connection_state() + { + std::vector states = { + Connection::ONLINE, Connection::OFFLINE, Connection::CONNECTIONBROKEN}; + auto state_idx = connection_state_dist_(rng_); + return states[state_idx]; + } + /// \brief Generate a fully populated message of a supported type template T generate() @@ -133,6 +190,13 @@ class RandomDataGenerator /// \brief Distribution for unsigned 32-bit integers std::uniform_int_distribution uint_dist_; + /// \brief Distribution for 64-bit floating-point numbers + std::uniform_real_distribution float_dist_; + + /// \brief Distribution for a boolean value + /// TODO (@shawnkchan): KIV should we be bounding this between 0 and 1? + std::uniform_int_distribution bool_dist_{0, 1}; + /// \brief Distribution for random string lengths std::uniform_int_distribution string_length_dist_; @@ -141,6 +205,12 @@ class RandomDataGenerator /// \brief Distribution for VDA 5050 connectionState std::uniform_int_distribution connection_state_dist_; + + /// \brief Distribution for random vector size + std::uniform_int_distribution size_dist_; + + /// \brief Upper bound for order.nodes and order.edges random vector; + uint8_t ORDER_VECTOR_SIZE_UPPER_BOUND = 10; }; #endif // TEST__GENERATOR__GENERATOR_HPP_ diff --git a/test/generator/jsonGenerator.hpp b/test/generator/jsonGenerator.hpp new file mode 100644 index 0000000..d752434 --- /dev/null +++ b/test/generator/jsonGenerator.hpp @@ -0,0 +1,15 @@ +#ifndef TEST__GENERATOR__JSON_GENERATOR_HPP_ +#define TEST__GENERATOR__JSON_GENERATOR_HPP_ + +#include "generator.hpp" + +class RandomJSONgenerator +{ +public: + + +private: + +} + +#endif \ No newline at end of file diff --git a/test/test_json_validators.cpp b/test/test_json_validators.cpp new file mode 100644 index 0000000..8487398 --- /dev/null +++ b/test/test_json_validators.cpp @@ -0,0 +1,20 @@ +#include +#include + +#include "vda5050_msgs/json_utils/validators.hpp" + +class JsonValidatorTest : public ::testing::Test +{ +protected: + JsonValidatorTest() + { + + } + + nlohmann::json +} + +TEST(JsonValidatorTest, basicValidationTest) +{ + +} \ No newline at end of file From 66ad1008c9f1f57edfd02c68df9657061d860bfd Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Thu, 18 Sep 2025 14:29:38 +0800 Subject: [PATCH 04/10] feat: adds enums for vda5050 objects Signed-off-by: Shawn Chan --- test/generator/jsonGenerator.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/generator/jsonGenerator.hpp b/test/generator/jsonGenerator.hpp index d752434..3c8448b 100644 --- a/test/generator/jsonGenerator.hpp +++ b/test/generator/jsonGenerator.hpp @@ -1,14 +1,27 @@ #ifndef TEST__GENERATOR__JSON_GENERATOR_HPP_ #define TEST__GENERATOR__JSON_GENERATOR_HPP_ +#include + #include "generator.hpp" +/// \brief Utility class to generate random VDA 5050 JSON objects class RandomJSONgenerator { public: + /// \brief Generate a fully populated JSON object of a supported type + nlohmann::json generate() + { + + } private: + enum class JsonTypes { + Connection, + Order, + + } } From 69c7b3248ce3c7757cb9514e7acfbd5fc9042380 Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Mon, 22 Sep 2025 17:54:31 +0800 Subject: [PATCH 05/10] feat: create test case for validator Signed-off-by: Shawn Chan --- CMakeLists.txt | 16 ++++++ include/vda5050_msgs/json_utils/schemas.hpp | 57 ++++++++++++++++++- .../vda5050_msgs/json_utils/validators.hpp | 2 +- json_schemas/schemas.cpp | 57 ------------------- test/generator/generator.hpp | 9 --- test/generator/jsonGenerator.hpp | 44 +++++++++++--- test/test_json_validators.cpp | 21 +++++-- 7 files changed, 123 insertions(+), 83 deletions(-) delete mode 100644 json_schemas/schemas.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c4d8250..a7784f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ install( ament_export_dependencies( rosidl_default_runtime nlohmann_json + nlohmann_json_schema_validator ) if(BUILD_TESTING) @@ -75,6 +76,21 @@ if(BUILD_TESTING) nlohmann_json::nlohmann_json ${cpp_typesupport_target} ) + + ament_add_gtest(${PROJECT_NAME}_validator_test + test/test_json_validators.cpp + + ) + target_include_directories(${PROJECT_NAME}_validator_test + PUBLIC + $ + $ + ) + target_link_libraries(${PROJECT_NAME}_validator_test + nlohmann_json::nlohmann_json + nlohmann_json_schema_validator + ${cpp_typesupport_target} + ) endif() ament_package() diff --git a/include/vda5050_msgs/json_utils/schemas.hpp b/include/vda5050_msgs/json_utils/schemas.hpp index 995f615..4b55101 100644 --- a/include/vda5050_msgs/json_utils/schemas.hpp +++ b/include/vda5050_msgs/json_utils/schemas.hpp @@ -3,6 +3,61 @@ #include -extern const char* connection_schema; +/// \brief Schema of the VDA5050 Connection Object +inline constexpr auto connection_schema = R"( + { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "connection", + "description": "The last will message of the AGV. Has to be sent with retain flag.\nOnce the AGV comes online, it has to send this message on its connect topic, with the connectionState enum set to \"ONLINE\".\n The last will message is to be configured with the connection state set to \"CONNECTIONBROKEN\".\nThus, if the AGV disconnects from the broker, master control gets notified via the topic \"connection\".\nIf the AGV is disconnecting in an orderly fashion (e.g. shutting down, sleeping), the AGV is to publish a message on this topic with the connectionState set to \"DISCONNECTED\".", + "subtopic": "/connection", + "type": "object", + "required": [ + "headerId", + "timestamp", + "version", + "manufacttimurer", + "serialNumber", + "connectionState" + ], + "properties": { + "headerId": { + "type": "integer", + "description": "Header ID of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message." + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ssZ).", + "examples": [ + "1991-03-11T11:40:03.12Z" + ] + }, + "version": { + "type": "string", + "description": "Version of the protocol [Major].[Minor].[Patch]", + "examples": [ + "1.3.2" + ] + }, + "manufacturer": { + "type": "string", + "description": "Manufacturer of the AGV." + }, + "serialNumber": { + "type": "string", + "description": "Serial number of the AGV." + }, + "connectionState": { + "type": "string", + "enum": [ + "ONLINE", + "OFFLINE", + "CONNECTIONBROKEN" + ], + "description": "ONLINE: connection between AGV and broker is active. OFFLINE: connection between AGV and broker has gone offline in a coordinated way. CONNECTIONBROKEN: The connection between AGV and broker has unexpectedly ended." + } + } + } +)"; #endif \ No newline at end of file diff --git a/include/vda5050_msgs/json_utils/validators.hpp b/include/vda5050_msgs/json_utils/validators.hpp index 77dc1cc..d6f5ca6 100644 --- a/include/vda5050_msgs/json_utils/validators.hpp +++ b/include/vda5050_msgs/json_utils/validators.hpp @@ -1,7 +1,7 @@ #include #include #include -#include "include/vda5050_msgs/json_utils/schemas.hpp" +#include "vda5050_msgs/json_utils/schemas.hpp" /// @brief Checks that a JSON object is following the specified schema /// diff --git a/json_schemas/schemas.cpp b/json_schemas/schemas.cpp deleted file mode 100644 index d10becc..0000000 --- a/json_schemas/schemas.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "include/vda5050_msgs/json_utils/schemas.hpp" - -const char* connection_schema = R"( - { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "connection", - "description": "The last will message of the AGV. Has to be sent with retain flag.\nOnce the AGV comes online, it has to send this message on its connect topic, with the connectionState enum set to \"ONLINE\".\n The last will message is to be configured with the connection state set to \"CONNECTIONBROKEN\".\nThus, if the AGV disconnects from the broker, master control gets notified via the topic \"connection\".\nIf the AGV is disconnecting in an orderly fashion (e.g. shutting down, sleeping), the AGV is to publish a message on this topic with the connectionState set to \"DISCONNECTED\".", - "subtopic": "/connection", - "type": "object", - "required": [ - "headerId", - "timestamp", - "version", - "manufacturer", - "serialNumber", - "connectionState" - ], - "properties": { - "headerId": { - "type": "integer", - "description": "Header ID of the message. The headerId is defined per topic and incremented by 1 with each sent (but not necessarily received) message." - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "Timestamp in ISO8601 format (YYYY-MM-DDTHH:mm:ss.ssZ).", - "examples": [ - "1991-03-11T11:40:03.12Z" - ] - }, - "version": { - "type": "string", - "description": "Version of the protocol [Major].[Minor].[Patch]", - "examples": [ - "1.3.2" - ] - }, - "manufacturer": { - "type": "string", - "description": "Manufacturer of the AGV." - }, - "serialNumber": { - "type": "string", - "description": "Serial number of the AGV." - }, - "connectionState": { - "type": "string", - "enum": [ - "ONLINE", - "OFFLINE", - "CONNECTIONBROKEN" - ], - "description": "ONLINE: connection between AGV and broker is active. OFFLINE: connection between AGV and broker has gone offline in a coordinated way. CONNECTIONBROKEN: The connection between AGV and broker has unexpectedly ended." - } - } - } -)"; \ No newline at end of file diff --git a/test/generator/generator.hpp b/test/generator/generator.hpp index 585baa0..7008708 100644 --- a/test/generator/generator.hpp +++ b/test/generator/generator.hpp @@ -145,15 +145,6 @@ class RandomDataGenerator return size_dist_(rng_); } - /// \brief Generate a random connection state value - std::string generate_connection_state() - { - std::vector states = { - Connection::ONLINE, Connection::OFFLINE, Connection::CONNECTIONBROKEN}; - auto state_idx = connection_state_dist_(rng_); - return states[state_idx]; - } - /// \brief Generate a fully populated message of a supported type template T generate() diff --git a/test/generator/jsonGenerator.hpp b/test/generator/jsonGenerator.hpp index 3c8448b..743c844 100644 --- a/test/generator/jsonGenerator.hpp +++ b/test/generator/jsonGenerator.hpp @@ -10,19 +10,45 @@ class RandomJSONgenerator { public: - /// \brief Generate a fully populated JSON object of a supported type - nlohmann::json generate() - { - - } - -private: + /// \brief Enum values for each VDA5050 JSON object enum class JsonTypes { Connection, Order, - + InstantActions + }; + + /// \brief Generate a fully populated JSON object of a supported type + nlohmann::json generate(const JsonTypes type) + { + nlohmann::json j; + RandomDataGenerator generator; + + j["headerId"] = generator.generate_uint(); + j["timestamp"] = generator.generate_random_string(); + j["version"] = generator.generate_random_string(); + j["manufacturer"] = generator.generate_random_string(); + j["serialNumber"] = generator.generate_random_string(); + + switch (type) + { + case JsonTypes::Connection: + /// create Connection JSON object + j["connectionState"] = generator.generate_connection_state(); + break; + + case JsonTypes::Order: + /// TODO: (@shawnkchan) complete this once random generator for Order message is completed + /// create Order JSON Object + break; + + case JsonTypes::InstantActions: + /// TODO: (@shawnkchan) complete this once random generator for InstantActions message is completed + /// create InstantActions JSON Object + break; + } + return j; } -} +}; #endif \ No newline at end of file diff --git a/test/test_json_validators.cpp b/test/test_json_validators.cpp index 8487398..2292877 100644 --- a/test/test_json_validators.cpp +++ b/test/test_json_validators.cpp @@ -2,19 +2,28 @@ #include #include "vda5050_msgs/json_utils/validators.hpp" +#include "vda5050_msgs/json_utils/schemas.hpp" +#include "generator/jsonGenerator.hpp" +/// \brief Fixture class to create VDA5050 JSON objects for tests class JsonValidatorTest : public ::testing::Test { protected: JsonValidatorTest() - { - + { + connection_object_json = json_generator.generate(RandomJSONgenerator::JsonTypes::Connection); + connection_schema_json = nlohmann::json::parse(connection_schema); } - nlohmann::json -} + RandomJSONgenerator json_generator; + nlohmann::json connection_object_json; + nlohmann::json connection_schema_json; + /// TODO: declare other VDA5050 JSON objects here +}; -TEST(JsonValidatorTest, basicValidationTest) +/// \brief tests that a valid VDA5050 JSON object passes against the schema validator +TEST_F(JsonValidatorTest, BasicValidationTest) { - + int connection_result = validate_schema(connection_schema_json, connection_object_json); + EXPECT_EQ(connection_result, 1); } \ No newline at end of file From 827c2ca3a560d4e3716f192ffa434b119ad507af Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Mon, 22 Sep 2025 17:55:06 +0800 Subject: [PATCH 06/10] nit: run linter Signed-off-by: Shawn Chan --- test/test_json_validators.cpp | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/test/test_json_validators.cpp b/test/test_json_validators.cpp index 2292877..0760cf8 100644 --- a/test/test_json_validators.cpp +++ b/test/test_json_validators.cpp @@ -1,29 +1,31 @@ #include #include -#include "vda5050_msgs/json_utils/validators.hpp" -#include "vda5050_msgs/json_utils/schemas.hpp" #include "generator/jsonGenerator.hpp" +#include "vda5050_msgs/json_utils/schemas.hpp" +#include "vda5050_msgs/json_utils/validators.hpp" -/// \brief Fixture class to create VDA5050 JSON objects for tests +/// \brief Fixture class to create VDA5050 JSON objects for tests class JsonValidatorTest : public ::testing::Test { protected: - JsonValidatorTest() - { - connection_object_json = json_generator.generate(RandomJSONgenerator::JsonTypes::Connection); - connection_schema_json = nlohmann::json::parse(connection_schema); - } + JsonValidatorTest() + { + connection_object_json = + json_generator.generate(RandomJSONgenerator::JsonTypes::Connection); + connection_schema_json = nlohmann::json::parse(connection_schema); + } - RandomJSONgenerator json_generator; - nlohmann::json connection_object_json; - nlohmann::json connection_schema_json; - /// TODO: declare other VDA5050 JSON objects here + RandomJSONgenerator json_generator; + nlohmann::json connection_object_json; + nlohmann::json connection_schema_json; + /// TODO: declare other VDA5050 JSON objects here }; /// \brief tests that a valid VDA5050 JSON object passes against the schema validator TEST_F(JsonValidatorTest, BasicValidationTest) { - int connection_result = validate_schema(connection_schema_json, connection_object_json); - EXPECT_EQ(connection_result, 1); + int connection_result = + validate_schema(connection_schema_json, connection_object_json); + EXPECT_EQ(connection_result, 1); } \ No newline at end of file From b59b150c7f1ddf72fd6f883110affc03745b6201 Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Thu, 25 Sep 2025 20:35:12 +0800 Subject: [PATCH 07/10] nit: add docstrings Signed-off-by: Shawn Chan --- CMakeLists.txt | 1 + include/vda5050_msgs/json_utils/schemas.hpp | 2 +- .../vda5050_msgs/json_utils/validators.hpp | 84 +++++++++++++++++-- test/generator/generator.hpp | 23 +++++ test/generator/jsonGenerator.hpp | 25 +++++- test/test_json_validators.cpp | 23 +++-- 6 files changed, 137 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a7784f7..2530319 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ if(BUILD_TESTING) ${cpp_typesupport_target} ) + # TODO: (@shawnkchan) Remove this once we move validators to another repo ament_add_gtest(${PROJECT_NAME}_validator_test test/test_json_validators.cpp diff --git a/include/vda5050_msgs/json_utils/schemas.hpp b/include/vda5050_msgs/json_utils/schemas.hpp index 4b55101..493c7ed 100644 --- a/include/vda5050_msgs/json_utils/schemas.hpp +++ b/include/vda5050_msgs/json_utils/schemas.hpp @@ -15,7 +15,7 @@ inline constexpr auto connection_schema = R"( "headerId", "timestamp", "version", - "manufacttimurer", + "manufacturer", "serialNumber", "connectionState" ], diff --git a/include/vda5050_msgs/json_utils/validators.hpp b/include/vda5050_msgs/json_utils/validators.hpp index d6f5ca6..2a99dbb 100644 --- a/include/vda5050_msgs/json_utils/validators.hpp +++ b/include/vda5050_msgs/json_utils/validators.hpp @@ -1,16 +1,81 @@ +#ifndef VDA5050_MSGS__JSON_UTILS__VALIDATORS_HPP_ +#define VDA5050_MSGS__JSON_UTILS__VALIDATORS_HPP_ /// TODO: change header guard name when we separate this from the VDA5050 Messages package + #include #include #include +#include + #include "vda5050_msgs/json_utils/schemas.hpp" -/// @brief Checks that a JSON object is following the specified schema +constexpr const char* ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S"; + +/// \brief Utility function to check that a given string is in ISO8601 format +/// \param value The string to be checked +/// \return True if the given string follows the format +bool is_in_ISO8601_format(const std::string& value) +{ + std::tm t = {}; + char sep; + int millisec = 0; + + std::istringstream ss(value); + + ss >> std::get_time(&t, ISO8601_FORMAT); + if (ss.fail()) + { + return false; + } + + ss >> sep; + if (ss.fail() || sep != '.') + { + return false; + } + + ss >> millisec; + if (ss.fail()) + { + return false; + } + if (!ss.eof()) + { + ss.ignore(std::numeric_limits::max(), 'Z'); + } + else + { + return false; + } + return true; +} + +/// TODO (@shawnkchan) This can probably be generalised for any other custom formats that we may need. Keeping it specific for now. +/// \brief Format checker for a date-time field +/// \param format Name of the field whose format is to be checked +/// \param value Value associated with the given field +static void date_time_format_checker(const std::string& format, const std::string& value) +{ + if (format == "date-time") + { + if (!is_in_ISO8601_format(value)) + { + throw std::invalid_argument("Value is not in valid ISO8601 format"); + } + } + else + { + throw std::logic_error("Don't know how to validate " + format); + } +} + +/// \brief Checks that a JSON object is following the a given schema /// -/// @param schema The schema to validate against, as an nlohmann::json object -/// @param j Reference to the nlohmann::json object to be validated -/// @return EXIT_FAILURE or EXIT_SUCCESS -int validate_schema(nlohmann::json schema, nlohmann::json& j) +/// \param schema The schema to validate against, as an nlohmann::json object +/// \param j Reference to the nlohmann::json object to be validated +/// \return true if schema is valid +bool is_valid_schema(nlohmann::json schema, nlohmann::json& j) { - nlohmann::json_schema::json_validator validator; + nlohmann::json_schema::json_validator validator(nullptr, date_time_format_checker); try { @@ -19,7 +84,7 @@ int validate_schema(nlohmann::json schema, nlohmann::json& j) catch (const std::exception &e) { std::cerr << "Validation of schema failed: " << e.what() << "\n"; - return EXIT_FAILURE; + return false; } try @@ -29,6 +94,9 @@ int validate_schema(nlohmann::json schema, nlohmann::json& j) catch(const std::exception& e) { std::cerr << e.what() << '\n'; + return false; } - return EXIT_SUCCESS; + return true; } + +#endif \ No newline at end of file diff --git a/test/generator/generator.hpp b/test/generator/generator.hpp index 7008708..2499d55 100644 --- a/test/generator/generator.hpp +++ b/test/generator/generator.hpp @@ -74,6 +74,29 @@ class RandomDataGenerator return bool_dist_(rng_); } + /// \brief Generate a random ISO8601 formatted timestamp + std::string generate_random_ISO8601_timestamp() + { + constexpr const char* ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S"; + + int64_t timestamp = generate_milliseconds(); + std::chrono::system_clock::time_point tp{std::chrono::milliseconds(timestamp)}; + std::time_t time_sec = std::chrono::system_clock::to_time_t(tp); + auto duration = tp.time_since_epoch(); + auto millisec = std::chrono::duration_cast(duration).count() % 1000; + + std::ostringstream oss; + oss << std::put_time(std::gmtime(&time_sec), ISO8601_FORMAT); + oss << "." << std::setw(3) << std::setfill('0') << millisec << "Z"; + + if (oss.fail()) + { + throw std::runtime_error("Failed to generate a random ISO8601 timestamp"); + } + + return oss.str(); + } + /// \brief Generate a random alphanumerical string with length upto 50 std::string generate_random_string() { diff --git a/test/generator/jsonGenerator.hpp b/test/generator/jsonGenerator.hpp index 743c844..7d2e365 100644 --- a/test/generator/jsonGenerator.hpp +++ b/test/generator/jsonGenerator.hpp @@ -5,7 +5,7 @@ #include "generator.hpp" -/// \brief Utility class to generate random VDA 5050 JSON objects +/// \brief Utility class to generate random VDA5050 JSON objects class RandomJSONgenerator { public: @@ -14,7 +14,10 @@ class RandomJSONgenerator enum class JsonTypes { Connection, Order, - InstantActions + InstantActions, + State, + Visualization, + Factsheet }; /// \brief Generate a fully populated JSON object of a supported type @@ -24,7 +27,7 @@ class RandomJSONgenerator RandomDataGenerator generator; j["headerId"] = generator.generate_uint(); - j["timestamp"] = generator.generate_random_string(); + j["timestamp"] = generator.generate_random_ISO8601_timestamp(); j["version"] = generator.generate_random_string(); j["manufacturer"] = generator.generate_random_string(); j["serialNumber"] = generator.generate_random_string(); @@ -45,6 +48,22 @@ class RandomJSONgenerator /// TODO: (@shawnkchan) complete this once random generator for InstantActions message is completed /// create InstantActions JSON Object break; + + case JsonTypes::State: + /// TODO: (@shawnkchan) complete this once random generator for State message is completed + /// create State object + break; + + case JsonTypes::Visualization: + /// TODO: (@shawnkchan) complete this once random generator for Visualization message is completed + /// create Visualization object + break; + + case JsonTypes::Factsheet: + /// TODO: (@shawnkchan) complete this once random generator for Factsheet message is completed + /// Factsheet + break; + } return j; } diff --git a/test/test_json_validators.cpp b/test/test_json_validators.cpp index 0760cf8..9456715 100644 --- a/test/test_json_validators.cpp +++ b/test/test_json_validators.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "generator/jsonGenerator.hpp" #include "vda5050_msgs/json_utils/schemas.hpp" @@ -11,21 +12,25 @@ class JsonValidatorTest : public ::testing::Test protected: JsonValidatorTest() { - connection_object_json = + connection_object = json_generator.generate(RandomJSONgenerator::JsonTypes::Connection); - connection_schema_json = nlohmann::json::parse(connection_schema); + connection_schema = nlohmann::json::parse(connection_schema); + /// TODO: Instantiate other VDA5050 JSON objects } RandomJSONgenerator json_generator; - nlohmann::json connection_object_json; - nlohmann::json connection_schema_json; - /// TODO: declare other VDA5050 JSON objects here + nlohmann::json connection_object; + nlohmann::json connection_schema; + /// TODO: Declare other VDA5050 JSON objects }; -/// \brief tests that a valid VDA5050 JSON object passes against the schema validator +/// \brief Tests the is_valid_schema function to check that it passes when validating a correctly formatted JSON object TEST_F(JsonValidatorTest, BasicValidationTest) { + /// TODO (@shawnkchan) Change this to a typed test so that we can iterate over the different VDA5050 object types + std::cout << "running test" << "\n"; int connection_result = - validate_schema(connection_schema_json, connection_object_json); - EXPECT_EQ(connection_result, 1); -} \ No newline at end of file + is_valid_schema(connection_schema, connection_object); + EXPECT_EQ(connection_result, true); + std::cout << "test ended" << "\n"; +} From fada0832c9c5444a2a2be9c1eddd90bbf50306a4 Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Thu, 25 Sep 2025 20:36:13 +0800 Subject: [PATCH 08/10] nit: run linter Signed-off-by: Shawn Chan --- .../vda5050_msgs/json_utils/validators.hpp | 122 +++++++++--------- test/generator/jsonGenerator.hpp | 100 +++++++------- test/test_json_validators.cpp | 5 +- 3 files changed, 113 insertions(+), 114 deletions(-) diff --git a/include/vda5050_msgs/json_utils/validators.hpp b/include/vda5050_msgs/json_utils/validators.hpp index 2a99dbb..22fa23b 100644 --- a/include/vda5050_msgs/json_utils/validators.hpp +++ b/include/vda5050_msgs/json_utils/validators.hpp @@ -1,9 +1,9 @@ #ifndef VDA5050_MSGS__JSON_UTILS__VALIDATORS_HPP_ -#define VDA5050_MSGS__JSON_UTILS__VALIDATORS_HPP_ /// TODO: change header guard name when we separate this from the VDA5050 Messages package +#define VDA5050_MSGS__JSON_UTILS__VALIDATORS_HPP_ /// TODO: change header guard name when we separate this from the VDA5050 Messages package +#include #include #include -#include #include #include "vda5050_msgs/json_utils/schemas.hpp" @@ -15,57 +15,58 @@ constexpr const char* ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S"; /// \return True if the given string follows the format bool is_in_ISO8601_format(const std::string& value) { - std::tm t = {}; - char sep; - int millisec = 0; + std::tm t = {}; + char sep; + int millisec = 0; - std::istringstream ss(value); + std::istringstream ss(value); - ss >> std::get_time(&t, ISO8601_FORMAT); - if (ss.fail()) - { - return false; - } + ss >> std::get_time(&t, ISO8601_FORMAT); + if (ss.fail()) + { + return false; + } - ss >> sep; - if (ss.fail() || sep != '.') - { - return false; - } + ss >> sep; + if (ss.fail() || sep != '.') + { + return false; + } - ss >> millisec; - if (ss.fail()) - { - return false; - } - if (!ss.eof()) - { - ss.ignore(std::numeric_limits::max(), 'Z'); - } - else - { - return false; - } - return true; + ss >> millisec; + if (ss.fail()) + { + return false; + } + if (!ss.eof()) + { + ss.ignore(std::numeric_limits::max(), 'Z'); + } + else + { + return false; + } + return true; } /// TODO (@shawnkchan) This can probably be generalised for any other custom formats that we may need. Keeping it specific for now. /// \brief Format checker for a date-time field /// \param format Name of the field whose format is to be checked /// \param value Value associated with the given field -static void date_time_format_checker(const std::string& format, const std::string& value) +static void date_time_format_checker( + const std::string& format, const std::string& value) { - if (format == "date-time") - { - if (!is_in_ISO8601_format(value)) - { - throw std::invalid_argument("Value is not in valid ISO8601 format"); - } - } - else + if (format == "date-time") + { + if (!is_in_ISO8601_format(value)) { - throw std::logic_error("Don't know how to validate " + format); + throw std::invalid_argument("Value is not in valid ISO8601 format"); } + } + else + { + throw std::logic_error("Don't know how to validate " + format); + } } /// \brief Checks that a JSON object is following the a given schema @@ -75,28 +76,29 @@ static void date_time_format_checker(const std::string& format, const std::strin /// \return true if schema is valid bool is_valid_schema(nlohmann::json schema, nlohmann::json& j) { - nlohmann::json_schema::json_validator validator(nullptr, date_time_format_checker); + nlohmann::json_schema::json_validator validator( + nullptr, date_time_format_checker); - try - { - validator.set_root_schema(schema); - } - catch (const std::exception &e) - { - std::cerr << "Validation of schema failed: " << e.what() << "\n"; - return false; - } + try + { + validator.set_root_schema(schema); + } + catch (const std::exception& e) + { + std::cerr << "Validation of schema failed: " << e.what() << "\n"; + return false; + } - try - { - validator.validate(j); - } - catch(const std::exception& e) - { - std::cerr << e.what() << '\n'; - return false; - } - return true; + try + { + validator.validate(j); + } + catch (const std::exception& e) + { + std::cerr << e.what() << '\n'; + return false; + } + return true; } #endif \ No newline at end of file diff --git a/test/generator/jsonGenerator.hpp b/test/generator/jsonGenerator.hpp index 7d2e365..25f6d58 100644 --- a/test/generator/jsonGenerator.hpp +++ b/test/generator/jsonGenerator.hpp @@ -5,69 +5,67 @@ #include "generator.hpp" -/// \brief Utility class to generate random VDA5050 JSON objects +/// \brief Utility class to generate random VDA5050 JSON objects class RandomJSONgenerator { public: + /// \brief Enum values for each VDA5050 JSON object + enum class JsonTypes + { + Connection, + Order, + InstantActions, + State, + Visualization, + Factsheet + }; - /// \brief Enum values for each VDA5050 JSON object - enum class JsonTypes { - Connection, - Order, - InstantActions, - State, - Visualization, - Factsheet - }; + /// \brief Generate a fully populated JSON object of a supported type + nlohmann::json generate(const JsonTypes type) + { + nlohmann::json j; + RandomDataGenerator generator; - /// \brief Generate a fully populated JSON object of a supported type - nlohmann::json generate(const JsonTypes type) - { - nlohmann::json j; - RandomDataGenerator generator; - - j["headerId"] = generator.generate_uint(); - j["timestamp"] = generator.generate_random_ISO8601_timestamp(); - j["version"] = generator.generate_random_string(); - j["manufacturer"] = generator.generate_random_string(); - j["serialNumber"] = generator.generate_random_string(); - - switch (type) - { - case JsonTypes::Connection: - /// create Connection JSON object - j["connectionState"] = generator.generate_connection_state(); - break; + j["headerId"] = generator.generate_uint(); + j["timestamp"] = generator.generate_random_ISO8601_timestamp(); + j["version"] = generator.generate_random_string(); + j["manufacturer"] = generator.generate_random_string(); + j["serialNumber"] = generator.generate_random_string(); - case JsonTypes::Order: - /// TODO: (@shawnkchan) complete this once random generator for Order message is completed - /// create Order JSON Object - break; + switch (type) + { + case JsonTypes::Connection: + /// create Connection JSON object + j["connectionState"] = generator.generate_connection_state(); + break; - case JsonTypes::InstantActions: - /// TODO: (@shawnkchan) complete this once random generator for InstantActions message is completed - /// create InstantActions JSON Object - break; + case JsonTypes::Order: + /// TODO: (@shawnkchan) complete this once random generator for Order message is completed + /// create Order JSON Object + break; - case JsonTypes::State: - /// TODO: (@shawnkchan) complete this once random generator for State message is completed - /// create State object - break; + case JsonTypes::InstantActions: + /// TODO: (@shawnkchan) complete this once random generator for InstantActions message is completed + /// create InstantActions JSON Object + break; - case JsonTypes::Visualization: - /// TODO: (@shawnkchan) complete this once random generator for Visualization message is completed - /// create Visualization object - break; + case JsonTypes::State: + /// TODO: (@shawnkchan) complete this once random generator for State message is completed + /// create State object + break; - case JsonTypes::Factsheet: - /// TODO: (@shawnkchan) complete this once random generator for Factsheet message is completed - /// Factsheet - break; + case JsonTypes::Visualization: + /// TODO: (@shawnkchan) complete this once random generator for Visualization message is completed + /// create Visualization object + break; - } - return j; + case JsonTypes::Factsheet: + /// TODO: (@shawnkchan) complete this once random generator for Factsheet message is completed + /// Factsheet + break; } - + return j; + } }; #endif \ No newline at end of file diff --git a/test/test_json_validators.cpp b/test/test_json_validators.cpp index 9456715..17eb014 100644 --- a/test/test_json_validators.cpp +++ b/test/test_json_validators.cpp @@ -1,6 +1,6 @@ #include -#include #include +#include #include "generator/jsonGenerator.hpp" #include "vda5050_msgs/json_utils/schemas.hpp" @@ -29,8 +29,7 @@ TEST_F(JsonValidatorTest, BasicValidationTest) { /// TODO (@shawnkchan) Change this to a typed test so that we can iterate over the different VDA5050 object types std::cout << "running test" << "\n"; - int connection_result = - is_valid_schema(connection_schema, connection_object); + int connection_result = is_valid_schema(connection_schema, connection_object); EXPECT_EQ(connection_result, true); std::cout << "test ended" << "\n"; } From 33137159a784e88f9d1925e756a7d371c22100bd Mon Sep 17 00:00:00 2001 From: Shawn Chan Date: Mon, 29 Sep 2025 11:57:24 +0800 Subject: [PATCH 09/10] nit: reformat docstrings Signed-off-by: Shawn Chan --- include/vda5050_msgs/json_utils/validators.hpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/include/vda5050_msgs/json_utils/validators.hpp b/include/vda5050_msgs/json_utils/validators.hpp index 22fa23b..4acbf97 100644 --- a/include/vda5050_msgs/json_utils/validators.hpp +++ b/include/vda5050_msgs/json_utils/validators.hpp @@ -11,7 +11,9 @@ constexpr const char* ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S"; /// \brief Utility function to check that a given string is in ISO8601 format +/// /// \param value The string to be checked +/// /// \return True if the given string follows the format bool is_in_ISO8601_format(const std::string& value) { @@ -51,8 +53,12 @@ bool is_in_ISO8601_format(const std::string& value) /// TODO (@shawnkchan) This can probably be generalised for any other custom formats that we may need. Keeping it specific for now. /// \brief Format checker for a date-time field +/// /// \param format Name of the field whose format is to be checked /// \param value Value associated with the given field +/// +/// \throw std::invalid_argument if the value in the date-time field does not follow ISO8601 format. +/// \throw std::logic_error if the format field is not "date-time". static void date_time_format_checker( const std::string& format, const std::string& value) { @@ -73,7 +79,8 @@ static void date_time_format_checker( /// /// \param schema The schema to validate against, as an nlohmann::json object /// \param j Reference to the nlohmann::json object to be validated -/// \return true if schema is valid +/// +/// \return true if schema is valid, false otherwise bool is_valid_schema(nlohmann::json schema, nlohmann::json& j) { nlohmann::json_schema::json_validator validator( @@ -101,4 +108,4 @@ bool is_valid_schema(nlohmann::json schema, nlohmann::json& j) return true; } -#endif \ No newline at end of file +#endif From 0a5f375baca944a4dacc277317b9d7fd8c7197a6 Mon Sep 17 00:00:00 2001 From: Chen Bainian Date: Sat, 8 Nov 2025 09:11:59 +0800 Subject: [PATCH 10/10] ci(trigger): trigger CI manually Signed-off-by: Chen Bainian