#include "reading_io.h" #include "json.h" #include /* Mirrors astro.c's own k_body_slug/k_aspect_slug (its i18n lookup-key * tables), duplicated here rather than linking astro.c into this * otherwise header-only, independently testable module - same policy * and same reasoning as significance.c's max_orb_for_pair(). Keep in * sync if those slugs ever change. */ static const char *const k_body_slug[NUM_BODIES] = { "sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", }; static const char *const k_aspect_slug[5] = { "conjunction", "sextile", "square", "trine", "opposition", }; static const char *const k_sign_slug[12] = { "aries", "taurus", "gemini", "cancer", "leo", "virgo", "libra", "scorpio", "sagittarius", "capricorn", "aquarius", "pisces", }; /* Mirrors tarot_data.c's own k_card_slug/k_position_slug, same * duplication policy as k_body_slug above. */ static const char *const k_card_slug[TAROT_DECK_SIZE] = { "fool", "magician", "high_priestess", "empress", "emperor", "hierophant", "lovers", "chariot", "strength", "hermit", "wheel_of_fortune", "justice", "hanged_man", "death", "temperance", "devil", "tower", "star", "moon", "sun", "judgement", "world", }; static const char *const k_position_slug[TAROT_SPREAD_SIZE] = { "present", "challenge", "crown", "foundation", "recent_past", "near_future", "attitude", "environment", "hopes_and_fears", "outcome", }; static bool body_from_slug(const char *slug, Body *out) { for (int i = 0; i < NUM_BODIES; i++) { if (strcmp(slug, k_body_slug[i]) == 0) { *out = (Body)i; return true; } } return false; } static bool sign_from_slug(const char *slug, ZodiacSign *out) { for (int i = 0; i < 12; i++) { if (strcmp(slug, k_sign_slug[i]) == 0) { *out = (ZodiacSign)i; return true; } } return false; } /* Fills out[NUM_BODIES] from a "bodies" JSON array, the shape shared by * both "natal" and "transits" (see reading_print_json in engine/src/ * reading.c). Used only for sign/house context in significant-event * reporting, not for scoring, so this is deliberately lenient: an entry * with an unrecognized/missing "body" slug is skipped, and a missing * "house" leaves that body's house at 0 - not a valid whole-sign house * number (always 1-12), so callers use house == 0 as "no position data * available" rather than this failing the whole load. */ static void parse_bodies(const JsonValue *bodies, PlanetPosition out[NUM_BODIES]) { int count = json_array_count(bodies); for (int i = 0; i < count; i++) { const JsonValue *item = json_array_get(bodies, i); Body body; if (!body_from_slug(json_as_string(json_object_get(item, "body")), &body)) continue; ZodiacSign sign; if (sign_from_slug(json_as_string(json_object_get(item, "sign")), &sign)) { out[body].sign = sign; } out[body].degree_in_sign = json_as_number(json_object_get(item, "degree_in_sign")); out[body].ecliptic_longitude = json_as_number(json_object_get(item, "ecliptic_longitude")); out[body].house = (int)json_as_number(json_object_get(item, "house")); } } static bool aspect_type_from_slug(const char *slug, AspectType *out) { for (int i = 0; i < 5; i++) { if (strcmp(slug, k_aspect_slug[i]) == 0) { *out = (AspectType)i; return true; } } return false; } static bool card_from_slug(const char *slug, TarotCard *out) { for (int i = 0; i < TAROT_DECK_SIZE; i++) { if (strcmp(slug, k_card_slug[i]) == 0) { *out = (TarotCard)i; return true; } } return false; } static bool position_from_slug(const char *slug, CelticCrossPosition *out) { for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { if (strcmp(slug, k_position_slug[i]) == 0) { *out = (CelticCrossPosition)i; return true; } } return false; } /* Fills out->positions[] from a "spread"."positions" JSON array. Used only * for guidance_print()'s Attitude/Outcome framing, never for scoring, so * this is deliberately lenient like parse_bodies(): an entry with an * unrecognized/missing "position" or "card" slug is skipped, and a * missing "spread" key entirely just leaves every position zeroed * (card = the first enum value, reversed = false) rather than failing * the whole load. */ static void parse_spread(const JsonValue *positions, CelticCrossSpread *out) { int count = json_array_count(positions); for (int i = 0; i < count; i++) { const JsonValue *item = json_array_get(positions, i); CelticCrossPosition position; TarotCard card; if (!position_from_slug(json_as_string(json_object_get(item, "position")), &position)) continue; if (!card_from_slug(json_as_string(json_object_get(item, "card")), &card)) continue; out->positions[position].card = card; const JsonValue *reversed = json_object_get(item, "reversed"); out->positions[position].reversed = reversed && reversed->type == JSON_BOOL && reversed->as.boolean; } } bool reading_load_json(const char *text, size_t length, DailyReading *out) { memset(out, 0, sizeof(*out)); JsonValue *root = json_parse(text, length); if (!root) return false; const JsonValue *transits = json_object_get(root, "transits"); const JsonValue *aspects = transits ? json_object_get(transits, "aspects") : NULL; if (!aspects) { json_free(root); return false; } bool ok = true; int filled = 0; int count = json_array_count(aspects); for (int i = 0; i < count && filled < MAX_ASPECTS; i++) { const JsonValue *item = json_array_get(aspects, i); const char *transiting_slug = json_as_string(json_object_get(item, "transiting_planet")); const char *natal_slug = json_as_string(json_object_get(item, "natal_planet")); const char *type_slug = json_as_string(json_object_get(item, "type")); const JsonValue *orb_value = json_object_get(item, "orb"); Body transiting, natal; AspectType type; if (!body_from_slug(transiting_slug, &transiting) || !body_from_slug(natal_slug, &natal) || !aspect_type_from_slug(type_slug, &type) || !orb_value) { ok = false; break; } out->transits.aspects[filled].transiting_planet = transiting; out->transits.aspects[filled].natal_planet = natal; out->transits.aspects[filled].type = type; out->transits.aspects[filled].orb = json_as_number(orb_value); filled++; } out->transits.aspect_count = filled; const JsonValue *natal = json_object_get(root, "natal"); parse_bodies(natal ? json_object_get(natal, "bodies") : NULL, out->natal.bodies); parse_bodies(json_object_get(transits, "bodies"), out->transits.bodies); const JsonValue *spread = json_object_get(root, "spread"); parse_spread(spread ? json_object_get(spread, "positions") : NULL, &out->spread); json_free(root); return ok; }