39#include <unordered_map>
49void add_cors_headers(crow::response& res,
const rest_server_context& ctx) {
50 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
51 res.add_header(
"Access-Control-Allow-Origin",
52 ctx.config->cors_allowed_origins);
59std::string format_timestamp(std::chrono::system_clock::time_point tp) {
60 if (tp == std::chrono::system_clock::time_point{}) {
63 auto time_t_val = std::chrono::system_clock::to_time_t(tp);
66 gmtime_s(&tm_val, &time_t_val);
68 gmtime_r(&time_t_val, &tm_val);
71 std::strftime(buf,
sizeof(buf),
"%Y-%m-%dT%H:%M:%SZ", &tm_val);
84 default:
return "normal";
101std::string condition_to_json(
const client::routing_condition& condition) {
102 std::ostringstream oss;
104 << R"(","pattern":")" << json_escape(condition.pattern)
105 << R"(","case_sensitive":)" << (condition.case_sensitive ? "true" :
"false")
106 << R
"(,"negate":)" << (condition.negate ? "true" :
"false")
114std::string action_to_json(
const client::routing_action& action) {
115 std::ostringstream oss;
116 oss << R
"({"destination_node_id":")" << json_escape(action.destination_node_id)
117 << R"(","priority":")" << priority_to_string(action.priority)
118 << R"(","delay_minutes":)" << action.delay.count()
119 << R"(,"delete_after_send":)" << (action.delete_after_send ? "true" :
"false")
120 << R
"(,"notify_on_failure":)" << (action.notify_on_failure ? "true" :
"false")
128std::string rule_to_json(
const client::routing_rule& rule) {
129 std::ostringstream oss;
130 oss << R
"({"rule_id":")" << json_escape(rule.rule_id)
132 << R"(","description":")" << json_escape(rule.description)
133 << R"(","enabled":)" << (rule.enabled ? "true" :
"false")
134 << R
"(,"priority":)" << rule.priority;
137 oss << R
"(,"conditions":[)";
138 for (
size_t i = 0; i < rule.conditions.size(); ++i) {
139 if (i > 0) oss <<
',';
140 oss << condition_to_json(rule.conditions[i]);
145 oss << R
"(,"actions":[)";
146 for (
size_t i = 0; i < rule.actions.size(); ++i) {
147 if (i > 0) oss <<
',';
148 oss << action_to_json(rule.actions[i]);
153 if (rule.schedule_cron.has_value()) {
154 oss << R
"(,"schedule_cron":")" << json_escape(*rule.schedule_cron) << '"';
158 oss << R
"(,"triggered_count":)" << rule.triggered_count
159 << R"(,"success_count":)" << rule.success_count
160 << R"(,"failure_count":)" << rule.failure_count;
162 auto last_triggered = format_timestamp(rule.last_triggered);
163 if (!last_triggered.empty()) {
164 oss << R
"(,"last_triggered":")" << last_triggered << '"';
167 auto created_at = format_timestamp(rule.created_at);
168 if (!created_at.empty()) {
169 oss << R
"(,"created_at":")" << created_at << '"';
172 auto updated_at = format_timestamp(rule.updated_at);
173 if (!updated_at.empty()) {
174 oss << R
"(,"updated_at":")" << updated_at << '"';
184std::string rules_to_json(
const std::vector<client::routing_rule>& rules,
185 size_t total_count) {
186 std::ostringstream oss;
187 oss << R
"({"rules":[)";
188 for (
size_t i = 0; i < rules.size(); ++i) {
189 if (i > 0) oss <<
',';
190 oss << rule_to_json(rules[i]);
192 oss << R
"(],"total":)" << total_count << '}';
199std::string get_json_string(
const std::string& body,
const std::string& key) {
200 std::string
search =
"\"" + key +
"\":\"";
201 auto pos = body.find(search);
202 if (pos == std::string::npos) {
206 auto end_pos = body.find(
'"', pos);
207 if (end_pos == std::string::npos) {
210 return body.substr(pos, end_pos - pos);
216std::optional<int64_t> get_json_int(
const std::string& body,
const std::string& key) {
217 std::string
search =
"\"" + key +
"\":";
218 auto pos = body.find(search);
219 if (pos == std::string::npos) {
223 while (pos < body.length() && (body[pos] ==
' ' || body[pos] ==
'\t')) {
226 if (pos >= body.length()) {
231 auto val = std::stoll(body.substr(pos), &idx);
241bool get_json_bool(
const std::string& body,
const std::string& key,
bool default_val) {
242 std::string
search =
"\"" + key +
"\":";
243 auto pos = body.find(search);
244 if (pos == std::string::npos) {
248 while (pos < body.length() && (body[pos] ==
' ' || body[pos] ==
'\t')) {
251 if (pos >= body.length()) {
254 if (body.substr(pos, 4) ==
"true") {
257 if (body.substr(pos, 5) ==
"false") {
266std::vector<client::routing_condition> parse_conditions(
const std::string& body) {
267 std::vector<client::routing_condition> conditions;
270 auto start = body.find(
"\"conditions\":[");
271 if (start == std::string::npos) {
276 auto end = body.find(
']', start);
277 if (end == std::string::npos) {
281 std::string arr = body.substr(start, end - start);
284 size_t obj_start = 0;
285 while ((obj_start = arr.find(
'{', obj_start)) != std::string::npos) {
286 auto obj_end = arr.find(
'}', obj_start);
287 if (obj_end == std::string::npos)
break;
289 std::string obj = arr.substr(obj_start, obj_end - obj_start + 1);
291 client::routing_condition cond;
292 auto field_str = get_json_string(obj,
"field");
294 cond.pattern = get_json_string(obj,
"pattern");
295 cond.case_sensitive = get_json_bool(obj,
"case_sensitive",
false);
296 cond.negate = get_json_bool(obj,
"negate",
false);
298 if (!cond.pattern.empty()) {
299 conditions.push_back(std::move(cond));
302 obj_start = obj_end + 1;
311std::vector<client::routing_action> parse_actions(
const std::string& body) {
312 std::vector<client::routing_action> actions;
315 auto start = body.find(
"\"actions\":[");
316 if (start == std::string::npos) {
321 auto end = body.find(
']', start);
322 if (end == std::string::npos) {
326 std::string arr = body.substr(start, end - start);
329 size_t obj_start = 0;
330 while ((obj_start = arr.find(
'{', obj_start)) != std::string::npos) {
331 auto obj_end = arr.find(
'}', obj_start);
332 if (obj_end == std::string::npos)
break;
334 std::string obj = arr.substr(obj_start, obj_end - obj_start + 1);
336 client::routing_action action;
337 action.destination_node_id = get_json_string(obj,
"destination_node_id");
338 auto priority_str = get_json_string(obj,
"priority");
339 action.priority = priority_from_string(priority_str);
341 auto delay = get_json_int(obj,
"delay_minutes");
343 action.delay = std::chrono::minutes(*delay);
346 action.delete_after_send = get_json_bool(obj,
"delete_after_send",
false);
347 action.notify_on_failure = get_json_bool(obj,
"notify_on_failure",
true);
349 if (!action.destination_node_id.empty()) {
350 actions.push_back(std::move(action));
353 obj_start = obj_end + 1;
362std::optional<client::routing_rule> parse_rule_from_json(
const std::string& body,
363 std::string& error_message) {
364 client::routing_rule rule;
367 rule.name = get_json_string(body,
"name");
368 if (rule.name.empty()) {
369 error_message =
"name is required";
374 rule.description = get_json_string(body,
"description");
375 rule.enabled = get_json_bool(body,
"enabled",
true);
377 auto priority = get_json_int(body,
"priority");
379 rule.priority =
static_cast<int>(*priority);
383 rule.conditions = parse_conditions(body);
384 if (rule.conditions.empty()) {
385 error_message =
"at least one condition is required";
390 rule.actions = parse_actions(body);
391 if (rule.actions.empty()) {
392 error_message =
"at least one action is required";
397 auto schedule = get_json_string(body,
"schedule_cron");
408std::vector<std::string> parse_rule_ids(
const std::string& body) {
409 std::vector<std::string> ids;
412 auto start = body.find(
"\"rule_ids\":[");
413 if (start == std::string::npos) {
418 auto end = body.find(
']', start);
419 if (end == std::string::npos) {
423 std::string arr = body.substr(start, end - start);
426 size_t str_start = 0;
427 while ((str_start = arr.find(
'"', str_start)) != std::string::npos) {
429 auto str_end = arr.find(
'"', str_start);
430 if (str_end == std::string::npos)
break;
432 std::string
id = arr.substr(str_start, str_end - str_start);
434 ids.push_back(std::move(
id));
437 str_start = str_end + 1;
446std::pair<size_t, size_t> parse_pagination(
const crow::request& req) {
450 auto limit_param = req.url_params.get(
"limit");
453 limit = std::stoul(limit_param);
462 auto offset_param = req.url_params.get(
"offset");
465 offset = std::stoul(offset_param);
471 return {limit, offset};
480core::dicom_dataset parse_test_dataset(
const std::string& body) {
481 core::dicom_dataset dataset;
484 auto start = body.find(
"\"dataset\":{");
485 if (start == std::string::npos) {
486 start = body.find(
"\"dataset\": {");
488 if (start == std::string::npos) {
493 auto brace_start = body.find(
'{', start);
494 if (brace_start == std::string::npos) {
500 size_t brace_end = brace_start + 1;
501 while (brace_end < body.length() && brace_count > 0) {
502 if (body[brace_end] ==
'{') {
504 }
else if (body[brace_end] ==
'}') {
510 if (brace_count != 0) {
514 std::string dataset_json = body.substr(brace_start, brace_end - brace_start);
518 struct FieldMapping {
519 const char* json_key;
523 static const FieldMapping field_mappings[] = {
527 {
"department", core::dicom_tag{0x0008, 0x1040}},
531 {
"body_part", core::dicom_tag{0x0018, 0x0015}},
537 for (
const auto& mapping : field_mappings) {
538 std::string value = get_json_string(dataset_json, mapping.json_key);
539 if (!value.empty()) {
550std::string test_result_to_json(
552 const std::vector<std::pair<std::string, std::vector<client::routing_action>>>& matches,
553 const std::shared_ptr<client::routing_manager>& routing_manager) {
555 std::ostringstream oss;
556 oss << R
"({"matched":)" << (matched ? "true" :
"false");
558 oss << R
"(,"matched_rules":[)";
560 for (
const auto& [rule_id, actions] : matches) {
561 if (!first) oss <<
',';
565 std::string rule_name;
566 auto rule = routing_manager->get_rule(rule_id);
568 rule_name = rule->name;
573 << R"(","actions":[)";
575 bool first_action =
true;
576 for (
const auto& action : actions) {
577 if (!first_action) oss <<
',';
578 first_action =
false;
579 oss << action_to_json(action);
592std::string single_rule_test_to_json(
bool matched,
593 const std::vector<client::routing_action>& actions) {
594 std::ostringstream oss;
595 oss << R
"({"matched":)" << (matched ? "true" :
"false")
596 << R
"(,"actions":[)";
599 for (
const auto& action : actions) {
600 if (!first) oss <<
',';
602 oss << action_to_json(action);
613 std::shared_ptr<rest_server_context> ctx) {
615 CROW_ROUTE(app,
"/api/v1/routing/rules")
616 .methods(crow::HTTPMethod::GET)([ctx](
const crow::request& req) {
618 res.add_header(
"Content-Type",
"application/json");
619 add_cors_headers(res, *ctx);
621 if (!ctx->routing_manager) {
624 "Routing manager not configured");
629 auto [limit, offset] = parse_pagination(req);
632 auto enabled_param = req.url_params.get(
"enabled");
633 std::vector<client::routing_rule> rules;
636 std::string enabled_str(enabled_param);
637 if (enabled_str ==
"true") {
638 rules = ctx->routing_manager->list_enabled_rules();
640 rules = ctx->routing_manager->list_rules();
643 rules = ctx->routing_manager->list_rules();
646 size_t total_count = rules.size();
649 std::vector<client::routing_rule> paginated;
650 for (
size_t i = offset; i < rules.size() && paginated.size() < limit; ++i) {
651 paginated.push_back(rules[i]);
655 res.body = rules_to_json(paginated, total_count);
660 CROW_ROUTE(app,
"/api/v1/routing/rules")
661 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request& req) {
663 res.add_header(
"Content-Type",
"application/json");
664 add_cors_headers(res, *ctx);
666 if (!ctx->routing_manager) {
669 "Routing manager not configured");
673 std::string error_message;
674 auto rule_opt = parse_rule_from_json(req.body, error_message);
681 auto& rule = *rule_opt;
684 if (rule.rule_id.empty()) {
685 auto now = std::chrono::system_clock::now();
686 auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
687 now.time_since_epoch()).count();
688 rule.rule_id =
"rule_" + std::to_string(ms);
691 auto result = ctx->routing_manager->add_rule(rule);
692 if (!result.is_ok()) {
699 auto created_rule = ctx->routing_manager->get_rule(rule.rule_id);
702 res.body = rule_to_json(rule);
707 res.body = rule_to_json(*created_rule);
712 CROW_ROUTE(app,
"/api/v1/routing/rules/<string>")
713 .methods(crow::HTTPMethod::GET)(
714 [ctx](
const crow::request& ,
const std::string& rule_id) {
716 res.add_header(
"Content-Type",
"application/json");
717 add_cors_headers(res, *ctx);
719 if (!ctx->routing_manager) {
722 "Routing manager not configured");
726 auto rule = ctx->routing_manager->get_rule(rule_id);
734 res.body = rule_to_json(*rule);
739 CROW_ROUTE(app,
"/api/v1/routing/rules/<string>")
740 .methods(crow::HTTPMethod::PUT)(
741 [ctx](
const crow::request& req,
const std::string& rule_id) {
743 res.add_header(
"Content-Type",
"application/json");
744 add_cors_headers(res, *ctx);
746 if (!ctx->routing_manager) {
749 "Routing manager not configured");
754 auto existing = ctx->routing_manager->get_rule(rule_id);
761 std::string error_message;
762 auto rule_opt = parse_rule_from_json(req.body, error_message);
769 auto& rule = *rule_opt;
770 rule.rule_id = rule_id;
772 auto result = ctx->routing_manager->update_rule(rule);
773 if (!result.is_ok()) {
780 auto updated_rule = ctx->routing_manager->get_rule(rule_id);
783 res.body = rule_to_json(rule);
788 res.body = rule_to_json(*updated_rule);
793 CROW_ROUTE(app,
"/api/v1/routing/rules/<string>")
794 .methods(crow::HTTPMethod::DELETE)(
795 [ctx](
const crow::request& ,
const std::string& rule_id) {
797 add_cors_headers(res, *ctx);
799 if (!ctx->routing_manager) {
800 res.add_header(
"Content-Type",
"application/json");
803 "Routing manager not configured");
808 auto existing = ctx->routing_manager->get_rule(rule_id);
810 res.add_header(
"Content-Type",
"application/json");
816 auto result = ctx->routing_manager->remove_rule(rule_id);
817 if (!result.is_ok()) {
818 res.add_header(
"Content-Type",
"application/json");
829 CROW_ROUTE(app,
"/api/v1/routing/rules/reorder")
830 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request& req) {
832 res.add_header(
"Content-Type",
"application/json");
833 add_cors_headers(res, *ctx);
835 if (!ctx->routing_manager) {
838 "Routing manager not configured");
842 auto rule_ids = parse_rule_ids(req.body);
843 if (rule_ids.empty()) {
846 "rule_ids array is required");
850 auto result = ctx->routing_manager->reorder_rules(rule_ids);
851 if (!result.is_ok()) {
858 res.body = R
"({"status":"success"})";
863 CROW_ROUTE(app,
"/api/v1/routing/enable")
864 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request& ) {
866 res.add_header(
"Content-Type",
"application/json");
867 add_cors_headers(res, *ctx);
869 if (!ctx->routing_manager) {
872 "Routing manager not configured");
876 ctx->routing_manager->enable();
879 res.body = R
"({"enabled":true})";
884 CROW_ROUTE(app,
"/api/v1/routing/disable")
885 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request& ) {
887 res.add_header(
"Content-Type",
"application/json");
888 add_cors_headers(res, *ctx);
890 if (!ctx->routing_manager) {
893 "Routing manager not configured");
897 ctx->routing_manager->disable();
900 res.body = R
"({"enabled":false})";
905 CROW_ROUTE(app,
"/api/v1/routing/status")
906 .methods(crow::HTTPMethod::GET)([ctx](
const crow::request& ) {
908 res.add_header(
"Content-Type",
"application/json");
909 add_cors_headers(res, *ctx);
911 if (!ctx->routing_manager) {
914 "Routing manager not configured");
918 bool enabled = ctx->routing_manager->is_enabled();
919 auto all_rules = ctx->routing_manager->list_rules();
920 auto enabled_rules = ctx->routing_manager->list_enabled_rules();
921 auto stats = ctx->routing_manager->get_statistics();
923 std::ostringstream oss;
924 oss << R
"({"enabled":)" << (enabled ? "true" :
"false")
925 << R
"(,"rules_count":)" << all_rules.size()
926 << R"(,"enabled_rules_count":)" << enabled_rules.size()
927 << R"(,"statistics":{)"
928 << R"("total_evaluated":)" << stats.total_evaluated
929 << R"(,"total_matched":)" << stats.total_matched
930 << R"(,"total_forwarded":)" << stats.total_forwarded
931 << R"(,"total_failed":)" << stats.total_failed
935 res.body = oss.str();
940 CROW_ROUTE(app,
"/api/v1/routing/test")
941 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request& req) {
943 res.add_header(
"Content-Type",
"application/json");
944 add_cors_headers(res, *ctx);
946 if (!ctx->routing_manager) {
949 "Routing manager not configured");
954 auto dataset = parse_test_dataset(req.body);
955 if (dataset.empty()) {
958 "dataset object is required");
965 auto matches = ctx->routing_manager->evaluate_with_rule_ids(dataset);
967 bool matched = !matches.empty();
969 res.body = test_result_to_json(matched, matches, ctx->routing_manager);
974 CROW_ROUTE(app,
"/api/v1/routing/rules/<string>/test")
975 .methods(crow::HTTPMethod::POST)(
976 [ctx](
const crow::request& req,
const std::string& rule_id) {
978 res.add_header(
"Content-Type",
"application/json");
979 add_cors_headers(res, *ctx);
981 if (!ctx->routing_manager) {
984 "Routing manager not configured");
989 auto rule = ctx->routing_manager->get_rule(rule_id);
997 auto dataset = parse_test_dataset(req.body);
998 if (dataset.empty()) {
1001 "dataset object is required");
1007 bool all_match = !rule->conditions.empty();
1008 for (
const auto& condition : rule->conditions) {
1011 switch (condition.match_field) {
1047 auto match_wildcard = [](std::string_view pattern,
1048 std::string_view val,
1049 bool case_sensitive) ->
bool {
1050 auto to_lower = [](std::string_view s) {
1052 result.reserve(s.size());
1054 result +=
static_cast<char>(std::tolower(
1055 static_cast<unsigned char>(c)));
1060 std::string pat_str = case_sensitive ?
1061 std::string(pattern) : to_lower(pattern);
1062 std::string val_str = case_sensitive ?
1063 std::string(val) : to_lower(val);
1065 const char* p = pat_str.c_str();
1066 const char* v = val_str.c_str();
1067 const char* star_p =
nullptr;
1068 const char* star_v =
nullptr;
1074 }
else if (*p ==
'?' || *p == *v) {
1077 }
else if (star_p) {
1092 bool matched = match_wildcard(condition.pattern, value,
1093 condition.case_sensitive);
1094 if (condition.negate) {
1106 res.body = single_rule_test_to_json(
true, rule->actions);
1108 res.body = single_rule_test_to_json(
false, {});
DICOM Dataset - ordered collection of Data Elements.
Compile-time constants for commonly used DICOM tags.
@ series_description
(0008,103E) Series Description
@ department
(0008,1040) Institutional Department Name
@ body_part
(0018,0015) Body Part Examined
@ study_description
(0008,1030) Study Description
@ modality
(0008,0060) Modality - CT, MR, US, etc.
@ sop_class_uid
(0008,0016) SOP Class UID
@ institution
(0008,0080) Institution Name
@ station_ae
(0008,1010) Station Name or calling AE
@ patient_id_pattern
(0010,0020) Patient ID (pattern matching)
@ referring_physician
(0008,0090) Referring Physician's Name
job_priority
Priority level for job execution.
@ low
Background operations.
@ high
User-requested operations.
@ urgent
Critical operations.
@ normal
Standard priority.
constexpr const char * to_string(job_type type) noexcept
Convert job_type to string representation.
routing_field routing_field_from_string(std::string_view str) noexcept
Parse routing_field from string.
@ LO
Long String (64 chars max)
@ empty
Z - Replace with zero-length value.
constexpr std::string_view search
void register_routing_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
std::string json_escape(std::string_view s)
Escape a string for JSON.
std::variant< interval_schedule, cron_schedule, one_time_schedule > schedule
Combined schedule type.
Configuration for REST API server.
Common types and utilities for REST API.
Routing rule management REST API endpoints.
Routing manager for automatic DICOM image forwarding.
Routing types and structures for auto-forwarding DICOM images.
System API endpoints for REST server.