PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
study_endpoints.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13// IMPORTANT: Include Crow FIRST before any PACS headers to avoid forward
14// declaration conflicts
15#include "crow.h"
16
17// Workaround for Windows: DELETE is defined as a macro in <winnt.h>
18// which conflicts with crow::HTTPMethod::DELETE
19#ifdef DELETE
20#undef DELETE
21#endif
22
31
32#include <sstream>
33
35
36namespace {
37
41void add_cors_headers(crow::response &res, const rest_server_context &ctx) {
42 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
43 res.add_header("Access-Control-Allow-Origin",
44 ctx.config->cors_allowed_origins);
45 }
46}
47
51std::string study_to_json(const storage::study_record &study) {
52 std::ostringstream oss;
53 oss << R"({"pk":)" << study.pk << R"(,"patient_pk":)" << study.patient_pk
54 << R"(,"study_instance_uid":")" << json_escape(study.study_uid)
55 << R"(","study_id":")" << json_escape(study.study_id)
56 << R"(","study_date":")" << json_escape(study.study_date)
57 << R"(","study_time":")" << json_escape(study.study_time)
58 << R"(","accession_number":")" << json_escape(study.accession_number)
59 << R"(","referring_physician":")"
60 << json_escape(study.referring_physician) << R"(","study_description":")"
61 << json_escape(study.study_description) << R"(","modalities_in_study":")"
62 << json_escape(study.modalities_in_study) << R"(","num_series":)"
63 << study.num_series << R"(,"num_instances":)" << study.num_instances
64 << "}";
65 return oss.str();
66}
67
71std::string studies_to_json(const std::vector<storage::study_record> &studies,
72 size_t total_count) {
73 std::ostringstream oss;
74 oss << R"({"data":[)";
75 for (size_t i = 0; i < studies.size(); ++i) {
76 if (i > 0) {
77 oss << ",";
78 }
79 oss << study_to_json(studies[i]);
80 }
81 oss << R"(],"pagination":{"total":)" << total_count << R"(,"count":)"
82 << studies.size() << "}}";
83 return oss.str();
84}
85
89std::string series_to_json(const storage::series_record &series) {
90 std::ostringstream oss;
91 oss << R"({"pk":)" << series.pk << R"(,"study_pk":)" << series.study_pk
92 << R"(,"series_instance_uid":")" << json_escape(series.series_uid)
93 << R"(","modality":")" << json_escape(series.modality)
94 << R"(","series_number":)";
95 if (series.series_number) {
96 oss << *series.series_number;
97 } else {
98 oss << "null";
99 }
100 oss << R"(,"series_description":")" << json_escape(series.series_description)
101 << R"(","body_part_examined":")" << json_escape(series.body_part_examined)
102 << R"(","station_name":")" << json_escape(series.station_name)
103 << R"(","num_instances":)" << series.num_instances << "}";
104 return oss.str();
105}
106
110std::string series_list_to_json(const std::vector<storage::series_record> &series_list) {
111 std::ostringstream oss;
112 oss << R"({"data":[)";
113 for (size_t i = 0; i < series_list.size(); ++i) {
114 if (i > 0) {
115 oss << ",";
116 }
117 oss << series_to_json(series_list[i]);
118 }
119 oss << R"(],"count":)" << series_list.size() << "}";
120 return oss.str();
121}
122
126std::string instance_to_json(const storage::instance_record &instance) {
127 std::ostringstream oss;
128 oss << R"({"pk":)" << instance.pk << R"(,"series_pk":)" << instance.series_pk
129 << R"(,"sop_instance_uid":")" << json_escape(instance.sop_uid)
130 << R"(","sop_class_uid":")" << json_escape(instance.sop_class_uid)
131 << R"(","transfer_syntax":")" << json_escape(instance.transfer_syntax)
132 << R"(","instance_number":)";
133 if (instance.instance_number) {
134 oss << *instance.instance_number;
135 } else {
136 oss << "null";
137 }
138 oss << R"(,"file_size":)" << instance.file_size << "}";
139 return oss.str();
140}
141
145std::string instances_to_json(const std::vector<storage::instance_record> &instances) {
146 std::ostringstream oss;
147 oss << R"({"data":[)";
148 for (size_t i = 0; i < instances.size(); ++i) {
149 if (i > 0) {
150 oss << ",";
151 }
152 oss << instance_to_json(instances[i]);
153 }
154 oss << R"(],"count":)" << instances.size() << "}";
155 return oss.str();
156}
157
161std::pair<size_t, size_t> parse_pagination(const crow::request &req) {
162 size_t limit = 20; // Default limit
163 size_t offset = 0;
164
165 auto limit_param = req.url_params.get("limit");
166 if (limit_param) {
167 try {
168 limit = std::stoul(limit_param);
169 if (limit > 100) {
170 limit = 100; // Cap at 100
171 }
172 } catch (...) {
173 // Use default
174 }
175 }
176
177 auto offset_param = req.url_params.get("offset");
178 if (offset_param) {
179 try {
180 offset = std::stoul(offset_param);
181 } catch (...) {
182 // Use default
183 }
184 }
185
186 return {limit, offset};
187}
188
189} // namespace
190
191// Internal implementation function called from rest_server.cpp
192void register_study_endpoints_impl(crow::SimpleApp &app,
193 std::shared_ptr<rest_server_context> ctx) {
194 // GET /api/v1/studies - List studies (paginated)
195 CROW_ROUTE(app, "/api/v1/studies")
196 .methods(crow::HTTPMethod::GET)([ctx](const crow::request &req) {
197 crow::response res;
198 res.add_header("Content-Type", "application/json");
199 add_cors_headers(res, *ctx);
200
201 if (!ctx->database) {
202 res.code = 503;
203 res.body =
204 make_error_json("DATABASE_UNAVAILABLE", "Database not configured");
205 return res;
206 }
207
208 // Parse pagination
209 auto [limit, offset] = parse_pagination(req);
210
211 // Build query from URL parameters
213 query.limit = limit;
214 query.offset = offset;
215
216 auto patient_id = req.url_params.get("patient_id");
217 if (patient_id) {
218 query.patient_id = patient_id;
219 }
220
221 auto patient_name = req.url_params.get("patient_name");
222 if (patient_name) {
223 query.patient_name = patient_name;
224 }
225
226 auto study_uid = req.url_params.get("study_uid");
227 if (study_uid) {
228 query.study_uid = study_uid;
229 }
230
231 auto study_id = req.url_params.get("study_id");
232 if (study_id) {
233 query.study_id = study_id;
234 }
235
236 auto study_date = req.url_params.get("study_date");
237 if (study_date) {
238 query.study_date = study_date;
239 }
240
241 auto study_date_from = req.url_params.get("study_date_from");
242 if (study_date_from) {
243 query.study_date_from = study_date_from;
244 }
245
246 auto study_date_to = req.url_params.get("study_date_to");
247 if (study_date_to) {
248 query.study_date_to = study_date_to;
249 }
250
251 auto accession_number = req.url_params.get("accession_number");
252 if (accession_number) {
253 query.accession_number = accession_number;
254 }
255
256 auto modality = req.url_params.get("modality");
257 if (modality) {
258 query.modality = modality;
259 }
260
261 auto referring_physician = req.url_params.get("referring_physician");
262 if (referring_physician) {
263 query.referring_physician = referring_physician;
264 }
265
266 auto study_description = req.url_params.get("study_description");
267 if (study_description) {
268 query.study_description = study_description;
269 }
270
271 // Get total count (without pagination)
272 storage::study_query count_query = query;
273 count_query.limit = 0;
274 count_query.offset = 0;
275 auto all_studies_result = ctx->database->search_studies(count_query);
276 if (!all_studies_result.is_ok()) {
277 res.code = 500;
278 res.body = make_error_json("QUERY_ERROR",
279 all_studies_result.error().message);
280 return res;
281 }
282 size_t total_count = all_studies_result.value().size();
283
284 // Get paginated results
285 auto studies_result = ctx->database->search_studies(query);
286 if (!studies_result.is_ok()) {
287 res.code = 500;
288 res.body = make_error_json("QUERY_ERROR",
289 studies_result.error().message);
290 return res;
291 }
292
293 res.code = 200;
294 res.body = studies_to_json(studies_result.value(), total_count);
295 return res;
296 });
297
298 // GET /api/v1/studies/:uid - Get study details
299 CROW_ROUTE(app, "/api/v1/studies/<string>")
300 .methods(crow::HTTPMethod::GET)(
301 [ctx](const crow::request & /*req*/, const std::string &study_uid) {
302 crow::response res;
303 res.add_header("Content-Type", "application/json");
304 add_cors_headers(res, *ctx);
305
306 if (!ctx->database) {
307 res.code = 503;
308 res.body = make_error_json("DATABASE_UNAVAILABLE",
309 "Database not configured");
310 return res;
311 }
312
313 auto study = ctx->database->find_study(study_uid);
314 if (!study) {
315 res.code = 404;
316 res.body = make_error_json("NOT_FOUND", "Study not found");
317 return res;
318 }
319
320 res.code = 200;
321 res.body = study_to_json(*study);
322 return res;
323 });
324
325 // GET /api/v1/studies/:uid/series - Get study's series
326 CROW_ROUTE(app, "/api/v1/studies/<string>/series")
327 .methods(crow::HTTPMethod::GET)(
328 [ctx](const crow::request & /*req*/, const std::string &study_uid) {
329 crow::response res;
330 res.add_header("Content-Type", "application/json");
331 add_cors_headers(res, *ctx);
332
333 if (!ctx->database) {
334 res.code = 503;
335 res.body = make_error_json("DATABASE_UNAVAILABLE",
336 "Database not configured");
337 return res;
338 }
339
340 // Verify study exists
341 auto study = ctx->database->find_study(study_uid);
342 if (!study) {
343 res.code = 404;
344 res.body = make_error_json("NOT_FOUND", "Study not found");
345 return res;
346 }
347
348 auto series_list_result = ctx->database->list_series(study_uid);
349 if (!series_list_result.is_ok()) {
350 res.code = 500;
351 res.body = make_error_json("QUERY_ERROR",
352 series_list_result.error().message);
353 return res;
354 }
355
356 res.code = 200;
357 res.body = series_list_to_json(series_list_result.value());
358 return res;
359 });
360
361 // GET /api/v1/studies/:uid/instances - Get study's instances (all series)
362 CROW_ROUTE(app, "/api/v1/studies/<string>/instances")
363 .methods(crow::HTTPMethod::GET)(
364 [ctx](const crow::request & /*req*/, const std::string &study_uid) {
365 crow::response res;
366 res.add_header("Content-Type", "application/json");
367 add_cors_headers(res, *ctx);
368
369 if (!ctx->database) {
370 res.code = 503;
371 res.body = make_error_json("DATABASE_UNAVAILABLE",
372 "Database not configured");
373 return res;
374 }
375
376 // Verify study exists
377 auto study = ctx->database->find_study(study_uid);
378 if (!study) {
379 res.code = 404;
380 res.body = make_error_json("NOT_FOUND", "Study not found");
381 return res;
382 }
383
384 // Get all series and their instances
385 std::vector<storage::instance_record> all_instances;
386 auto series_list_result = ctx->database->list_series(study_uid);
387 if (!series_list_result.is_ok()) {
388 res.code = 500;
389 res.body = make_error_json("QUERY_ERROR",
390 series_list_result.error().message);
391 return res;
392 }
393 for (const auto &series : series_list_result.value()) {
394 auto instances_result = ctx->database->list_instances(series.series_uid);
395 if (!instances_result.is_ok()) {
396 res.code = 500;
397 res.body = make_error_json("QUERY_ERROR",
398 instances_result.error().message);
399 return res;
400 }
401 const auto& instances = instances_result.value();
402 all_instances.insert(all_instances.end(), instances.begin(),
403 instances.end());
404 }
405
406 res.code = 200;
407 res.body = instances_to_json(all_instances);
408 return res;
409 });
410
411 // DELETE /api/v1/studies/:uid - Delete study
412 CROW_ROUTE(app, "/api/v1/studies/<string>")
413 .methods(crow::HTTPMethod::DELETE)(
414 [ctx](const crow::request & /*req*/, const std::string &study_uid) {
415 crow::response res;
416 res.add_header("Content-Type", "application/json");
417 add_cors_headers(res, *ctx);
418
419 if (!ctx->database) {
420 res.code = 503;
421 res.body = make_error_json("DATABASE_UNAVAILABLE",
422 "Database not configured");
423 return res;
424 }
425
426 // Verify study exists
427 auto study = ctx->database->find_study(study_uid);
428 if (!study) {
429 res.code = 404;
430 res.body = make_error_json("NOT_FOUND", "Study not found");
431 return res;
432 }
433
434 auto result = ctx->database->delete_study(study_uid);
435 if (result.is_err()) {
436 res.code = 500;
437 res.body = make_error_json("DELETE_FAILED",
438 result.error().message);
439 return res;
440 }
441
442 res.code = 200;
443 res.body = make_success_json("Study deleted successfully");
444 return res;
445 });
446}
447
448} // namespace kcenon::pacs::web::endpoints
PACS index database for metadata storage and retrieval.
Instance record data structures for database operations.
void register_study_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.
Definition rest_types.h:79
std::string json_escape(std::string_view s)
Escape a string for JSON.
Definition rest_types.h:101
std::string make_success_json(std::string_view message="OK")
Create success response with optional message.
Definition rest_types.h:91
Configuration for REST API server.
Common types and utilities for REST API.
Series record data structures for database operations.
size_t limit
Maximum number of results to return (0 = unlimited)
size_t offset
Offset for pagination.
Study API endpoints for REST server.
Study record data structures for database operations.
System API endpoints for REST server.