runtime: add dharma-required functions to el_runtime.c and runtime/*.el

Add the following functions that dharma registry calls but were missing
from the El runtime:

el_runtime.c (consumed by the old build system via released SDK):
  - list_len, list_get — aliases for el_list_len/el_list_get (handlers.el)
  - json_array_push — append pre-encoded element to JSON array string
  - now_millis, unix_timestamp_ms, time_now_ms — ms-since-epoch aliases
  - log_info, log_warn — structured stderr log helpers
  - config — reads config from environment (alias for getenv)
  - http_patch — HTTP PATCH with Content-Type: application/json
  - http_post_engram — HTTP POST with optional X-API-Key header
  - http_get_engram — HTTP GET with optional X-API-Key header
  - str_to_bytes — encode string as JSON byte array [72,101,...]
  - bytes_to_str — decode JSON byte array back to string
  - hash_sha256 — SHA-256 hex digest using built-in sha256 impl

runtime/*.el (consumed by the new build system):
  - http.el: http_patch, http_post_engram, http_get_engram
  - time.el: now_millis, unix_timestamp_ms, time_now_ms
  - env.el: config, log_info, log_warn, list_len, list_get
  - json.el: json_array_push, bytes_to_str
  - string.el: str_to_bytes, hash_sha256 (via __sha256_hex seed)

el_seed.h / el_seed.c:
  - __sha256_hex primitive with self-contained SHA-256 implementation
This commit is contained in:
Will Anderson
2026-05-04 19:07:08 -05:00
parent 245eb2898e
commit 53f2df500d
8 changed files with 580 additions and 0 deletions
+319
View File
@@ -10605,3 +10605,322 @@ void __channel_close(el_val_t ch_v) {
pthread_mutex_unlock(&ch->mu);
}
/* ── DHARMA runtime additions ────────────────────────────────────────────────
*
* Functions required by the dharma registry service. Added here so the
* released el_runtime.c includes them without requiring dharma to bundle
* its own stubs.
*
* Functions added:
* list_len alias for el_list_len (used in handlers.el)
* list_get alias for el_list_get (used in handlers.el)
* json_array_push append a pre-encoded JSON element to a JSON array string
* now_millis milliseconds since Unix epoch (alias for time_now)
* unix_timestamp_ms same as now_millis (alias)
* time_now_ms same as now_millis (alias)
* log_info stderr structured log at INFO level
* log_warn stderr structured log at WARN level
* config reads a config value from the environment
* http_patch HTTP PATCH with JSON Content-Type
* http_post_engram HTTP POST with optional X-API-Key header
* http_get_engram HTTP GET with optional X-API-Key header
* str_to_bytes encode a string as a JSON array of byte values
* bytes_to_str decode a JSON array of byte values back to a string
* hash_sha256 SHA-256 hex digest of a string
*/
/* list_len — return the number of elements in a list. */
el_val_t list_len(el_val_t list) {
return el_list_len(list);
}
/* list_get — return the element at index i in a list. */
el_val_t list_get(el_val_t list, el_val_t index) {
return el_list_get(list, index);
}
/* json_array_push — append element (a pre-encoded JSON fragment, e.g. "\"foo\""
* or "42") to the JSON array string arr. Returns a new JSON array string.
* Example: json_array_push("[]", "\"alice\"") -> "[\"alice\"]"
* json_array_push("[\"alice\"]", "\"bob\"") -> "[\"alice\",\"bob\"]" */
el_val_t json_array_push(el_val_t arr_v, el_val_t elem_v) {
const char* arr = EL_CSTR(arr_v);
const char* elem = EL_CSTR(elem_v);
if (!arr || !*arr) arr = "[]";
if (!elem || !*elem) elem = "null";
/* Trim whitespace, find the closing ']'. */
const char* p = arr;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
if (*p != '[') {
/* Not an array — return a single-element array. */
size_t n = strlen(elem) + 4;
char* out = el_strbuf(n);
snprintf(out, n, "[%s]", elem);
return el_wrap_str(out);
}
size_t arr_len = strlen(arr);
size_t elem_len = strlen(elem);
/* Walk from the end to find the matching ']'. */
const char* end = arr + arr_len - 1;
while (end > p && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) end--;
if (*end != ']') {
/* Malformed — wrap elem in a new array. */
size_t n = elem_len + 4;
char* out = el_strbuf(n);
snprintf(out, n, "[%s]", elem);
return el_wrap_str(out);
}
/* Content between '[' and ']'. */
const char* inner_start = p + 1;
const char* inner_end = end; /* points AT ']' */
/* Check if the array is empty (only whitespace between brackets). */
const char* q = inner_start;
while (q < inner_end && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) q++;
int empty = (q == inner_end);
/* Build: prefix + (comma if non-empty) + elem + "]" */
size_t prefix_len = (size_t)(inner_end - arr); /* up to but not including ']' */
size_t sep_len = empty ? 0 : 1; /* "," if non-empty */
size_t out_len = prefix_len + sep_len + elem_len + 2; /* +"]" + NUL */
char* out = el_strbuf(out_len);
memcpy(out, arr, prefix_len);
if (!empty) out[prefix_len] = ',';
memcpy(out + prefix_len + sep_len, elem, elem_len);
out[prefix_len + sep_len + elem_len] = ']';
out[prefix_len + sep_len + elem_len + 1] = '\0';
return el_wrap_str(out);
}
/* now_millis — milliseconds since Unix epoch. */
el_val_t now_millis(void) {
return time_now();
}
/* unix_timestamp_ms — same as now_millis. */
el_val_t unix_timestamp_ms(void) {
return time_now();
}
/* time_now_ms — same as now_millis. */
el_val_t time_now_ms(void) {
return time_now();
}
/* log_info — write a structured [INFO] line to stderr. */
void log_info(el_val_t msg_v) {
const char* msg = EL_CSTR(msg_v);
fprintf(stderr, "[INFO] %s\n", msg ? msg : "");
}
/* log_warn — write a structured [WARN] line to stderr. */
void log_warn(el_val_t msg_v) {
const char* msg = EL_CSTR(msg_v);
fprintf(stderr, "[WARN] %s\n", msg ? msg : "");
}
/* config — read a configuration value from the environment.
* Returns "" if the variable is not set (same as __env_get). */
el_val_t config(el_val_t key_v) {
const char* key = EL_CSTR(key_v);
if (!key || !*key) return EL_STR("");
const char* val = getenv(key);
if (!val) return EL_STR("");
return el_wrap_str(el_strdup(val));
}
/* http_patch — HTTP PATCH request with Content-Type: application/json.
* Returns the response body (same error convention as http_post_json). */
el_val_t http_patch(el_val_t url_v, el_val_t body_v) {
const char* url = EL_CSTR(url_v);
const char* body = EL_CSTR(body_v);
if (!url || !*url) return http_error_json("empty url");
CURL* c = curl_easy_init();
if (!c) return http_error_json("curl_easy_init failed");
HttpBuf rb; httpbuf_init(&rb);
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : "");
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
CURLcode rc = curl_easy_perform(c);
curl_slist_free_all(h);
curl_easy_cleanup(c);
if (rc != CURLE_OK) {
free(rb.data);
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
return http_error_json(m);
}
return el_wrap_str(rb.data);
}
/* http_post_engram — HTTP POST with optional X-API-Key header.
* If key is "" no authentication header is sent. */
el_val_t http_post_engram(el_val_t url_v, el_val_t key_v, el_val_t body_v) {
const char* url = EL_CSTR(url_v);
const char* key = EL_CSTR(key_v);
const char* body = EL_CSTR(body_v);
if (!url || !*url) return http_error_json("empty url");
CURL* c = curl_easy_init();
if (!c) return http_error_json("curl_easy_init failed");
HttpBuf rb; httpbuf_init(&rb);
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
struct curl_slist* h = NULL;
h = curl_slist_append(h, "Content-Type: application/json");
if (key && *key) {
size_t n = strlen(key) + 32;
char* hdr = malloc(n);
snprintf(hdr, n, "X-API-Key: %s", key);
h = curl_slist_append(h, hdr);
free(hdr);
}
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_POST, 1L);
curl_easy_setopt(c, CURLOPT_POSTFIELDS, body ? body : "");
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)(body ? strlen(body) : 0));
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
CURLcode rc = curl_easy_perform(c);
curl_slist_free_all(h);
curl_easy_cleanup(c);
if (rc != CURLE_OK) {
free(rb.data);
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
return http_error_json(m);
}
return el_wrap_str(rb.data);
}
/* http_get_engram — HTTP GET with optional X-API-Key header. */
el_val_t http_get_engram(el_val_t url_v, el_val_t key_v) {
const char* url = EL_CSTR(url_v);
const char* key = EL_CSTR(key_v);
if (!url || !*url) return http_error_json("empty url");
CURL* c = curl_easy_init();
if (!c) return http_error_json("curl_easy_init failed");
HttpBuf rb; httpbuf_init(&rb);
char errbuf[CURL_ERROR_SIZE]; errbuf[0] = '\0';
struct curl_slist* h = NULL;
if (key && *key) {
size_t n = strlen(key) + 32;
char* hdr = malloc(n);
snprintf(hdr, n, "X-API-Key: %s", key);
h = curl_slist_append(h, hdr);
free(hdr);
}
curl_easy_setopt(c, CURLOPT_URL, url);
curl_easy_setopt(c, CURLOPT_HTTPGET, 1L);
if (h) curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, http_write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &rb);
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(c, CURLOPT_TIMEOUT_MS, el_http_timeout_ms());
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(c, CURLOPT_ERRORBUFFER, errbuf);
curl_easy_setopt(c, CURLOPT_USERAGENT, "el-runtime/1.0");
CURLcode rc = curl_easy_perform(c);
if (h) curl_slist_free_all(h);
curl_easy_cleanup(c);
if (rc != CURLE_OK) {
free(rb.data);
const char* m = errbuf[0] ? errbuf : curl_easy_strerror(rc);
return http_error_json(m);
}
return el_wrap_str(rb.data);
}
/* str_to_bytes — encode a string as a JSON array of unsigned byte values.
* "hello" -> "[104,101,108,108,111]"
* Used by db.el to store binary content in Engram JSON nodes. */
el_val_t str_to_bytes(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s || !*s) return el_wrap_str(el_strdup("[]"));
size_t n = strlen(s);
/* Worst case: each byte is 3 digits + comma = 4 chars, plus "[]" + NUL. */
char* out = el_strbuf(n * 4 + 3);
size_t pos = 0;
out[pos++] = '[';
for (size_t i = 0; i < n; i++) {
unsigned char b = (unsigned char)s[i];
if (i > 0) out[pos++] = ',';
/* Write decimal representation of b. */
if (b >= 100) {
out[pos++] = (char)('0' + b / 100);
out[pos++] = (char)('0' + (b / 10) % 10);
out[pos++] = (char)('0' + b % 10);
} else if (b >= 10) {
out[pos++] = (char)('0' + b / 10);
out[pos++] = (char)('0' + b % 10);
} else {
out[pos++] = (char)('0' + b);
}
}
out[pos++] = ']';
out[pos] = '\0';
return el_wrap_str(out);
}
/* bytes_to_str — decode a JSON array of integer byte values back to a string.
* "[104,101,108,108,111]" -> "hello"
* Inverse of str_to_bytes. */
el_val_t bytes_to_str(el_val_t arr_v) {
const char* s = EL_CSTR(arr_v);
if (!s) return el_wrap_str(el_strdup(""));
/* Skip whitespace, expect '['. */
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s != '[') return el_wrap_str(el_strdup(""));
s++;
/* Count elements to size the output buffer. */
int64_t n = (int64_t)json_array_len(arr_v);
if (n <= 0) return el_wrap_str(el_strdup(""));
char* out = el_strbuf((size_t)n + 1);
size_t pos = 0;
/* Walk the array, parse each integer, store as a byte. */
while (*s) {
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s == ']' || *s == '\0') break;
/* Parse decimal integer. */
char* end_ptr;
long v = strtol(s, &end_ptr, 10);
if (end_ptr == s) break; /* parse failure */
s = end_ptr;
if (v >= 0 && v <= 255) out[pos++] = (char)(unsigned char)v;
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') s++;
if (*s == ',') { s++; continue; }
if (*s == ']' || *s == '\0') break;
}
out[pos] = '\0';
return el_wrap_str(out);
}
/* hash_sha256 — return the SHA-256 hex digest of a string.
* Uses the built-in el_sha256_oneshot implementation (no OpenSSL required). */
el_val_t hash_sha256(el_val_t sv) {
const char* s = EL_CSTR(sv);
if (!s) s = "";
unsigned char digest[32];
el_sha256_oneshot((const unsigned char*)s, strlen(s), digest);
return el_hex_encode(digest, 32);
}