/****************************************************************************** * * Copyright (c) 2017-2019 by Löwenware Ltd * Please, refer LICENSE file for legal information * ******************************************************************************/ /** * @file todo.c * @author Ilja Kartašov * @brief AISL ToDo.txt module code file * * @see https://lowenware.com/ */ #include #include #include #include #include #include #include #include #include typedef enum { AX_MINION_UNDEFINED , AX_MINION_TODAY , AX_MINION_CREATE_TASK , AX_MINION_READ_TASK , AX_MINION_UPDATE_TASK , AX_MINION_DELETE_TASK } AxMinionMode; struct context { struct ax_context root; struct ax_query qs; struct ax_todo_task task; AxMinionMode mode; }; typedef struct context * Context; typedef int (*AxMinionCompare)(void *object1, void *object2); static const char modName[] = "minion"; static Context context_new(const char *path, AislHttpMethod method, AxMinion mod) { Context ctx; if (!(ctx = (Context)ax_context_new(AX_MODULE(mod)))) return ctx; if (strcmp(path, "today/") == 0) ctx->mode = AX_MINION_TODAY; else if (strcmp(path, "create/") == 0) ctx->mode = AX_MINION_CREATE_TASK; else if (strcmp(path, "read/") == 0) ctx->mode = AX_MINION_READ_TASK; else if (strcmp(path, "update/") == 0) ctx->mode = AX_MINION_UPDATE_TASK; else if (strcmp(path, "delete/") == 0) ctx->mode = AX_MINION_DELETE_TASK; ctx->task.priority = AX_TODO_ZERO_PRIORITY; return ctx; } static void context_free(Context ctx) { ax_query_release(&ctx->qs); ax_todo_task_release(&ctx->task); ax_context_free((AxContext)ctx); } static int ax_minion_compare_tasks(AxTodoTask task1, AxTodoTask task2) { if (task1->is_done) { if (task2->is_done) { if (task1->completed > task2->completed) { return -1; } else if (task1->completed > task2->completed) { return 1; } } else { return 1; } } else { if (task2->is_done) { return -1; } else { if (task1->priority < task2->priority) { return -1; } else if (task1->priority == task2->priority) { if (task1->created > task2->created) { return -1; } else if (task1->created < task2->created) { return 1; } } else { return 1; } } } return 0; } static int ax_minion_compare_projects(AxTodoProject proj1, AxTodoProject proj2) { if (proj1->last_completed < proj2->last_completed) { return -1; } else if (proj1->last_completed > proj2->last_completed) { return 1; } if (proj1->ref_count > proj2->ref_count) { return -1; } else if (proj1->ref_count < proj2->ref_count) { return 1; } return 0; } static int ax_minion_compare_tags(AxTodoTag tag1, AxTodoTag tag2) { if (tag1->ref_count > tag2->ref_count) { return -1; } else if (tag1->ref_count < tag2->ref_count) { return 1; } return 0; } static void ax_minion_sort(CStuffList list, AxMinionCompare compare_objects) { int l = list->length, ld = l - 1, i, j; void **data = list->items, *swap; for (i = 0; i < l; i++) { int c = i + 1; for (j = ld; j >= c; j--) { if (compare_objects((AxTodoTask)data[j], (AxTodoTask)data[j - 1]) == -1) { swap = data[j]; data[j] = data[j - 1]; data[j - 1] = swap; } } } } static void on_input_dt(const char *val, uint32_t v_len, time_t *p_out) { char str_dt[36 + 1]; if (!(*p_out) && !(v_len < 10 || v_len > sizeof (str_dt) - 1)) { strncpy(str_dt, val, v_len); str_dt[v_len] = 0; (void) ax_todo__get_dt(str_dt, p_out); } } static void on_input_uuid(const char *val, uint32_t v_len, uuid_t uuid_out) { char str_uuid[36 + 1]; if (v_len == 36) { strncpy(str_uuid, val, v_len); str_uuid[v_len] = 0; uuid_parse(str_uuid, uuid_out); } } static int on_input_var(const char *key, uint32_t k_len, const char *val, uint32_t v_len, void *p_ctx) { Context ctx = (Context) p_ctx; /* AxMinion mod = (AxMinion)((AxContext)ctx)->mod; */ char **out = NULL; if (k_len == 1) { if (*key == 't') { out = &ctx->task.description; } else if (*key == 'x') { ctx->task.is_done = (v_len == 4 && strncmp(val, "true", 4) == 0); } else if (*key == 'p') { if (v_len == 1 && !((*val < 0x41 || *val > 0x5A))) { ctx->task.priority = *val; } } } else if (k_len == 2) { if (strncmp(key, "id", 2) == 0) { on_input_uuid(val, v_len, ctx->task.task_id); } else if (strncmp(key, "li", 2) == 0) { on_input_uuid(val, v_len, ctx->task.list_id); } else if (strncmp(key, "co", 2) == 0) { on_input_dt(val, v_len, &ctx->task.completed); } else if (strncmp(key, "cr", 2) == 0) { on_input_dt(val, v_len, &ctx->task.created); } } if (out && !(*out)) { if (cstuff_strncpy(out, val, v_len) == -1) return -1; ax_query__decode(*out); ax_todo__remove_line_break(*out); } return 0; } static AislStatus on_stream_open(AxMinion mod, const struct aisl_evt_open *evt) { Context ctx; AislStream s = (AislStream)evt->evt.source; if (!(ctx = context_new(&evt->path[AX_MODULE(mod)->ep_length], evt->http_method, mod))) { return AISL_MALLOC_ERROR; } aisl_set_context(s, ctx); return AISL_SUCCESS; } static AislStatus on_stream_header(AxMinion mod, const struct aisl_evt_header *evt) { AislStream s = (AislStream)evt->evt.source; Context ctx = aisl_get_context(s); AX_LOG_DEBUG("header: %s = %s", evt->key, evt->value); if (strcmp(evt->key, "content-length") == 0) { size_t total = strtoll(evt->value, NULL, 10); if(ax_query_init(&ctx->qs, total, on_input_var, (void*)ctx) != 0) { ax_quick_response(s, AISL_HTTP_INTERNAL_SERVER_ERROR); } } return AISL_SUCCESS; } static AislStatus on_stream_input(AxMinion mod, const struct aisl_evt_input *evt) { Context ctx = aisl_get_context((AislStream)evt->evt.source); AX_LOG_DEBUG("input[%d]: %s", evt->size, evt->data); ax_query_feed(&ctx->qs, evt->data, evt->size); return AISL_SUCCESS; } static void ax_minion_date2str(time_t tt, char *str_dt, size_t sz) { struct tm *p_tm = gmtime(&tt); strftime(str_dt, sz, "%Y-%m-%d", p_tm); } static bool ax_minion_today(AxMinion mod, AislStream s, Context ctx) { /* sprint: [{task1}, {task2}] */ return true; } static bool ax_minion_create_task(AxMinion mod, AislStream s, Context ctx) { AxTodoTask task; char str_uuid[36 + 1]; if (!(ctx->task.description)) { AX_LOG_STATE("%s: !description", modName); return false; } if (uuid_compare(ctx->task.list_id, mod->todo.list_id) != 0) { AX_LOG_STATE("%s: !list_id", modName); return false; } if (!(task = ax_todo_task_new(ctx->task.list_id))) goto e_system; task->is_done = ctx->task.is_done; task->priority = ctx->task.priority; if (!(task->created = ctx->task.created)) time(&task->created); task->completed = ctx->task.completed; task->description = ctx->task.description; uuid_copy(task->list_id, ctx->task.list_id); ctx->task.description = NULL; if (cstuff_list_append(&mod->tasks, task) == -1) { ax_todo_task_free(task); goto e_system; } if (ax_todo_write(&mod->todo, &mod->tasks) != AISL_SUCCESS) goto e_system; if (aisl_header(s, "Content-Type", "text/json") == -1) goto e_reject; uuid_unparse_lower(task->list_id, str_uuid); if (aisl_printf(s, "{\"id\": \"%s\"}", str_uuid) == -1) goto e_reject; if (aisl_flush(s) != AISL_SUCCESS) goto e_reject; goto finally; e_reject: aisl_reject(s); goto finally; e_system: ax_quick_response(s, AISL_HTTP_INTERNAL_SERVER_ERROR); goto finally; finally: return true; } static void ax_minion_read_task(AxMinion mod, AislStream s, Context ctx) { int i; char str_buf[36 + 1]; CStuffList lst; if (aisl_response(s, AISL_HTTP_OK, AISL_AUTO_LENGTH) != AISL_SUCCESS) goto e_reject; if (aisl_header(s, "Content-Type", "text/json") == -1) goto e_reject; /* tasks */ if (aisl_write(s, "{\"tasks\":[", 10) == -1) goto e_reject; lst = &mod->tasks; for (i = 0; i < lst->length; i++) { AxTodoTask task = lst->items[i]; int rc; if (i != 0 && aisl_write(s, ",", 1) == -1) goto e_reject; uuid_unparse_lower(task->task_id, str_buf); if (aisl_printf(s, "\n{\"id\":\"%s\",", str_buf) == -1) goto e_reject; uuid_unparse_lower(task->list_id, str_buf); if (aisl_printf(s, "\"li\":\"%s\",", str_buf) == -1) goto e_reject; if (aisl_printf(s, "\"x\":%s,", task->is_done ? "true" : "false") == -1) goto e_reject; rc = aisl_printf(s, "\"p\":\"%c\",", task->priority ? task->priority : '0'); if (rc == -1) goto e_reject; if (task->is_done && task->completed) { ax_minion_date2str(task->completed, str_buf, sizeof(str_buf)); if (aisl_printf(s, "\"co\":\"%s\",", str_buf) == -1) goto e_reject; } if (task->created) { ax_minion_date2str(task->created, str_buf, sizeof(str_buf)); if (aisl_printf(s, "\"cr\":\"%s\",", str_buf) == -1) goto e_reject; } rc = aisl_printf(s, "\"t\":\"%s\"}", task->description); if (rc == -1) goto e_reject; } /* projects */ if (aisl_write(s, "\n], \"projects\":[", 16) == -1) goto e_reject; lst = &mod->projects; for (i = 0; i < lst->length; i++) { AxTodoProject proj = lst->items[i]; int rc; if (i != 0 && aisl_write(s, ",", 1) == -1) goto e_reject; uuid_unparse_lower(proj->project_id, str_buf); if (aisl_printf(s, "\n{\"id\":\"%s\",", str_buf) == -1) goto e_reject; if (aisl_printf(s, "\"n\":\"%s\",", proj->name) == -1) goto e_reject; rc = aisl_printf(s, "\"r\":%"PRIu32"", proj->ref_count); if (rc == -1) goto e_reject; if (proj->last_completed) { ax_minion_date2str(proj->last_completed, str_buf, sizeof(str_buf)); if (aisl_printf(s, ",\"lc\":\"%s\"", str_buf) == -1) goto e_reject; } if (aisl_write(s, "}", 1) == -1) goto e_reject; } /* tags */ if (aisl_write(s, "\n], \"tags\":[", 12) == -1) goto e_reject; lst = &mod->tags; for (i = 0; i < lst->length; i++) { AxTodoTag tag = lst->items[i]; int rc; if (i != 0 && aisl_write(s, ",", 1) == -1) goto e_reject; uuid_unparse_lower(tag->tag_id, str_buf); if (aisl_printf(s, "\n{\"id\":\"%s\",", str_buf) == -1) goto e_reject; if (aisl_printf(s, "\"n\":\"%s\",", tag->name) == -1) goto e_reject; rc = aisl_printf(s, "\"r\":%"PRIu32"}", tag->ref_count); if (rc == -1) goto e_reject; } if (aisl_write(s, "]}", 2) == -1) goto e_reject; if (aisl_flush(s) != AISL_SUCCESS) goto e_reject; return; e_reject: AX_LOG_DEBUG("%s: reject", modName); aisl_reject(s); } static bool ax_minion_update_task(AxMinion mod, AislStream s, Context ctx) { AxTodoTask task = NULL; char str_uuid[36 + 1]; int i; if (!(ctx->task.description)) { AX_LOG_STATE("%s: !description", modName); return false; } if (uuid_compare(ctx->task.list_id, mod->todo.list_id) != 0) { AX_LOG_STATE("%s: !list_id", modName); return false; } for (i = 0; i < mod->tasks.length; i++) { task = (AxTodoTask) mod->tasks.items[i]; if (uuid_compare(task->task_id, ctx->task.task_id) == 0) break; task = NULL; } if (!task) { AX_LOG_STATE("%s: !task_id", modName); return false; } if (!(task->priority = ctx->task.priority)) task->priority = AX_TODO_ZERO_PRIORITY; task->created = ctx->task.created; task->completed = ctx->task.completed; if ((task->is_done = ctx->task.is_done) && !task->completed) { time(&task->completed); } if (task->description) free(task->description); task->description = ctx->task.description; ctx->task.description = NULL; uuid_copy(task->list_id, ctx->task.list_id); if (ax_todo_write(&mod->todo, &mod->tasks) != AISL_SUCCESS) goto e_system; if (aisl_header(s, "Content-Type", "text/json") == -1) goto e_reject; uuid_unparse_lower(task->list_id, str_uuid); if (aisl_printf(s, "{\"id\": \"%s\"}", str_uuid) == -1) goto e_reject; if (aisl_flush(s) != AISL_SUCCESS) goto e_reject; goto finally; e_reject: aisl_reject(s); goto finally; e_system: ax_quick_response(s, AISL_HTTP_INTERNAL_SERVER_ERROR); goto finally; finally: return true; } static bool ax_minion_delete_task(AxMinion mod, AislStream s, Context ctx) { AxTodoTask task = NULL; char str_uuid[36 + 1]; int i; for (i = 0; i < mod->tasks.length; i++) { task = (AxTodoTask) mod->tasks.items[i]; if (uuid_compare(task->task_id, ctx->task.task_id) == 0) { cstuff_list_remove(&mod->tasks, i); break; } task = NULL; } if (!task) { AX_LOG_STATE("%s: !task_id", modName); return false; } uuid_unparse_lower(task->list_id, str_uuid); ax_todo_task_free(task); if (ax_todo_write(&mod->todo, &mod->tasks) != AISL_SUCCESS) goto e_system; if (aisl_header(s, "Content-Type", "text/json") == -1) goto e_reject; if (aisl_printf(s, "{\"id\": \"%s\"}", str_uuid) == -1) goto e_reject; if (aisl_flush(s) != AISL_SUCCESS) goto e_reject; goto finally; e_reject: aisl_reject(s); goto finally; e_system: ax_quick_response(s, AISL_HTTP_INTERNAL_SERVER_ERROR); goto finally; finally: return true; } static AislStatus on_stream_request(AxMinion mod, const struct aisl_evt *evt) { Context ctx = aisl_get_context((AislStream)evt->source); AislStream s = (AislStream)evt->source; /* verify input */ AX_LOG_STATE("%s: request mode = %d", modName, ctx->mode); switch(ctx->mode) { case AX_MINION_TODAY: if (!ax_minion_today(mod, s, ctx)) goto bad_request; break; case AX_MINION_CREATE_TASK: if (!ax_minion_create_task(mod, s, ctx)) goto bad_request; break; case AX_MINION_READ_TASK: ax_minion_read_task(mod, s, ctx); break; case AX_MINION_UPDATE_TASK: if (!ax_minion_update_task(mod, s, ctx)) goto bad_request; break; case AX_MINION_DELETE_TASK: if (!ax_minion_delete_task(mod, s, ctx)) goto bad_request; break; default: goto bad_request; } goto finally; bad_request: ax_quick_response(s, AISL_HTTP_BAD_REQUEST); finally: return AISL_SUCCESS; } static AislStatus on_stream_close(AxMinion mod, const struct aisl_evt *evt) { context_free((Context)aisl_get_context((AislStream)evt->source)); return AISL_SUCCESS; } static AislStatus ax_minion_on_event(AxMinion mod, const struct aisl_evt *evt) { switch(evt->code) { case AISL_EVENT_STREAM_OPEN: return on_stream_open(mod, (const struct aisl_evt_open *)evt); case AISL_EVENT_STREAM_HEADER: return on_stream_header(mod, (const struct aisl_evt_header *)evt); case AISL_EVENT_STREAM_INPUT: return on_stream_input(mod, (const struct aisl_evt_input *)evt); case AISL_EVENT_STREAM_REQUEST: return on_stream_request(mod, evt); case AISL_EVENT_STREAM_CLOSE: return on_stream_close(mod, evt); default: return AISL_IDLE; } } AislStatus ax_minion_init(AxMinion mod, const struct ax_minion_cfg *cfg) { AislStatus result; AX_MODULE_INIT(ax_minion, cfg->end_point); if ((result = ax_todo_init(&mod->todo, cfg->todo_file)) != AISL_SUCCESS) return result; if ((cstuff_list_init(&mod->tasks, 32)) != CSTUFF_SUCCESS) return AISL_MALLOC_ERROR; if ((cstuff_list_init(&mod->projects, 8)) != CSTUFF_SUCCESS) return AISL_MALLOC_ERROR; if ((cstuff_list_init(&mod->tags, 32)) != CSTUFF_SUCCESS) return AISL_MALLOC_ERROR; if ((result = ax_todo_read(&mod->todo, &mod->tasks, &mod->projects, &mod->tags)) != AISL_SUCCESS) return result; ax_minion_sort(&mod->tasks, (AxMinionCompare)ax_minion_compare_tasks); ax_minion_sort(&mod->projects, (AxMinionCompare)ax_minion_compare_projects); ax_minion_sort(&mod->tags, (AxMinionCompare)ax_minion_compare_tags); if (ax_todo_write(&mod->todo, &mod->tasks) != AISL_SUCCESS) { AX_LOG_DEBUG("Write failed ;("); } return result; } void ax_minion_release(AxMinion mod) { ax_todo_release(&mod->todo); cstuff_list_release(&mod->tasks, (CStuffListFree) ax_todo_task_free); cstuff_list_release(&mod->projects, (CStuffListFree) ax_todo_project_free); cstuff_list_release(&mod->tags, (CStuffListFree) ax_todo_tag_free); }