// el-services — Pluggable service bindings. // // A "service" is a logical interface. A "binding" is the wire protocol that // fulfills it. The binding kind is set in manifest.el; the service code does // not change when the binding changes. // // binding = "rest" -> RestBinding // binding = "grpc" -> GrpcBinding (planned) // binding = "websocket" -> WebSocketBinding // binding = "direct" -> in-process function call (for testing) // ── Binding kinds ──────────────────────────────────────────────────────────── let BIND_REST: String = "rest" let BIND_GRPC: String = "grpc" let BIND_WEBSOCKET: String = "websocket" let BIND_DIRECT: String = "direct" // ── Errors ─────────────────────────────────────────────────────────────────── let SVC_ERR_NOT_FOUND: String = "service.not_found" let SVC_ERR_BINDING: String = "service.binding" let SVC_ERR_TRANSPORT: String = "service.transport" let SVC_ERR_AUTH: String = "service.auth" let SVC_ERR_METHOD_NOT_FOUND: String = "service.method_not_found" // ── Service config ─────────────────────────────────────────────────────────── let AUTH_NONE: String = "none" let AUTH_BEARER: String = "bearer" let AUTH_BASIC: String = "basic" let AUTH_HMAC: String = "hmac" type AuthConfig { kind: String // none | bearer | basic | hmac secret_ref: String // e.g. "env:API_TOKEN" or "vault:path" } type ServiceConfig { name: String binding: String base_url: String auth: AuthConfig timeout_ms: Int } fn service_config_rest(name: String, base_url: String, auth_secret_ref: String) -> ServiceConfig { let auth: AuthConfig = { "kind": "bearer", "secret_ref": auth_secret_ref } { "name": name, "binding": "rest", "base_url": base_url, "auth": auth, "timeout_ms": 5000 } } // ── Service request/response ──────────────────────────────────────────────── type ServiceRequest { method: String // logical method name (e.g. "list_users") args: String // JSON metadata: String // JSON (auth tokens, trace ids) } type ServiceResponse { status: Int // 0 = ok, non-zero = error code body: String // JSON error: String // empty if ok } fn response_ok(body: String) -> ServiceResponse { { "status": 0, "body": body, "error": "" } } fn response_err(code: Int, error: String) -> ServiceResponse { { "status": code, "body": "", "error": error } } // ── REST binding ──────────────────────────────────────────────────────────── // // Method name -> (HTTP verb, path template). The compiler emits this table // from the service interface declaration. fn rest_invoke(cfg: ServiceConfig, http_method: String, path: String, req: ServiceRequest) -> ServiceResponse { let url: String = cfg.base_url + path let headers: String = build_auth_header(cfg.auth) let body: String = req.args let resp: String = "" if str_eq(http_method, "GET") { let resp = http_get_with_headers(url, headers) } if str_eq(http_method, "POST") { let resp = http_post_with_headers(url, body, headers) } if str_eq(http_method, "PUT") { let resp = http_put_with_headers(url, body, headers) } if str_eq(http_method, "DELETE") { let resp = http_delete_with_headers(url, headers) } if str_eq(resp, "") { return response_err(599, SVC_ERR_TRANSPORT) } response_ok(resp) } fn build_auth_header(auth: AuthConfig) -> String { if str_eq(auth.kind, "bearer") { let secret: String = resolve_secret_ref(auth.secret_ref) return "Authorization: Bearer " + secret } if str_eq(auth.kind, "basic") { let creds: String = resolve_secret_ref(auth.secret_ref) return "Authorization: Basic " + base64_encode(creds) } "" } fn resolve_secret_ref(ref: String) -> String { if str_starts_with(ref, "env:") { return env(str_slice(ref, 4, str_len(ref))) } if str_starts_with(ref, "vault:") { return vault_kv_get(str_slice(ref, 6, str_len(ref))) } "" } // ── gRPC binding (planned) ────────────────────────────────────────────────── // // Requires generated stubs from a .proto. The shape is identical to REST: // a method dispatcher that converts logical method calls to wire calls. fn grpc_invoke(cfg: ServiceConfig, method: String, req: ServiceRequest) -> ServiceResponse { response_err(501, "grpc binding not yet implemented") } // ── WebSocket binding ─────────────────────────────────────────────────────── fn ws_invoke(cfg: ServiceConfig, method: String, req: ServiceRequest) -> ServiceResponse { let frame: String = "{\"method\":\"" + method + "\",\"args\":" + req.args + "}" let reply: String = ws_send_recv(cfg.base_url, frame) if str_eq(reply, "") { return response_err(599, SVC_ERR_TRANSPORT) } response_ok(reply) } // ── Direct binding (in-process — used in tests / single-process apps) ────── fn direct_invoke(handler_fn_name: String, req: ServiceRequest) -> ServiceResponse { let body: String = call_dynamic2(handler_fn_name, req.method, req.args) response_ok(body) } // ── ServiceRegistry ───────────────────────────────────────────────────────── // // Maps service name -> ServiceConfig. Methods are looked up in a separate // per-service method table (logical method -> (verb, path) for REST, etc). fn registry_new() -> String { "{}" } fn registry_register(reg: String, cfg: ServiceConfig) -> String { json_set(reg, cfg.name, json_encode(cfg)) } fn registry_lookup(reg: String, name: String) -> String { json_get(reg, name) } // ── ServiceProxy — the user-facing call site ──────────────────────────────── // // A ServiceProxy is a (registry, service_name, method_table) trio. The // el-ui-compiler emits one proxy class per service interface. type ServiceProxy { service_name: String config: ServiceConfig method_table: String // JSON: method -> { http_verb, path } } fn proxy_invoke(proxy: ServiceProxy, method: String, args_json: String) -> ServiceResponse { let req: ServiceRequest = { "method": method, "args": args_json, "metadata": "{}" } let cfg: ServiceConfig = proxy.config if str_eq(cfg.binding, "rest") { let m: String = json_get(proxy.method_table, method) if str_eq(m, "") { return response_err(404, SVC_ERR_METHOD_NOT_FOUND) } let verb: String = json_get(m, "verb") let path: String = json_get(m, "path") return rest_invoke(cfg, verb, path, req) } if str_eq(cfg.binding, "grpc") { return grpc_invoke(cfg, method, req) } if str_eq(cfg.binding, "websocket") { return ws_invoke(cfg, method, req) } response_err(400, SVC_ERR_BINDING) } // ── Entry — smoke test ────────────────────────────────────────────────────── let cfg: ServiceConfig = service_config_rest("UserService", "https://api.example.com", "env:API_TOKEN") println("[el-services] registered " + cfg.name + " @ " + cfg.base_url)