PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
patient_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
24
25#include <sstream>
26
28
29namespace {
30
34void add_cors_headers(crow::response &res, const rest_server_context &ctx) {
35 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
36 res.add_header("Access-Control-Allow-Origin",
37 ctx.config->cors_allowed_origins);
38 }
39}
40
44std::string patient_to_json(const storage::patient_record &patient) {
45 std::ostringstream oss;
46 oss << R"({"pk":)" << patient.pk << R"(,"patient_id":")"
47 << json_escape(patient.patient_id) << R"(","patient_name":")"
48 << json_escape(patient.patient_name) << R"(","birth_date":")"
49 << json_escape(patient.birth_date) << R"(","sex":")"
50 << json_escape(patient.sex) << R"(","other_ids":")"
51 << json_escape(patient.other_ids) << R"(","ethnic_group":")"
52 << json_escape(patient.ethnic_group) << R"(","comments":")"
53 << json_escape(patient.comments) << R"("})";
54 return oss.str();
55}
56
60std::string patients_to_json(const std::vector<storage::patient_record> &patients,
61 size_t total_count) {
62 std::ostringstream oss;
63 oss << R"({"data":[)";
64 for (size_t i = 0; i < patients.size(); ++i) {
65 if (i > 0) {
66 oss << ",";
67 }
68 oss << patient_to_json(patients[i]);
69 }
70 oss << R"(],"pagination":{"total":)" << total_count << R"(,"count":)"
71 << patients.size() << "}}";
72 return oss.str();
73}
74
78std::string study_to_json(const storage::study_record &study) {
79 std::ostringstream oss;
80 oss << R"({"pk":)" << study.pk << R"(,"study_instance_uid":")"
81 << json_escape(study.study_uid) << R"(","study_id":")"
82 << json_escape(study.study_id) << R"(","study_date":")"
83 << json_escape(study.study_date) << R"(","study_time":")"
84 << json_escape(study.study_time) << R"(","accession_number":")"
85 << json_escape(study.accession_number) << R"(","referring_physician":")"
86 << json_escape(study.referring_physician) << R"(","study_description":")"
87 << json_escape(study.study_description) << R"(","modalities_in_study":")"
88 << json_escape(study.modalities_in_study) << R"(","num_series":)"
89 << study.num_series << R"(,"num_instances":)" << study.num_instances
90 << "}";
91 return oss.str();
92}
93
97std::string studies_to_json(const std::vector<storage::study_record> &studies) {
98 std::ostringstream oss;
99 oss << R"({"data":[)";
100 for (size_t i = 0; i < studies.size(); ++i) {
101 if (i > 0) {
102 oss << ",";
103 }
104 oss << study_to_json(studies[i]);
105 }
106 oss << R"(],"count":)" << studies.size() << "}";
107 return oss.str();
108}
109
113std::pair<size_t, size_t> parse_pagination(const crow::request &req) {
114 size_t limit = 20; // Default limit
115 size_t offset = 0;
116
117 auto limit_param = req.url_params.get("limit");
118 if (limit_param) {
119 try {
120 limit = std::stoul(limit_param);
121 if (limit > 100) {
122 limit = 100; // Cap at 100
123 }
124 } catch (...) {
125 // Use default
126 }
127 }
128
129 auto offset_param = req.url_params.get("offset");
130 if (offset_param) {
131 try {
132 offset = std::stoul(offset_param);
133 } catch (...) {
134 // Use default
135 }
136 }
137
138 return {limit, offset};
139}
140
141} // namespace
142
143// Internal implementation function called from rest_server.cpp
144void register_patient_endpoints_impl(crow::SimpleApp &app,
145 std::shared_ptr<rest_server_context> ctx) {
146 // GET /api/v1/patients - List patients (paginated)
147 CROW_ROUTE(app, "/api/v1/patients")
148 .methods(crow::HTTPMethod::GET)([ctx](const crow::request &req) {
149 crow::response res;
150 res.add_header("Content-Type", "application/json");
151 add_cors_headers(res, *ctx);
152
153 if (!ctx->database) {
154 res.code = 503;
155 res.body =
156 make_error_json("DATABASE_UNAVAILABLE", "Database not configured");
157 return res;
158 }
159
160 // Parse pagination
161 auto [limit, offset] = parse_pagination(req);
162
163 // Build query from URL parameters
165 query.limit = limit;
166 query.offset = offset;
167
168 auto patient_id = req.url_params.get("patient_id");
169 if (patient_id) {
170 query.patient_id = patient_id;
171 }
172
173 auto patient_name = req.url_params.get("patient_name");
174 if (patient_name) {
175 query.patient_name = patient_name;
176 }
177
178 auto birth_date = req.url_params.get("birth_date");
179 if (birth_date) {
180 query.birth_date = birth_date;
181 }
182
183 auto birth_date_from = req.url_params.get("birth_date_from");
184 if (birth_date_from) {
185 query.birth_date_from = birth_date_from;
186 }
187
188 auto birth_date_to = req.url_params.get("birth_date_to");
189 if (birth_date_to) {
190 query.birth_date_to = birth_date_to;
191 }
192
193 auto sex = req.url_params.get("sex");
194 if (sex) {
195 query.sex = sex;
196 }
197
198 // Get total count (without pagination for accurate count)
199 storage::patient_query count_query = query;
200 count_query.limit = 0;
201 count_query.offset = 0;
202 auto all_patients_result = ctx->database->search_patients(count_query);
203 if (!all_patients_result.is_ok()) {
204 res.code = 500;
205 res.body = make_error_json("QUERY_ERROR",
206 all_patients_result.error().message);
207 return res;
208 }
209 size_t total_count = all_patients_result.value().size();
210
211 // Get paginated results
212 auto patients_result = ctx->database->search_patients(query);
213 if (!patients_result.is_ok()) {
214 res.code = 500;
215 res.body = make_error_json("QUERY_ERROR",
216 patients_result.error().message);
217 return res;
218 }
219
220 res.code = 200;
221 res.body = patients_to_json(patients_result.value(), total_count);
222 return res;
223 });
224
225 // GET /api/v1/patients/:id - Get patient details
226 CROW_ROUTE(app, "/api/v1/patients/<string>")
227 .methods(crow::HTTPMethod::GET)(
228 [ctx](const crow::request & /*req*/, const std::string &patient_id) {
229 crow::response res;
230 res.add_header("Content-Type", "application/json");
231 add_cors_headers(res, *ctx);
232
233 if (!ctx->database) {
234 res.code = 503;
235 res.body = make_error_json("DATABASE_UNAVAILABLE",
236 "Database not configured");
237 return res;
238 }
239
240 auto patient = ctx->database->find_patient(patient_id);
241 if (!patient) {
242 res.code = 404;
243 res.body = make_error_json("NOT_FOUND", "Patient not found");
244 return res;
245 }
246
247 res.code = 200;
248 res.body = patient_to_json(*patient);
249 return res;
250 });
251
252 // GET /api/v1/patients/:id/studies - Get patient's studies
253 CROW_ROUTE(app, "/api/v1/patients/<string>/studies")
254 .methods(crow::HTTPMethod::GET)(
255 [ctx](const crow::request & /*req*/, const std::string &patient_id) {
256 crow::response res;
257 res.add_header("Content-Type", "application/json");
258 add_cors_headers(res, *ctx);
259
260 if (!ctx->database) {
261 res.code = 503;
262 res.body = make_error_json("DATABASE_UNAVAILABLE",
263 "Database not configured");
264 return res;
265 }
266
267 // Verify patient exists
268 auto patient = ctx->database->find_patient(patient_id);
269 if (!patient) {
270 res.code = 404;
271 res.body = make_error_json("NOT_FOUND", "Patient not found");
272 return res;
273 }
274
275 auto studies_result = ctx->database->list_studies(patient_id);
276 if (!studies_result.is_ok()) {
277 res.code = 500;
278 res.body = make_error_json("QUERY_ERROR",
279 studies_result.error().message);
280 return res;
281 }
282
283 res.code = 200;
284 res.body = studies_to_json(studies_result.value());
285 return res;
286 });
287}
288
289} // namespace kcenon::pacs::web::endpoints
PACS index database for metadata storage and retrieval.
void register_patient_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
Patient API endpoints for REST server.
Patient record data structures for database operations.
Configuration for REST API server.
Common types and utilities for REST API.
size_t limit
Maximum number of results to return (0 = unlimited)
size_t offset
Offset for pagination.
Study record data structures for database operations.
System API endpoints for REST server.