Skip to main content

intent_runtime/
server.rs

1use intent_ir::Module;
2use serde::Serialize;
3use serde_json::json;
4use tiny_http::{Header, Method, Response, Server};
5
6use crate::contract::{ActionRequest, execute_action};
7
8/// Serve a compiled module as a REST API.
9///
10/// Endpoints:
11/// - `GET  /`                  — module info (name, actions, entities)
12/// - `POST /actions/{name}`    — execute an action
13pub fn serve(module: Module, addr: &str) -> Result<(), Box<dyn std::error::Error>> {
14    let server = Server::http(addr).map_err(|e| format!("failed to bind {addr}: {e}"))?;
15    eprintln!("intent serve: listening on http://{addr}");
16    eprintln!("  module: {}", module.name);
17    for func in &module.functions {
18        eprintln!("  POST /actions/{}", func.name);
19    }
20    eprintln!("  GET  /");
21
22    for mut request in server.incoming_requests() {
23        let url = request.url().to_string();
24        let method = request.method().clone();
25
26        let (status, body) = match (method, url.as_str()) {
27            (Method::Get, "/") => {
28                let info = module_info(&module);
29                (200, serde_json::to_string_pretty(&info).unwrap())
30            }
31            (Method::Post, path) if path.starts_with("/actions/") => {
32                let action_name = &path["/actions/".len()..];
33                handle_action(&module, action_name, &mut request)
34            }
35            _ => (404, json!({"error": "not found"}).to_string()),
36        };
37
38        let content_type = Header::from_bytes("Content-Type", "application/json").unwrap();
39        let response = Response::from_string(body)
40            .with_status_code(status)
41            .with_header(content_type);
42        request.respond(response).ok();
43    }
44    Ok(())
45}
46
47fn handle_action(
48    module: &Module,
49    action_name: &str,
50    request: &mut tiny_http::Request,
51) -> (i32, String) {
52    let mut body = String::new();
53    if request.as_reader().read_to_string(&mut body).is_err() {
54        return (
55            400,
56            json!({"error": "failed to read request body"}).to_string(),
57        );
58    }
59
60    let action_request: ActionRequest = match serde_json::from_str::<ActionRequest>(&body) {
61        Ok(mut req) => {
62            req.action = action_name.to_string();
63            req
64        }
65        Err(e) => {
66            return (
67                400,
68                json!({"error": format!("invalid JSON: {e}")}).to_string(),
69            );
70        }
71    };
72
73    match execute_action(module, &action_request) {
74        Ok(result) => {
75            let status = if result.ok { 200 } else { 422 };
76            (status, serde_json::to_string_pretty(&result).unwrap())
77        }
78        Err(e) => (400, json!({"error": format!("{e}")}).to_string()),
79    }
80}
81
82#[derive(Serialize)]
83struct ModuleInfo {
84    name: String,
85    entities: Vec<EntityInfo>,
86    actions: Vec<ActionInfo>,
87    invariants: Vec<String>,
88}
89
90#[derive(Serialize)]
91struct EntityInfo {
92    name: String,
93    fields: Vec<FieldInfo>,
94}
95
96#[derive(Serialize)]
97struct FieldInfo {
98    name: String,
99    #[serde(rename = "type")]
100    ty: String,
101}
102
103#[derive(Serialize)]
104struct ActionInfo {
105    name: String,
106    params: Vec<FieldInfo>,
107    precondition_count: usize,
108    postcondition_count: usize,
109}
110
111fn module_info(module: &Module) -> ModuleInfo {
112    ModuleInfo {
113        name: module.name.clone(),
114        entities: module
115            .structs
116            .iter()
117            .map(|s| EntityInfo {
118                name: s.name.clone(),
119                fields: s
120                    .fields
121                    .iter()
122                    .map(|f| FieldInfo {
123                        name: f.name.clone(),
124                        ty: format!("{:?}", f.ty),
125                    })
126                    .collect(),
127            })
128            .collect(),
129        actions: module
130            .functions
131            .iter()
132            .map(|f| ActionInfo {
133                name: f.name.clone(),
134                params: f
135                    .params
136                    .iter()
137                    .map(|p| FieldInfo {
138                        name: p.name.clone(),
139                        ty: format!("{:?}", p.ty),
140                    })
141                    .collect(),
142                precondition_count: f.preconditions.len(),
143                postcondition_count: f.postconditions.len(),
144            })
145            .collect(),
146        invariants: module.invariants.iter().map(|i| i.name.clone()).collect(),
147    }
148}