46void add_cors_headers(crow::response &res,
const rest_server_context &ctx) {
47 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
48 res.add_header(
"Access-Control-Allow-Origin",
49 ctx.config->cors_allowed_origins);
56std::string generate_uuid() {
57 static std::random_device rd;
58 static std::mt19937 gen(rd());
59 static std::uniform_int_distribution<> dis(0, 15);
60 static const char *hex =
"0123456789abcdef";
62 std::string uuid =
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
63 for (
char &c : uuid) {
66 }
else if (c ==
'y') {
67 c = hex[(dis(gen) & 0x3) | 0x8];
76std::string format_timestamp(
77 const std::chrono::system_clock::time_point &tp) {
78 auto time = std::chrono::system_clock::to_time_t(tp);
79 std::ostringstream oss;
80 oss << std::put_time(std::gmtime(&time),
"%Y-%m-%dT%H:%M:%SZ");
87std::string measurement_to_json(
const storage::measurement_record &meas) {
88 std::ostringstream oss;
89 oss << R
"({"measurement_id":")" << json_escape(meas.measurement_id)
90 << R"(","sop_instance_uid":")" << json_escape(meas.sop_instance_uid)
91 << R"(","frame_number":)";
92 if (meas.frame_number.has_value()) {
93 oss << meas.frame_number.value();
97 oss << R
"(,"user_id":")" << json_escape(meas.user_id)
98 << R"(","measurement_type":")" << json_escape(to_string(meas.type))
99 << R"(","geometry":)" << meas.geometry_json
100 << R"(,"value":)" << meas.value
103 << R"(","created_at":")" << format_timestamp(meas.created_at) << R"("})";
110std::string measurements_to_json(
111 const std::vector<storage::measurement_record> &measurements,
112 size_t total_count) {
113 std::ostringstream oss;
114 oss << R
"({"data":[)";
115 for (
size_t i = 0; i < measurements.size(); ++i) {
119 oss << measurement_to_json(measurements[i]);
121 oss << R
"(],"pagination":{"total":)" << total_count << R"(,"count":)"
122 << measurements.size() << "}}";
129std::pair<size_t, size_t> parse_pagination(
const crow::request &req) {
133 auto limit_param = req.url_params.get(
"limit");
136 limit = std::stoul(limit_param);
145 auto offset_param = req.url_params.get(
"offset");
148 offset = std::stoul(offset_param);
154 return {limit, offset};
160std::string parse_json_string(
const std::string &json,
const std::string &key) {
161 std::string
search =
"\"" + key +
"\":\"";
162 auto pos = json.find(search);
163 if (pos == std::string::npos) {
167 auto end = json.find(
"\"", pos);
168 if (end == std::string::npos) {
171 return json.substr(pos, end - pos);
177std::optional<int> parse_json_int(
const std::string &json,
178 const std::string &key) {
179 std::string
search =
"\"" + key +
"\":";
180 auto pos = json.find(search);
181 if (pos == std::string::npos) {
185 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
188 if (pos >= json.size()) {
191 if (json.substr(pos, 4) ==
"null") {
195 return std::stoi(json.substr(pos));
204double parse_json_double(
const std::string &json,
const std::string &key) {
205 std::string
search =
"\"" + key +
"\":";
206 auto pos = json.find(search);
207 if (pos == std::string::npos) {
211 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
214 if (pos >= json.size()) {
218 return std::stod(json.substr(pos));
227std::string parse_json_object(
const std::string &json,
const std::string &key) {
228 std::string
search =
"\"" + key +
"\":";
229 auto pos = json.find(search);
230 if (pos == std::string::npos) {
234 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
237 if (pos >= json.size() || json[pos] !=
'{') {
243 for (; pos < json.size(); ++pos) {
244 if (json[pos] ==
'{') {
246 }
else if (json[pos] ==
'}') {
249 return json.substr(start, pos - start + 1);
260 std::shared_ptr<rest_server_context> ctx) {
262 CROW_ROUTE(app,
"/api/v1/measurements")
263 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request &req) {
265 res.add_header(
"Content-Type",
"application/json");
266 add_cors_headers(res, *ctx);
268 if (!ctx->database) {
275 std::string body = req.body;
278 res.body =
make_error_json(
"INVALID_REQUEST",
"Request body is empty");
285 meas.
frame_number = parse_json_int(body,
"frame_number");
286 meas.
user_id = parse_json_string(body,
"user_id");
288 auto type_str = parse_json_string(body,
"measurement_type");
290 if (!type_opt.has_value()) {
292 res.body =
make_error_json(
"INVALID_TYPE",
"Invalid measurement type");
295 meas.
type = type_opt.value();
298 meas.
value = parse_json_double(body,
"value");
299 meas.
unit = parse_json_string(body,
"unit");
300 meas.
label = parse_json_string(body,
"label");
301 meas.
created_at = std::chrono::system_clock::now();
310#ifdef PACS_WITH_DATABASE_SYSTEM
315 auto save_result = repo.
save(meas);
316 if (!save_result.is_ok()) {
324 std::ostringstream oss;
326 << R"(","value":)" << meas.value << R"(,"unit":")"
328 res.body = oss.str();
333 CROW_ROUTE(app,
"/api/v1/measurements")
334 .methods(crow::HTTPMethod::GET)([ctx](
const crow::request &req) {
336 res.add_header(
"Content-Type",
"application/json");
337 add_cors_headers(res, *ctx);
339 if (!ctx->database) {
346 auto [limit, offset] = parse_pagination(req);
350 query.offset = offset;
352 auto sop_instance_uid = req.url_params.get(
"sop_instance_uid");
353 if (sop_instance_uid) {
354 query.sop_instance_uid = sop_instance_uid;
356 auto study_uid = req.url_params.get(
"study_uid");
358 query.study_uid = study_uid;
360 auto user_id = req.url_params.get(
"user_id");
362 query.user_id = user_id;
364 auto measurement_type = req.url_params.get(
"measurement_type");
365 if (measurement_type) {
367 if (type_opt.has_value()) {
368 query.type = type_opt.value();
372#ifdef PACS_WITH_DATABASE_SYSTEM
376 count_query.
limit = 0;
378 auto count_result = repo.
count(count_query);
379 if (!count_result.is_ok()) {
381 res.body =
make_error_json(
"COUNT_ERROR", count_result.error().message);
384 size_t total_count = count_result.value();
386 auto measurements_result = repo.
search(query);
387 if (!measurements_result.is_ok()) {
389 res.body =
make_error_json(
"QUERY_ERROR", measurements_result.error().message);
393 res.body = measurements_to_json(measurements_result.value(), total_count);
398 count_query.
limit = 0;
400 size_t total_count = repo.
count(count_query);
402 auto measurements = repo.
search(query);
404 res.body = measurements_to_json(measurements, total_count);
410 CROW_ROUTE(app,
"/api/v1/measurements/<string>")
411 .methods(crow::HTTPMethod::GET)(
412 [ctx](
const crow::request & ,
const std::string &measurement_id) {
414 res.add_header(
"Content-Type",
"application/json");
415 add_cors_headers(res, *ctx);
417 if (!ctx->database) {
420 "Database not configured");
424#ifdef PACS_WITH_DATABASE_SYSTEM
426 auto meas_result = repo.
find_by_id(measurement_id);
427 if (!meas_result.is_ok()) {
429 if (meas_result.error().message.find(
"not found") != std::string::npos ||
430 meas_result.error().message.find(
"NOT_FOUND") != std::string::npos) {
435 res.body =
make_error_json(
"QUERY_ERROR", meas_result.error().message);
440 res.body = measurement_to_json(meas_result.value());
443 auto meas_result = repo.
find_by_id(measurement_id);
444 if (!meas_result.has_value()) {
450 res.body = measurement_to_json(meas_result.value());
456 CROW_ROUTE(app,
"/api/v1/measurements/<string>")
457 .methods(crow::HTTPMethod::DELETE)(
458 [ctx](
const crow::request & ,
const std::string &measurement_id) {
460 add_cors_headers(res, *ctx);
462 if (!ctx->database) {
464 res.add_header(
"Content-Type",
"application/json");
466 "Database not configured");
470#ifdef PACS_WITH_DATABASE_SYSTEM
472 auto exists_result = repo.
exists(measurement_id);
473 if (!exists_result.is_ok()) {
475 res.add_header(
"Content-Type",
"application/json");
476 res.body =
make_error_json(
"QUERY_ERROR", exists_result.error().message);
479 if (!exists_result.value()) {
481 res.add_header(
"Content-Type",
"application/json");
487 if (!repo.
exists(measurement_id)) {
489 res.add_header(
"Content-Type",
"application/json");
495 auto remove_result = repo.
remove(measurement_id);
496 if (!remove_result.is_ok()) {
498 res.add_header(
"Content-Type",
"application/json");
509 CROW_ROUTE(app,
"/api/v1/instances/<string>/measurements")
510 .methods(crow::HTTPMethod::GET)(
511 [ctx](
const crow::request & ,
512 const std::string &sop_instance_uid) {
514 res.add_header(
"Content-Type",
"application/json");
515 add_cors_headers(res, *ctx);
517 if (!ctx->database) {
520 "Database not configured");
524#ifdef PACS_WITH_DATABASE_SYSTEM
527 if (!measurements_result.is_ok()) {
529 res.body =
make_error_json(
"QUERY_ERROR", measurements_result.error().message);
532 const auto& measurements = measurements_result.value();
538 std::ostringstream oss;
539 oss << R
"({"data":[)";
540 for (
size_t i = 0; i < measurements.size(); ++i) {
544 oss << measurement_to_json(measurements[i]);
549 res.body = oss.str();
Repository for measurement persistence (legacy SQLite interface)
auto find_by_id(std::string_view measurement_id) const -> std::optional< measurement_record >
auto find_by_instance(std::string_view sop_instance_uid) const -> std::vector< measurement_record >
auto remove(std::string_view measurement_id) -> VoidResult
auto search(const measurement_query &query) const -> std::vector< measurement_record >
auto save(const measurement_record &record) -> VoidResult
auto count() const -> size_t
auto exists(std::string_view measurement_id) const -> bool
PACS index database for metadata storage and retrieval.
Measurement API endpoints for REST server.
Measurement record data structures for database operations.
Repository for measurement persistence using base_repository pattern.
auto measurement_type_from_string(std::string_view str) -> std::optional< measurement_type >
Parse string to measurement_type.
constexpr std::string_view search
void register_measurement_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.
Configuration for REST API server.
Common types and utilities for REST API.
size_t offset
Offset for pagination.
size_t limit
Maximum number of results to return (0 = unlimited)
Measurement record from the database.
std::string unit
Unit of measurement (mm, cm2, degrees, HU, g/ml, etc.)
std::string measurement_id
Unique measurement identifier (UUID)
std::string geometry_json
Geometry data as JSON string (coordinates)
std::chrono::system_clock::time_point created_at
Record creation timestamp.
std::string sop_instance_uid
SOP Instance UID - DICOM tag (0008,0018)
measurement_type type
Type of measurement.
std::string label
Optional label/description.
std::optional< int > frame_number
Frame number for multi-frame images (1-based)
double value
Calculated measurement value.
std::string user_id
User who created the measurement.
System API endpoints for REST server.