Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,6 +32,7 @@ install(
ament_export_dependencies(
rosidl_default_runtime
nlohmann_json
nlohmann_json_schema_validator
)

if(BUILD_TESTING)
Expand Down Expand Up @@ -74,6 +76,22 @@ if(BUILD_TESTING)
nlohmann_json::nlohmann_json
${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

)
target_include_directories(${PROJECT_NAME}_validator_test
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_link_libraries(${PROJECT_NAME}_validator_test
nlohmann_json::nlohmann_json
nlohmann_json_schema_validator
${cpp_typesupport_target}
)
endif()

ament_package()
Expand Down
63 changes: 63 additions & 0 deletions include/vda5050_msgs/json_utils/schemas.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#ifndef VDA5050_MSGS__JSON_UTILS__SCHEMAS_HPP_
#define VDA5050_MSGS__JSON_UTILS__SCHEMAS_HPP_

#include <string>

/// \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",
"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."
}
}
}
)";

#endif
111 changes: 111 additions & 0 deletions include/vda5050_msgs/json_utils/validators.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#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 <iostream>
#include <nlohmann/json-schema.hpp>
#include <nlohmann/json.hpp>
#include <string>

#include "vda5050_msgs/json_utils/schemas.hpp"

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<std::streamsize>::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
///
/// \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)
{
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 true if schema is valid, false otherwise
bool is_valid_schema(nlohmann::json schema, nlohmann::json& j)
{
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.validate(j);
}
catch (const std::exception& e)
{
std::cerr << e.what() << '\n';
return false;
}
return true;
}

#endif
1 change: 1 addition & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<buildtool_depend>rosidl_default_generators</buildtool_depend>

<build_export_depend>nlohmann-json-dev</build_export_depend>
<build_export_depend>nlohmann-json-schema-validator-dev</build_export_depend>

<exec_depend>rosidl_default_runtime</exec_depend>

Expand Down
84 changes: 84 additions & 0 deletions test/generator/generator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,41 @@ 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 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<std::chrono::milliseconds>(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()
{
Expand Down Expand Up @@ -97,6 +132,42 @@ 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<uint8_t> index_dist(0, size - 1);
return index_dist(rng_);
}

/// \brief Generate a random vector of type float64
std::vector<double> generate_random_float_vector(const uint8_t size)
{
std::vector<double> 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 <typename T>
std::vector<T> generate_random_vector(const uint8_t size)
{
std::vector<T> vec(size);
for (auto it = vec.begin(); it != vec.end(); ++it)
{
*it = generate<T>();
}
return vec;
}

/// \brief
uint8_t generate_random_size()
{
return size_dist_(rng_);
}

/// \brief Generate a fully populated message of a supported type
template <typename T>
T generate()
Expand Down Expand Up @@ -133,6 +204,13 @@ class RandomDataGenerator
/// \brief Distribution for unsigned 32-bit integers
std::uniform_int_distribution<uint32_t> uint_dist_;

/// \brief Distribution for 64-bit floating-point numbers
std::uniform_real_distribution<double> float_dist_;

/// \brief Distribution for a boolean value
/// TODO (@shawnkchan): KIV should we be bounding this between 0 and 1?
std::uniform_int_distribution<int> bool_dist_{0, 1};

/// \brief Distribution for random string lengths
std::uniform_int_distribution<int> string_length_dist_;

Expand All @@ -141,6 +219,12 @@ class RandomDataGenerator

/// \brief Distribution for VDA 5050 connectionState
std::uniform_int_distribution<uint8_t> connection_state_dist_;

/// \brief Distribution for random vector size
std::uniform_int_distribution<uint8_t> 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_
Loading