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.
Query parameters for patient search.
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.