trainlog/tui/src/web_sleep.c

751 lines
34 KiB
C

#include "trainlog/web_sleep.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <yyjson.h>
#include "trainlog/id.h"
#include "trainlog/sleep_diary.h"
#include "timestamp.h"
typedef struct SleepListContext {
yyjson_mut_doc *document;
yyjson_mut_val *items;
size_t count;
int64_t sleep_seconds;
int64_t long_awake_seconds;
int64_t nap_seconds;
size_t long_awake_count;
size_t nap_count;
size_t sleepiness_count;
size_t intake_count;
size_t quality[3][5];
int64_t bed_minute_total;
int64_t get_up_minute_total;
size_t bed_count;
size_t get_up_count;
} SleepListContext;
static bool event_duration(const TrainlogSleepEvent *event, int64_t *seconds) {
TrainlogTimestampKey start;
TrainlogTimestampKey end;
if (event->end_at[0] == '\0' ||
!trainlog_timestamp_parse(event->start_at, strlen(event->start_at), &start) ||
!trainlog_timestamp_parse(event->end_at, strlen(event->end_at), &end) ||
end.utc_second <= start.utc_second) {
return false;
}
*seconds = end.utc_second - start.utc_second;
return true;
}
/* WHY: ordinary HH:MM arithmetic maps 23:30 and 00:30 to noon. CONTRACT:
* analysis projects clock readings onto the diary's 18:00 -> 18:00 axis.
* INVARIANT: storage remains an absolute offset-bearing timestamp. */
static bool diary_minute(const char *timestamp, int64_t *minute) {
const char *separator = strchr(timestamp, 'T');
int hour;
int value;
if (separator == NULL || sscanf(separator + 1, "%2d:%2d", &hour, &value) != 2 || hour < 0 ||
hour > 23 || value < 0 || value > 59) {
return false;
}
*minute = (int64_t)((hour < 18 ? hour + 24 : hour) * 60 + value) - 18 * 60;
return true;
}
static const char *quality_text(TrainlogSleepQuality quality) {
static const char *const values[] = {NULL, "TB", "B", "Moy", "M", "TM"};
return quality >= TRAINLOG_SLEEP_QUALITY_UNSET && quality <= TRAINLOG_SLEEP_QUALITY_TM
? values[quality]
: NULL;
}
static const char *publication_text(TrainlogSleepPublicationStatus status) {
static const char *const values[] = {"draft", "ready", "synchronized", "modified"};
return status >= TRAINLOG_SLEEP_PUBLICATION_DRAFT &&
status <= TRAINLOG_SLEEP_PUBLICATION_MODIFIED
? values[status]
: NULL;
}
static bool quality_value(yyjson_val *value, TrainlogSleepQuality *output) {
int index;
if (yyjson_is_null(value)) {
*output = TRAINLOG_SLEEP_QUALITY_UNSET;
return true;
}
if (!yyjson_is_str(value)) {
return false;
}
for (index = 1; index <= (int)TRAINLOG_SLEEP_QUALITY_TM; ++index) {
if (strcmp(yyjson_get_str(value), quality_text((TrainlogSleepQuality)index)) == 0) {
*output = (TrainlogSleepQuality)index;
return true;
}
}
return false;
}
static const char *event_text(TrainlogSleepEventType type) {
static const char *const values[] = {"bed_time",
"final_get_up",
"night_get_up",
"sleep",
"nap",
"long_awake",
"half_sleep",
"daytime_sleepiness"};
return type >= TRAINLOG_SLEEP_EVENT_BED_TIME && type <= TRAINLOG_SLEEP_EVENT_DAYTIME_SLEEPINESS
? values[type]
: NULL;
}
static bool event_value(yyjson_val *value, TrainlogSleepEventType *output) {
int index;
if (!yyjson_is_str(value)) {
return false;
}
for (index = 0; index <= (int)TRAINLOG_SLEEP_EVENT_DAYTIME_SLEEPINESS; ++index) {
if (strcmp(yyjson_get_str(value), event_text((TrainlogSleepEventType)index)) == 0) {
*output = (TrainlogSleepEventType)index;
return true;
}
}
return false;
}
static bool add_nullable_quality(yyjson_mut_doc *document,
yyjson_mut_val *object,
const char *key,
TrainlogSleepQuality quality) {
return quality == TRAINLOG_SLEEP_QUALITY_UNSET
? yyjson_mut_obj_add_null(document, object, key)
: yyjson_mut_obj_add_strcpy(document, object, key, quality_text(quality));
}
static bool
add_entry(yyjson_mut_doc *document, yyjson_mut_val *items, const TrainlogSleepDiaryEntry *entry) {
yyjson_mut_val *object = yyjson_mut_obj(document);
yyjson_mut_val *events = yyjson_mut_arr(document);
yyjson_mut_val *intakes = yyjson_mut_arr(document);
size_t index;
if (object == NULL || events == NULL || intakes == NULL ||
!yyjson_mut_obj_add_strcpy(document, object, "entry_id", entry->entry_id) ||
!yyjson_mut_obj_add_strcpy(document, object, "night_start_date", entry->night_start_date) ||
!yyjson_mut_obj_add_strcpy(document, object, "night_end_date", entry->night_end_date) ||
!yyjson_mut_obj_add_strcpy(document, object, "created_at", entry->created_at) ||
!yyjson_mut_obj_add_strcpy(document, object, "updated_at", entry->updated_at) ||
!yyjson_mut_obj_add_strcpy(document, object, "revision_id", entry->revision_id) ||
!yyjson_mut_obj_add_strcpy(
document, object, "publication_status", publication_text(entry->publication_status)) ||
!add_nullable_quality(document, object, "sleep_quality", entry->sleep_quality) ||
!add_nullable_quality(document, object, "wake_quality", entry->wake_quality) ||
!add_nullable_quality(document, object, "day_form", entry->day_form) ||
!yyjson_mut_obj_add_strcpy(
document, object, "treatment_and_notes", entry->treatment_and_notes)) {
return false;
}
for (index = 0U; index < entry->event_count; ++index) {
const TrainlogSleepEvent *event = &entry->events[index];
yyjson_mut_val *item = yyjson_mut_obj(document);
if (item == NULL ||
!yyjson_mut_obj_add_strcpy(document, item, "event_id", event->event_id) ||
!yyjson_mut_obj_add_strcpy(document, item, "type", event_text(event->type)) ||
!yyjson_mut_obj_add_strcpy(document, item, "start_at", event->start_at) ||
!(event->end_at[0] == '\0'
? yyjson_mut_obj_add_null(document, item, "end_at")
: yyjson_mut_obj_add_strcpy(document, item, "end_at", event->end_at)) ||
!yyjson_mut_arr_add_val(events, item)) {
return false;
}
}
for (index = 0U; index < entry->intake_count; ++index) {
const TrainlogMedicationIntake *intake = &entry->intakes[index];
yyjson_mut_val *item = yyjson_mut_obj(document);
if (item == NULL ||
!yyjson_mut_obj_add_strcpy(document, item, "intake_id", intake->intake_id) ||
!yyjson_mut_obj_add_strcpy(document, item, "medication_id", intake->medication_id) ||
!yyjson_mut_obj_add_strcpy(
document, item, "medication_name", intake->medication_name) ||
!yyjson_mut_obj_add_strcpy(document, item, "taken_at", intake->taken_at) ||
!(intake->has_dose
? yyjson_mut_obj_add_real(document, item, "dose_value", intake->dose_value)
: yyjson_mut_obj_add_null(document, item, "dose_value")) ||
!(intake->has_dose
? yyjson_mut_obj_add_strcpy(document, item, "dose_unit", intake->dose_unit)
: yyjson_mut_obj_add_null(document, item, "dose_unit")) ||
!yyjson_mut_obj_add_strcpy(document, item, "note", intake->note) ||
!yyjson_mut_obj_add_strcpy(document, item, "created_at", intake->created_at) ||
!yyjson_mut_arr_add_val(intakes, item)) {
return false;
}
}
return yyjson_mut_obj_add_val(document, object, "events", events) &&
yyjson_mut_obj_add_val(document, object, "intakes", intakes) &&
yyjson_mut_arr_add_val(items, object);
}
static TrainlogStatus list_visitor(void *opaque, const TrainlogSleepDiaryEntry *entry) {
SleepListContext *context = opaque;
size_t index;
if (!add_entry(context->document, context->items, entry)) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
++context->count;
context->intake_count += entry->intake_count;
if (entry->sleep_quality != TRAINLOG_SLEEP_QUALITY_UNSET) {
++context->quality[0][entry->sleep_quality - 1];
}
if (entry->wake_quality != TRAINLOG_SLEEP_QUALITY_UNSET) {
++context->quality[1][entry->wake_quality - 1];
}
if (entry->day_form != TRAINLOG_SLEEP_QUALITY_UNSET) {
++context->quality[2][entry->day_form - 1];
}
for (index = 0U; index < entry->event_count; ++index) {
const TrainlogSleepEvent *event = &entry->events[index];
int64_t duration;
int64_t minute;
if (event->type == TRAINLOG_SLEEP_EVENT_LONG_AWAKE) {
++context->long_awake_count;
}
if (event->type == TRAINLOG_SLEEP_EVENT_NAP) {
++context->nap_count;
}
if (event->type == TRAINLOG_SLEEP_EVENT_DAYTIME_SLEEPINESS) {
++context->sleepiness_count;
}
if (event_duration(event, &duration)) {
if (event->type == TRAINLOG_SLEEP_EVENT_SLEEP) {
context->sleep_seconds += duration;
}
if (event->type == TRAINLOG_SLEEP_EVENT_LONG_AWAKE) {
context->long_awake_seconds += duration;
}
if (event->type == TRAINLOG_SLEEP_EVENT_NAP) {
context->nap_seconds += duration;
}
}
if (event->type == TRAINLOG_SLEEP_EVENT_BED_TIME &&
diary_minute(event->start_at, &minute)) {
context->bed_minute_total += minute;
++context->bed_count;
}
if (event->type == TRAINLOG_SLEEP_EVENT_FINAL_GET_UP &&
diary_minute(event->start_at, &minute)) {
context->get_up_minute_total += minute;
++context->get_up_count;
}
}
return TRAINLOG_STATUS_OK;
}
static TrainlogStatus
write_document(yyjson_mut_doc *document, char **output_json, size_t *output_size) {
yyjson_write_err error;
char *json = yyjson_mut_write_opts(document, YYJSON_WRITE_NOFLAG, NULL, output_size, &error);
yyjson_mut_doc_free(document);
if (json == NULL || *output_size > TRAINLOG_WEB_SLEEP_BYTES_MAX) {
free(json);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
*output_json = json;
return TRAINLOG_STATUS_OK;
}
TrainlogStatus trainlog_web_sleep_list_json(TrainlogDatabase *database,
const char *start_date,
const char *end_date,
size_t limit,
char **output_json,
size_t *output_size) {
static const char *const quality_names[] = {"TB", "B", "Moy", "M", "TM"};
yyjson_mut_doc *document;
yyjson_mut_val *root;
yyjson_mut_val *summary;
yyjson_mut_val *distribution;
SleepListContext context = {0};
TrainlogStatus status;
size_t category, quality;
if (database == NULL || output_json == NULL || output_size == NULL) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
*output_json = NULL;
*output_size = 0U;
document = yyjson_mut_doc_new(NULL);
root = document == NULL ? NULL : yyjson_mut_obj(document);
context.items = document == NULL ? NULL : yyjson_mut_arr(document);
summary = document == NULL ? NULL : yyjson_mut_obj(document);
distribution = document == NULL ? NULL : yyjson_mut_obj(document);
if (document == NULL || root == NULL || context.items == NULL || summary == NULL ||
distribution == NULL) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
context.document = document;
yyjson_mut_doc_set_root(document, root);
status =
trainlog_sleep_diary_list(database, start_date, end_date, limit, list_visitor, &context);
if (status != TRAINLOG_STATUS_OK ||
!yyjson_mut_obj_add_uint(document, root, "api_version", 1U) ||
!yyjson_mut_obj_add_uint(document, summary, "nights", context.count) ||
!yyjson_mut_obj_add_uint(document, summary, "long_awake_count", context.long_awake_count) ||
!yyjson_mut_obj_add_uint(document, summary, "nap_count", context.nap_count) ||
!yyjson_mut_obj_add_uint(document, summary, "sleepiness_count", context.sleepiness_count) ||
!yyjson_mut_obj_add_uint(document, summary, "intake_count", context.intake_count) ||
!yyjson_mut_obj_add_sint(
document, summary, "sleep_duration_seconds", context.sleep_seconds) ||
!yyjson_mut_obj_add_sint(
document, summary, "long_awake_duration_seconds", context.long_awake_seconds) ||
!yyjson_mut_obj_add_sint(document, summary, "nap_duration_seconds", context.nap_seconds) ||
!(context.bed_count == 0U
? yyjson_mut_obj_add_null(document, summary, "average_bed_minute")
: yyjson_mut_obj_add_sint(document,
summary,
"average_bed_minute",
context.bed_minute_total / (int64_t)context.bed_count)) ||
!(context.get_up_count == 0U
? yyjson_mut_obj_add_null(document, summary, "average_get_up_minute")
: yyjson_mut_obj_add_sint(document,
summary,
"average_get_up_minute",
context.get_up_minute_total /
(int64_t)context.get_up_count))) {
yyjson_mut_doc_free(document);
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_SYSTEM_ERROR : status;
}
for (category = 0U; category < 3U; ++category) {
yyjson_mut_val *values = yyjson_mut_obj(document);
const char *key = category == 0U ? "sleep_quality"
: category == 1U ? "wake_quality"
: "day_form";
if (values == NULL) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
for (quality = 0U; quality < 5U; ++quality) {
if (!yyjson_mut_obj_add_uint(
document, values, quality_names[quality], context.quality[category][quality])) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
}
if (!yyjson_mut_obj_add_val(document, distribution, key, values)) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
}
if (!yyjson_mut_obj_add_val(document, summary, "quality_distribution", distribution) ||
!yyjson_mut_obj_add_val(document, root, "summary", summary) ||
!yyjson_mut_obj_add_val(document, root, "entries", context.items)) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
return write_document(document, output_json, output_size);
}
static bool
copy_string(yyjson_val *object, const char *key, char *output, size_t capacity, bool nullable) {
yyjson_val *value = yyjson_obj_get(object, key);
size_t length;
if (nullable && yyjson_is_null(value)) {
output[0] = '\0';
return true;
}
if (!yyjson_is_str(value)) {
return false;
}
length = yyjson_get_len(value);
if (length >= capacity) {
return false;
}
memcpy(output, yyjson_get_str(value), length + 1U);
return true;
}
static bool parse_entry(const char *body,
size_t body_size,
TrainlogSleepDiaryEntry *entry,
char expected_revision[TRAINLOG_SLEEP_REVISION_ID_CAPACITY]) {
yyjson_read_err error;
yyjson_doc *document =
yyjson_read_opts((char *)body, body_size, YYJSON_READ_NOFLAG, NULL, &error);
yyjson_val *root = document == NULL ? NULL : yyjson_doc_get_root(document);
yyjson_val *events;
yyjson_val *intakes;
size_t index, count;
bool valid =
root != NULL && yyjson_is_obj(root) &&
copy_string(root, "entry_id", entry->entry_id, sizeof(entry->entry_id), true) &&
copy_string(root,
"expected_revision",
expected_revision,
TRAINLOG_SLEEP_REVISION_ID_CAPACITY,
true) &&
copy_string(root,
"night_start_date",
entry->night_start_date,
sizeof(entry->night_start_date),
false) &&
copy_string(
root, "night_end_date", entry->night_end_date, sizeof(entry->night_end_date), false) &&
copy_string(root, "created_at", entry->created_at, sizeof(entry->created_at), false) &&
copy_string(root, "updated_at", entry->updated_at, sizeof(entry->updated_at), false) &&
copy_string(root,
"treatment_and_notes",
entry->treatment_and_notes,
sizeof(entry->treatment_and_notes),
false) &&
quality_value(yyjson_obj_get(root, "sleep_quality"), &entry->sleep_quality) &&
quality_value(yyjson_obj_get(root, "wake_quality"), &entry->wake_quality) &&
quality_value(yyjson_obj_get(root, "day_form"), &entry->day_form);
events = valid ? yyjson_obj_get(root, "events") : NULL;
count = yyjson_is_arr(events) ? yyjson_arr_size(events) : TRAINLOG_SLEEP_EVENTS_MAX + 1U;
if (count > TRAINLOG_SLEEP_EVENTS_MAX) {
valid = false;
}
for (index = 0U; valid && index < count; ++index) {
yyjson_val *value = yyjson_arr_get(events, index);
TrainlogSleepEvent *event = &entry->events[index];
valid = yyjson_is_obj(value) &&
copy_string(value, "event_id", event->event_id, sizeof(event->event_id), true) &&
event_value(yyjson_obj_get(value, "type"), &event->type) &&
copy_string(value, "start_at", event->start_at, sizeof(event->start_at), false) &&
copy_string(value, "end_at", event->end_at, sizeof(event->end_at), true);
if (valid && event->event_id[0] == '\0') {
valid = trainlog_id_generate("sle", event->event_id, sizeof(event->event_id)) ==
TRAINLOG_STATUS_OK;
}
}
entry->event_count = count;
intakes = valid ? yyjson_obj_get(root, "intakes") : NULL;
count = yyjson_is_arr(intakes) ? yyjson_arr_size(intakes) : TRAINLOG_SLEEP_INTAKES_MAX + 1U;
if (count > TRAINLOG_SLEEP_INTAKES_MAX) {
valid = false;
}
for (index = 0U; valid && index < count; ++index) {
yyjson_val *value = yyjson_arr_get(intakes, index);
yyjson_val *dose_value = yyjson_obj_get(value, "dose_value");
yyjson_val *dose_unit = yyjson_obj_get(value, "dose_unit");
TrainlogMedicationIntake *intake = &entry->intakes[index];
valid =
yyjson_is_obj(value) &&
copy_string(value, "intake_id", intake->intake_id, sizeof(intake->intake_id), true) &&
copy_string(value,
"medication_id",
intake->medication_id,
sizeof(intake->medication_id),
false) &&
copy_string(value,
"medication_name",
intake->medication_name,
sizeof(intake->medication_name),
false) &&
copy_string(value, "taken_at", intake->taken_at, sizeof(intake->taken_at), false) &&
copy_string(value, "note", intake->note, sizeof(intake->note), false) &&
copy_string(
value, "created_at", intake->created_at, sizeof(intake->created_at), false) &&
((yyjson_is_null(dose_value) && yyjson_is_null(dose_unit)) ||
(yyjson_is_num(dose_value) && yyjson_get_num(dose_value) > 0.0 &&
copy_string(
value, "dose_unit", intake->dose_unit, sizeof(intake->dose_unit), false)));
intake->has_dose = yyjson_is_num(dose_value);
intake->dose_value = intake->has_dose ? yyjson_get_num(dose_value) : 0.0;
if (valid && intake->intake_id[0] == '\0') {
valid = trainlog_id_generate("mdi", intake->intake_id, sizeof(intake->intake_id)) ==
TRAINLOG_STATUS_OK;
}
}
entry->intake_count = count;
yyjson_doc_free(document);
return valid;
}
TrainlogStatus trainlog_web_sleep_save_json(TrainlogDatabase *database,
const char *body,
size_t body_size,
char **output_json,
size_t *output_size) {
TrainlogSleepDiaryEntry entry = {0};
char expected[TRAINLOG_SLEEP_REVISION_ID_CAPACITY] = "";
TrainlogStatus status;
yyjson_mut_doc *document;
yyjson_mut_val *root;
if (database == NULL || body == NULL || body_size == 0U ||
body_size > TRAINLOG_WEB_SLEEP_BYTES_MAX || output_json == NULL || output_size == NULL ||
!parse_entry(body, body_size, &entry, expected)) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
if (expected[0] == '\0') {
status = trainlog_sleep_diary_create(database, &entry);
} else {
(void)snprintf(entry.revision_id, sizeof(entry.revision_id), "%s", expected);
status = trainlog_sleep_diary_update(database, expected, &entry);
}
if (status != TRAINLOG_STATUS_OK) {
return status;
}
document = yyjson_mut_doc_new(NULL);
root = document == NULL ? NULL : yyjson_mut_obj(document);
if (document == NULL || root == NULL ||
!yyjson_mut_obj_add_strcpy(document, root, "entry_id", entry.entry_id) ||
!yyjson_mut_obj_add_strcpy(document, root, "revision_id", entry.revision_id)) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
yyjson_mut_doc_set_root(document, root);
return write_document(document, output_json, output_size);
}
TrainlogStatus trainlog_web_sleep_delete_json(TrainlogDatabase *database,
const char *entry_id,
const char *expected_revision,
const char *deleted_at,
char **output_json,
size_t *output_size) {
char revision[TRAINLOG_SLEEP_REVISION_ID_CAPACITY];
TrainlogStatus status =
trainlog_sleep_diary_delete(database, entry_id, expected_revision, deleted_at, revision);
yyjson_mut_doc *document;
yyjson_mut_val *root;
if (status != TRAINLOG_STATUS_OK) {
return status;
}
document = yyjson_mut_doc_new(NULL);
root = document == NULL ? NULL : yyjson_mut_obj(document);
if (document == NULL || root == NULL ||
!yyjson_mut_obj_add_strcpy(document, root, "entry_id", entry_id) ||
!yyjson_mut_obj_add_strcpy(document, root, "revision_id", revision) ||
!yyjson_mut_obj_add_bool(document, root, "deleted", true)) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
yyjson_mut_doc_set_root(document, root);
return write_document(document, output_json, output_size);
}
TrainlogStatus trainlog_web_sleep_delete_request_json(TrainlogDatabase *database,
const char *body,
size_t body_size,
char **output_json,
size_t *output_size) {
yyjson_read_err error;
yyjson_doc *document;
yyjson_val *root;
const char *entry_id;
const char *revision;
const char *deleted_at;
TrainlogStatus status;
if (body == NULL || body_size == 0U || body_size > TRAINLOG_WEB_SLEEP_BYTES_MAX) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
document = yyjson_read_opts((char *)body, body_size, YYJSON_READ_NOFLAG, NULL, &error);
root = document == NULL ? NULL : yyjson_doc_get_root(document);
entry_id = yyjson_is_obj(root) && yyjson_is_str(yyjson_obj_get(root, "entry_id"))
? yyjson_get_str(yyjson_obj_get(root, "entry_id"))
: NULL;
revision = yyjson_is_obj(root) && yyjson_is_str(yyjson_obj_get(root, "expected_revision"))
? yyjson_get_str(yyjson_obj_get(root, "expected_revision"))
: NULL;
deleted_at = yyjson_is_obj(root) && yyjson_is_str(yyjson_obj_get(root, "deleted_at"))
? yyjson_get_str(yyjson_obj_get(root, "deleted_at"))
: NULL;
if (entry_id == NULL || revision == NULL || deleted_at == NULL || yyjson_obj_size(root) != 3U) {
yyjson_doc_free(document);
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
status = trainlog_web_sleep_delete_json(
database, entry_id, revision, deleted_at, output_json, output_size);
yyjson_doc_free(document);
return status;
}
TrainlogStatus trainlog_web_sleep_validate_json(TrainlogDatabase *database,
const char *body,
size_t body_size,
char **output_json,
size_t *output_size) {
yyjson_read_err error;
yyjson_doc *input;
yyjson_val *root;
const char *entry_id;
const char *revision;
const char *validated_at;
char entry_id_copy[TRAINLOG_SLEEP_ENTRY_ID_CAPACITY];
char revision_copy[TRAINLOG_SLEEP_REVISION_ID_CAPACITY];
TrainlogStatus status;
yyjson_mut_doc *output;
yyjson_mut_val *object;
if (database == NULL || body == NULL || body_size == 0U ||
body_size > TRAINLOG_WEB_SLEEP_BYTES_MAX || output_json == NULL || output_size == NULL) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
input = yyjson_read_opts((char *)body, body_size, YYJSON_READ_NOFLAG, NULL, &error);
root = input == NULL ? NULL : yyjson_doc_get_root(input);
entry_id = yyjson_is_obj(root) && yyjson_is_str(yyjson_obj_get(root, "entry_id"))
? yyjson_get_str(yyjson_obj_get(root, "entry_id"))
: NULL;
revision = yyjson_is_obj(root) && yyjson_is_str(yyjson_obj_get(root, "expected_revision"))
? yyjson_get_str(yyjson_obj_get(root, "expected_revision"))
: NULL;
validated_at = yyjson_is_obj(root) && yyjson_is_str(yyjson_obj_get(root, "validated_at"))
? yyjson_get_str(yyjson_obj_get(root, "validated_at"))
: NULL;
if (entry_id == NULL || revision == NULL || validated_at == NULL ||
yyjson_obj_size(root) != 3U) {
yyjson_doc_free(input);
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
status = trainlog_sleep_diary_validate_revision(database, entry_id, revision, validated_at);
(void)snprintf(entry_id_copy, sizeof(entry_id_copy), "%s", entry_id);
(void)snprintf(revision_copy, sizeof(revision_copy), "%s", revision);
yyjson_doc_free(input);
if (status != TRAINLOG_STATUS_OK) {
return status;
}
output = yyjson_mut_doc_new(NULL);
object = output == NULL ? NULL : yyjson_mut_obj(output);
if (output == NULL || object == NULL ||
!yyjson_mut_obj_add_strcpy(output, object, "entry_id", entry_id_copy) ||
!yyjson_mut_obj_add_strcpy(output, object, "revision_id", revision_copy) ||
!yyjson_mut_obj_add_strcpy(output, object, "publication_status", "ready")) {
yyjson_mut_doc_free(output);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
yyjson_mut_doc_set_root(output, object);
return write_document(output, output_json, output_size);
}
typedef struct MedicationListContext {
yyjson_mut_doc *document;
yyjson_mut_val *items;
} MedicationListContext;
static TrainlogStatus medication_list_visitor(void *opaque, const TrainlogMedication *medication) {
MedicationListContext *context = opaque;
yyjson_mut_val *item = yyjson_mut_obj(context->document);
if (item == NULL ||
!yyjson_mut_obj_add_strcpy(
context->document, item, "medication_id", medication->medication_id) ||
!yyjson_mut_obj_add_strcpy(
context->document, item, "revision_id", medication->revision_id) ||
!yyjson_mut_obj_add_strcpy(context->document, item, "name", medication->name) ||
!(medication->has_default_dose
? yyjson_mut_obj_add_real(
context->document, item, "default_dose_value", medication->default_dose_value)
: yyjson_mut_obj_add_null(context->document, item, "default_dose_value")) ||
!(medication->has_default_dose
? yyjson_mut_obj_add_strcpy(
context->document, item, "default_dose_unit", medication->default_dose_unit)
: yyjson_mut_obj_add_null(context->document, item, "default_dose_unit")) ||
!yyjson_mut_obj_add_strcpy(context->document, item, "form", medication->form) ||
!yyjson_mut_obj_add_strcpy(context->document, item, "note", medication->note) ||
!yyjson_mut_obj_add_bool(context->document, item, "active", medication->active) ||
!yyjson_mut_arr_add_val(context->items, item)) {
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
return TRAINLOG_STATUS_OK;
}
TrainlogStatus trainlog_web_medication_list_json(TrainlogDatabase *database,
char **output_json,
size_t *output_size) {
yyjson_mut_doc *document = yyjson_mut_doc_new(NULL);
yyjson_mut_val *root = document == NULL ? NULL : yyjson_mut_obj(document);
MedicationListContext context = {document, document == NULL ? NULL : yyjson_mut_arr(document)};
TrainlogStatus status;
if (database == NULL || output_json == NULL || output_size == NULL || root == NULL ||
context.items == NULL) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
yyjson_mut_doc_set_root(document, root);
status = trainlog_medication_list(database, true, 1000U, medication_list_visitor, &context);
if (status != TRAINLOG_STATUS_OK ||
!yyjson_mut_obj_add_uint(document, root, "api_version", 1U) ||
!yyjson_mut_obj_add_val(document, root, "medications", context.items)) {
yyjson_mut_doc_free(document);
return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_SYSTEM_ERROR : status;
}
return write_document(document, output_json, output_size);
}
TrainlogStatus trainlog_web_medication_save_json(TrainlogDatabase *database,
const char *body,
size_t body_size,
char **output_json,
size_t *output_size) {
yyjson_read_err error;
yyjson_doc *input;
yyjson_val *root;
yyjson_val *dose;
yyjson_val *active;
TrainlogMedication medication = {0};
char expected[sizeof(medication.revision_id)] = "";
TrainlogStatus status;
yyjson_mut_doc *document;
yyjson_mut_val *result;
if (database == NULL || body == NULL || body_size == 0U ||
body_size > TRAINLOG_WEB_SLEEP_BYTES_MAX) {
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
input = yyjson_read_opts((char *)body, body_size, YYJSON_READ_NOFLAG, NULL, &error);
root = input == NULL ? NULL : yyjson_doc_get_root(input);
dose = yyjson_is_obj(root) ? yyjson_obj_get(root, "default_dose_value") : NULL;
active = yyjson_is_obj(root) ? yyjson_obj_get(root, "active") : NULL;
if (!yyjson_is_obj(root) ||
!copy_string(root,
"medication_id",
medication.medication_id,
sizeof(medication.medication_id),
true) ||
!copy_string(root, "expected_revision", expected, sizeof(expected), true) ||
!copy_string(
root, "created_at", medication.created_at, sizeof(medication.created_at), false) ||
!copy_string(
root, "updated_at", medication.updated_at, sizeof(medication.updated_at), false) ||
!copy_string(root, "name", medication.name, sizeof(medication.name), false) ||
!copy_string(root, "form", medication.form, sizeof(medication.form), false) ||
!copy_string(root, "note", medication.note, sizeof(medication.note), false) ||
!yyjson_is_bool(active) ||
!((yyjson_is_null(dose) && copy_string(root,
"default_dose_unit",
medication.default_dose_unit,
sizeof(medication.default_dose_unit),
true)) ||
(yyjson_is_num(dose) && yyjson_get_num(dose) > 0.0 &&
copy_string(root,
"default_dose_unit",
medication.default_dose_unit,
sizeof(medication.default_dose_unit),
false)))) {
yyjson_doc_free(input);
return TRAINLOG_STATUS_INVALID_ARGUMENT;
}
medication.has_default_dose = yyjson_is_num(dose);
medication.default_dose_value = medication.has_default_dose ? yyjson_get_num(dose) : 0.0;
medication.active = yyjson_get_bool(active);
if (expected[0] == '\0') {
status = trainlog_medication_create(database, &medication);
} else {
(void)snprintf(medication.revision_id, sizeof(medication.revision_id), "%s", expected);
status = trainlog_medication_update(database, expected, &medication);
}
yyjson_doc_free(input);
if (status != TRAINLOG_STATUS_OK) {
return status;
}
document = yyjson_mut_doc_new(NULL);
result = document == NULL ? NULL : yyjson_mut_obj(document);
if (result == NULL ||
!yyjson_mut_obj_add_strcpy(document, result, "medication_id", medication.medication_id) ||
!yyjson_mut_obj_add_strcpy(document, result, "revision_id", medication.revision_id)) {
yyjson_mut_doc_free(document);
return TRAINLOG_STATUS_SYSTEM_ERROR;
}
yyjson_mut_doc_set_root(document, result);
return write_document(document, output_json, output_size);
}