use std::collections::BTreeMap;
use serde::Deserialize;
use serde_json::json;
use crate::engine::{
FindMode, JoinOptions, JoinPredicate, Objective, Predicate, ProcurementAction, Query,
};
use crate::error::{Error, Result};
use crate::model::coupling::Coupling;
use crate::model::domain::Domain;
use crate::model::evidence::{Claim, EvidenceRecord, Source};
use crate::store::Db;
use crate::time::{now_unix, parse_when};
const MAX_BODY_BYTES: usize = 8 * 1024 * 1024;
pub struct NescioServer {
inner: tiny_http::Server,
}
impl NescioServer {
pub fn bind(addr: &str) -> Result<Self> {
let inner = tiny_http::Server::http(addr)
.map_err(|e| Error::Invalid(format!("cannot bind {addr}: {e}")))?;
Ok(NescioServer { inner })
}
pub fn port(&self) -> u16 {
self.inner
.server_addr()
.to_ip()
.map(|a| a.port())
.unwrap_or(0)
}
pub fn run(self, db: Db) -> Result<()> {
let server = std::sync::Arc::new(self.inner);
let db = std::sync::Arc::new(std::sync::RwLock::new(db));
let workers = std::thread::available_parallelism()
.map(|n| n.get().clamp(2, 8))
.unwrap_or(4);
let mut handles = Vec::new();
for _ in 0..workers {
let server = server.clone();
let db = db.clone();
handles.push(std::thread::spawn(move || {
while let Ok(mut request) = server.recv() {
let method = request.method().as_str().to_string();
let url = request.url().to_string();
let (path, query) = match url.split_once('?') {
Some((p, q)) => (p.to_string(), q.to_string()),
None => (url, String::new()),
};
let body = if request.body_length().unwrap_or(0) > MAX_BODY_BYTES {
None
} else {
let mut s = String::new();
match request.as_reader().read_to_string(&mut s) {
Ok(_) => Some(s),
Err(_) => Some(String::new()),
}
};
let (status, payload) = match body {
None => (413, json!({"error": "body too large"}).to_string()),
Some(b) => {
if is_write(&method, &path) {
let mut guard = db.write().expect("db lock poisoned");
handle(&mut guard, &method, &path, &query, &b)
} else {
let guard = db.read().expect("db lock poisoned");
let params = parse_query(&query);
to_response(route_read(&guard, &method, &path, ¶ms, &b))
}
}
};
let header = tiny_http::Header::from_bytes(
&b"Content-Type"[..],
&b"application/json"[..],
)
.expect("static header");
let response = tiny_http::Response::from_string(payload)
.with_status_code(status)
.with_header(header);
let _ = request.respond(response);
}
}));
}
for h in handles {
let _ = h.join();
}
Ok(())
}
}
fn is_write(method: &str, path: &str) -> bool {
method == "POST" && !matches!(path, "/resolve" | "/decide" | "/join")
}
pub fn handle(db: &mut Db, method: &str, path: &str, query: &str, body: &str) -> (u16, String) {
let params = parse_query(query);
let result = if is_write(method, path) {
route_write(db, method, path, body)
} else {
route_read(db, method, path, ¶ms, body)
};
to_response(result)
}
fn to_response(result: Result<String>) -> (u16, String) {
match result {
Ok(payload) => (200, payload),
Err(Error::AxiomConflict(m)) => (
409,
json!({"error": format!("axiom conflict: {m}")}).to_string(),
),
Err(Error::Invalid(m)) => {
let status = if m.starts_with("no such route") {
404
} else {
400
};
(status, json!({"error": m}).to_string())
}
Err(e) => (500, json!({"error": e.to_string()}).to_string()),
}
}
fn route_read(
db: &Db,
method: &str,
path: &str,
params: &BTreeMap<String, String>,
body: &str,
) -> Result<String> {
match (method, path) {
("GET", "/health") => {
Ok(json!({"ok": true, "version": env!("CARGO_PKG_VERSION")}).to_string())
}
("GET", "/status") => {
let sources: Vec<&Source> = db.sources.values().collect();
Ok(serde_json::to_string(&json!({
"evidence": db.evidence.len(),
"entities": db.entities().count(),
"slots": db.schema.slots,
"couplings": db.schema.couplings.iter().map(|c| c.label()).collect::<Vec<_>>(),
"sources": sources,
}))?)
}
("GET", "/bound") => {
let q = Query::new(db, at_param(params)?);
let credible = num_param(params, "credible")?.unwrap_or(0.95);
let b = q.bound(
req_param(params, "entity")?,
req_param(params, "slot")?,
credible,
)?;
Ok(serde_json::to_string(&b)?)
}
("GET", "/sample") => {
let q = Query::new(db, at_param(params)?);
let seed = num_param(params, "seed")?.unwrap_or(0.0) as u64;
let world = q.sample(req_param(params, "entity")?, seed)?;
Ok(serde_json::to_string(&world)?)
}
("GET", "/certainly") => {
let q = Query::new(db, at_param(params)?);
let pred = predicate_from(params)?;
let tri = q.certainly(
req_param(params, "entity")?,
req_param(params, "slot")?,
&pred,
)?;
Ok(serde_json::to_string(&json!({"result": tri}))?)
}
("GET", "/find") => {
let q = Query::new(db, at_param(params)?);
let lo = num_param(params, "lo")?
.ok_or_else(|| Error::Invalid("missing param lo".into()))?;
let hi = num_param(params, "hi")?
.ok_or_else(|| Error::Invalid("missing param hi".into()))?;
let mode: FindMode = params
.get("mode")
.map_or("possible", |s| s.as_str())
.parse()?;
let found = q.find(req_param(params, "slot")?, lo, hi, mode)?;
Ok(serde_json::to_string(&found)?)
}
("POST", "/resolve") => {
#[derive(Deserialize)]
struct Body {
entity: String,
slot: String,
target_bits: f64,
actions: Vec<ProcurementAction>,
max_steps: Option<usize>,
mc: Option<usize>,
seed: Option<u64>,
at: Option<serde_json::Value>,
}
let b: Body = from_body(body)?;
let q = Query::new(db, at_value(b.at)?);
let plan = q.resolve(
&b.entity,
&b.slot,
b.target_bits,
&b.actions,
b.max_steps.unwrap_or(10),
b.mc.unwrap_or(12),
b.seed.unwrap_or(0),
)?;
Ok(serde_json::to_string(&plan)?)
}
("POST", "/decide") => {
#[derive(Deserialize)]
struct Body {
entity: String,
slot: String,
objective: Objective,
target: f64,
actions: Vec<ProcurementAction>,
max_steps: Option<usize>,
mc: Option<usize>,
seed: Option<u64>,
at: Option<serde_json::Value>,
}
let b: Body = from_body(body)?;
let q = Query::new(db, at_value(b.at)?);
let plan = q.resolve_decision(
&b.entity,
&b.slot,
&b.objective,
b.target,
&b.actions,
b.max_steps.unwrap_or(10),
b.mc.unwrap_or(12),
b.seed.unwrap_or(0),
)?;
Ok(serde_json::to_string(&plan)?)
}
("POST", "/join") => {
#[derive(Deserialize)]
struct Body {
predicate: JoinPredicate,
#[serde(default)]
options: JoinOptions,
#[serde(default)]
at: Option<serde_json::Value>,
}
let b: Body = from_body(body)?;
let q = Query::new(db, at_value(b.at)?);
let res = q.join(&b.predicate, &b.options)?;
Ok(serde_json::to_string(&res)?)
}
_ => Err(Error::Invalid(format!("no such route: {method} {path}"))),
}
}
#[derive(Deserialize)]
struct IngestBody {
entity: String,
claim: Claim,
source: String,
at: Option<serde_json::Value>,
}
impl IngestBody {
fn into_record(self) -> Result<EvidenceRecord> {
Ok(EvidenceRecord {
entity: self.entity,
claim: self.claim,
source: self.source,
observed_at: at_value(self.at)?,
})
}
}
fn route_write(db: &mut Db, method: &str, path: &str, body: &str) -> Result<String> {
match (method, path) {
("POST", "/ingest") => {
let b: IngestBody = from_body(body)?;
let rec = b.into_record()?;
let observed_at = rec.observed_at;
db.ingest(rec)?;
Ok(json!({"ok": true, "observed_at": observed_at}).to_string())
}
("POST", "/ingest-batch") => {
let bodies: Vec<IngestBody> = from_body(body)?;
let recs = bodies
.into_iter()
.map(IngestBody::into_record)
.collect::<Result<Vec<_>>>()?;
let n = db.ingest_batch(recs)?;
Ok(json!({"ok": true, "ingested": n}).to_string())
}
("POST", "/sources") => {
let source: Source = from_body(body)?;
let n = db.put_source(source)?;
Ok(json!({"ok": true, "reinterpreted": n}).to_string())
}
("POST", "/forget-source") => {
#[derive(Deserialize)]
struct Body {
source: String,
}
let b: Body = from_body(body)?;
let removed = db.forget_source(&b.source)?;
Ok(json!({"ok": true, "erased": removed}).to_string())
}
("POST", "/recalibrate") => {
#[derive(Deserialize)]
struct Body {
source: String,
apply: Option<bool>,
min_truth_reliability: Option<f64>,
}
let b: Body = from_body(body)?;
let fit = db.recalibrate_source(&b.source, b.min_truth_reliability.unwrap_or(0.99))?;
let mut applied = 0;
if b.apply.unwrap_or(false) {
let axiomatic = db.sources.get(&b.source).is_some_and(|s| s.axiomatic);
applied = db.put_source(Source {
name: b.source.clone(),
reliability: fit.r0,
half_life_days: fit.half_life_days,
axiomatic,
})?;
}
Ok(serde_json::to_string(
&json!({"fit": fit, "applied": applied}),
)?)
}
("POST", "/priors/register") => {
#[derive(Deserialize)]
struct Body {
name: String,
slot: String,
weights: Vec<f64>,
}
let b: Body = from_body(body)?;
db.register_prior(&b.name, &b.slot, b.weights)?;
Ok(json!({"ok": true}).to_string())
}
("POST", "/schema/add-slot") => {
#[derive(Deserialize)]
struct Body {
name: String,
domain: Domain,
}
let b: Body = from_body(body)?;
db.add_slot(&b.name, b.domain)?;
Ok(json!({"ok": true}).to_string())
}
("POST", "/schema/remove-slot") => {
#[derive(Deserialize)]
struct Body {
name: String,
}
let b: Body = from_body(body)?;
let r = db.remove_slot(&b.name)?;
Ok(json!({
"ok": true,
"evidence_erased": r.evidence_erased,
"priors_removed": r.priors_removed,
})
.to_string())
}
("POST", "/schema/add-value") => {
#[derive(Deserialize)]
struct Body {
slot: String,
value: String,
}
let b: Body = from_body(body)?;
let extended = db.add_value(&b.slot, &b.value)?;
Ok(json!({"ok": true, "priors_extended": extended}).to_string())
}
("POST", "/schema/add-coupling") => {
let c: Coupling = from_body(body)?;
db.add_coupling(c)?;
Ok(json!({"ok": true}).to_string())
}
("POST", "/schema/remove-coupling") => {
#[derive(Deserialize)]
struct Body {
name: String,
}
let b: Body = from_body(body)?;
db.remove_coupling(&b.name)?;
Ok(json!({"ok": true}).to_string())
}
("POST", "/priors/use") => {
#[derive(Deserialize)]
struct Body {
entity: String,
slot: String,
name: String,
}
let b: Body = from_body(body)?;
db.use_prior(&b.entity, &b.slot, &b.name)?;
Ok(json!({"ok": true}).to_string())
}
_ => Err(Error::Invalid(format!("no such route: {method} {path}"))),
}
}
fn from_body<T: for<'de> Deserialize<'de>>(body: &str) -> Result<T> {
serde_json::from_str(body).map_err(|e| Error::Invalid(format!("bad request body: {e}")))
}
fn req_param<'a>(params: &'a BTreeMap<String, String>, key: &str) -> Result<&'a str> {
params
.get(key)
.map(|s| s.as_str())
.ok_or_else(|| Error::Invalid(format!("missing param {key}")))
}
fn num_param(params: &BTreeMap<String, String>, key: &str) -> Result<Option<f64>> {
match params.get(key) {
None => Ok(None),
Some(s) => s
.parse::<f64>()
.map(Some)
.map_err(|_| Error::Invalid(format!("param {key} is not a number"))),
}
}
fn at_param(params: &BTreeMap<String, String>) -> Result<i64> {
match params.get("at") {
Some(s) => parse_when(s),
None => Ok(now_unix()),
}
}
fn at_value(v: Option<serde_json::Value>) -> Result<i64> {
match v {
None => Ok(now_unix()),
Some(serde_json::Value::String(s)) => parse_when(&s),
Some(serde_json::Value::Number(n)) => n
.as_i64()
.ok_or_else(|| Error::Invalid("'at' must be an integer or date string".into())),
Some(_) => Err(Error::Invalid(
"'at' must be an integer or date string".into(),
)),
}
}
fn predicate_from(params: &BTreeMap<String, String>) -> Result<Predicate> {
let op = req_param(params, "op")?;
let value_num = || {
num_param(params, "value")?
.ok_or_else(|| Error::Invalid("missing numeric param value".into()))
};
match op {
"gt" => Ok(Predicate::Gt {
value: value_num()?,
}),
"lt" => Ok(Predicate::Lt {
value: value_num()?,
}),
"between" => {
let lo = num_param(params, "lo")?
.ok_or_else(|| Error::Invalid("missing param lo".into()))?;
let hi = num_param(params, "hi")?
.ok_or_else(|| Error::Invalid("missing param hi".into()))?;
Ok(Predicate::Between { lo, hi })
}
"is" => Ok(Predicate::Is {
value: req_param(params, "value")?.to_string(),
}),
"is_not" => Ok(Predicate::IsNot {
value: req_param(params, "value")?.to_string(),
}),
_ => Err(Error::Invalid(format!("unknown predicate op {op:?}"))),
}
}
fn parse_query(query: &str) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
for pair in query.split('&') {
if pair.is_empty() {
continue;
}
let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
out.insert(percent_decode(k), percent_decode(v));
}
out
}
fn percent_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'+' => {
out.push(b' ');
i += 1;
}
b'%' if i + 2 < bytes.len() => {
let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
match hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
Some(b) => {
out.push(b);
i += 3;
}
None => {
out.push(bytes[i]);
i += 1;
}
}
}
b => {
out.push(b);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}