grove
at @6
1//! The model as JSON, for the API: what the server sends and the CLI reads back.
2
3use crate::hex;
4use crate::json::{obj, Value};
5use crate::model::{Actor, Change, ChangeState, Event, Evidence, Message, Point, Project, Revision, State, Step};
6use crate::objects::Id;
7
8fn id(i: &Id) -> Value {
9 Value::Str(hex::encode(i))
10}
11
12fn actor(a: &Actor) -> Value {
13 a.to_json()
14}
15
16fn step(s: &Step) -> Value {
17 crate::model::step_to_json(s)
18}
19
20pub fn project_json(state: &State, p: &Project) -> Value {
21 // Names of every agent that appears, so clients can say "claude for anton".
22 let mut agents: Vec<(String, Value)> = Vec::new();
23 let mut note = |a: &Actor| {
24 if let Some(aid) = &a.agent {
25 if !agents.iter().any(|(k, _)| k == aid) {
26 let name = state.agent(aid).map(|g| g.name.clone()).unwrap_or_else(|| aid.clone());
27 agents.push((aid.clone(), name.into()));
28 }
29 }
30 };
31 for pt in &p.points {
32 note(&pt.author);
33 note(&pt.landed_by);
34 }
35 for c in &p.changes {
36 for (_, by, _) in &c.timeline {
37 note(by);
38 }
39 }
40 obj([
41 ("owner", p.owner.clone().into()),
42 ("name", p.name.clone().into()),
43 ("public", p.public.into()),
44 ("at", p.at.into()),
45 ("points", Value::Array(p.points.iter().map(point).collect())),
46 ("changes", Value::Array(p.changes.iter().map(change).collect())),
47 ("agents", Value::Object(agents)),
48 ])
49}
50
51fn point(p: &Point) -> Value {
52 obj([
53 ("n", p.n.into()),
54 ("snapshot", id(&p.snapshot)),
55 ("message", p.message.clone().into()),
56 ("author", actor(&p.author)),
57 ("landed_by", actor(&p.landed_by)),
58 ("change", p.change.into()),
59 ("step", p.step.clone().into()),
60 ("at", p.at.into()),
61 ])
62}
63
64fn change(c: &Change) -> Value {
65 obj([
66 ("number", c.number.into()),
67 ("intent", c.intent.clone().into()),
68 ("proposer", actor(&c.proposer)),
69 ("state", c.state.name().into()),
70 ("at", c.at.into()),
71 (
72 "revisions",
73 Value::Array(
74 c.revisions
75 .iter()
76 .map(|r| {
77 obj([
78 ("number", (r.number as u64).into()),
79 ("base", r.base.into()),
80 ("steps", Value::Array(r.steps.iter().map(step).collect())),
81 ("note", r.note.clone().into()),
82 ("by", actor(&r.by)),
83 ("at", r.at.into()),
84 ])
85 })
86 .collect(),
87 ),
88 ),
89 (
90 "messages",
91 Value::Array(c.messages.iter().map(|m| obj([("id", (m.id as u64).into()), ("body", m.body.clone().into()), ("by", actor(&m.by)), ("at", m.at.into())])).collect()),
92 ),
93 (
94 "evidence",
95 Value::Array(
96 c.evidence
97 .iter()
98 .map(|e| {
99 obj([
100 ("revision", (e.revision as u64).into()),
101 ("title", e.title.clone().into()),
102 ("type", e.media_type.clone().into()),
103 ("blob", e.blob.as_ref().map(|b| hex::encode(b)).into()),
104 ("url", e.url.clone().into()),
105 ("by", actor(&e.by)),
106 ("at", e.at.into()),
107 ])
108 })
109 .collect(),
110 ),
111 ),
112 ("timeline", Value::Array(c.timeline.iter().map(|(at, by, ev)| event(*at, by, ev)).collect())),
113 ])
114}
115
116fn event(at: i64, by: &Actor, ev: &Event) -> Value {
117 let (kind, detail): (&str, Value) = match ev {
118 Event::Opened => ("opened", Value::Null),
119 Event::Revised(n) => ("revised", (*n as u64).into()),
120 Event::State(s) => ("state", s.name().into()),
121 Event::Said(n) => ("said", (*n as u64).into()),
122 Event::Attached(i) => ("attached", (*i as u64).into()),
123 Event::Landed { revision, points } => (
124 "landed",
125 obj([("revision", (*revision as u64).into()), ("first", points.0.into()), ("last", points.1.into())]),
126 ),
127 };
128 obj([("at", at.into()), ("by", actor(by)), ("event", kind.into()), ("detail", detail)])
129}
130
131// ── Reading it back ────────────────────────────────────────
132
133type R<T> = Result<T, String>;
134
135fn s(v: &Value, k: &str) -> R<String> {
136 v.get(k).and_then(Value::as_str).map(str::to_string).ok_or_else(|| format!("missing {k}"))
137}
138fn os(v: &Value, k: &str) -> Option<String> {
139 v.get(k).and_then(Value::as_str).map(str::to_string)
140}
141fn n(v: &Value, k: &str) -> R<i64> {
142 v.get(k).and_then(Value::as_i64).ok_or_else(|| format!("missing {k}"))
143}
144fn arr<'a>(v: &'a Value, k: &str) -> &'a [Value] {
145 v.get(k).and_then(Value::as_array).unwrap_or(&[])
146}
147fn id_of(v: &Value, k: &str) -> R<Id> {
148 hex::decode(&s(v, k)?).and_then(|b| b.try_into().ok()).ok_or_else(|| format!("bad id in {k}"))
149}
150fn actor_of(v: &Value, k: &str) -> R<Actor> {
151 let a = v.get(k).ok_or_else(|| format!("missing {k}"))?;
152 Ok(Actor { person: s(a, "person")?, agent: os(a, "agent") })
153}
154
155/// A project as sent by `project_json`, plus agent names by id.
156pub fn project_from_json(v: &Value) -> R<(Project, Vec<(String, String)>)> {
157 let points = arr(v, "points")
158 .iter()
159 .map(|p| {
160 Ok(Point {
161 n: n(p, "n")? as u64,
162 snapshot: id_of(p, "snapshot")?,
163 message: s(p, "message")?,
164 author: actor_of(p, "author")?,
165 landed_by: actor_of(p, "landed_by")?,
166 change: p.get("change").and_then(Value::as_i64).map(|x| x as u64),
167 step: os(p, "step"),
168 at: n(p, "at")?,
169 })
170 })
171 .collect::<R<Vec<_>>>()?;
172 let changes = arr(v, "changes")
173 .iter()
174 .map(|c| {
175 let revisions = arr(c, "revisions")
176 .iter()
177 .map(|r| {
178 Ok(Revision {
179 number: n(r, "number")? as u32,
180 base: n(r, "base")? as u64,
181 steps: arr(r, "steps")
182 .iter()
183 .map(|st| Ok(Step { id: s(st, "id")?, snapshot: id_of(st, "snapshot")?, message: s(st, "message")? }))
184 .collect::<R<Vec<_>>>()?,
185 note: os(r, "note"),
186 by: actor_of(r, "by")?,
187 at: n(r, "at")?,
188 })
189 })
190 .collect::<R<Vec<_>>>()?;
191 let messages = arr(c, "messages")
192 .iter()
193 .map(|m| Ok(Message { id: n(m, "id")? as u32, body: s(m, "body")?, by: actor_of(m, "by")?, at: n(m, "at")? }))
194 .collect::<R<Vec<_>>>()?;
195 let evidence = arr(c, "evidence")
196 .iter()
197 .map(|e| {
198 Ok(Evidence {
199 revision: n(e, "revision")? as u32,
200 title: s(e, "title")?,
201 media_type: s(e, "type")?,
202 blob: os(e, "blob").and_then(|h| hex::decode(&h)).and_then(|b| b.try_into().ok()),
203 url: os(e, "url"),
204 by: actor_of(e, "by")?,
205 at: n(e, "at")?,
206 })
207 })
208 .collect::<R<Vec<_>>>()?;
209 let timeline = arr(c, "timeline")
210 .iter()
211 .map(|t| {
212 let d = t.get("detail").cloned().unwrap_or(Value::Null);
213 let num = |v: &Value| v.as_i64().unwrap_or(0);
214 let ev = match s(t, "event")?.as_str() {
215 "opened" => Event::Opened,
216 "revised" => Event::Revised(num(&d) as u32),
217 "state" => Event::State(ChangeState::parse(d.as_str().unwrap_or("")).ok_or("bad state")?),
218 "said" => Event::Said(num(&d) as u32),
219 "attached" => Event::Attached(num(&d) as usize),
220 "landed" => Event::Landed {
221 revision: d.get("revision").map(num).unwrap_or(0) as u32,
222 points: (d.get("first").map(num).unwrap_or(0) as u64, d.get("last").map(num).unwrap_or(0) as u64),
223 },
224 other => return Err(format!("unknown event {other}")),
225 };
226 Ok((n(t, "at")?, actor_of(t, "by")?, ev))
227 })
228 .collect::<R<Vec<_>>>()?;
229 Ok(Change {
230 number: n(c, "number")? as u64,
231 intent: s(c, "intent")?,
232 proposer: actor_of(c, "proposer")?,
233 state: ChangeState::parse(&s(c, "state")?).ok_or("bad state")?,
234 revisions,
235 messages,
236 evidence,
237 timeline,
238 at: n(c, "at")?,
239 })
240 })
241 .collect::<R<Vec<_>>>()?;
242 let agents = match v.get("agents") {
243 Some(Value::Object(f)) => f.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or(k).to_string())).collect(),
244 _ => Vec::new(),
245 };
246 let public = matches!(v.get("public"), Some(Value::Bool(true)));
247 Ok((Project { owner: s(v, "owner")?, name: s(v, "name")?, public, points, changes, at: n(v, "at")? }, agents))
248}
249
250/// Objects in transit: each is `length (u32 LE), stored bytes`.
251pub fn frame_objects(objects: &[Vec<u8>]) -> Vec<u8> {
252 let mut out = Vec::new();
253 for o in objects {
254 out.extend_from_slice(&(o.len() as u32).to_le_bytes());
255 out.extend_from_slice(o);
256 }
257 out
258}
259
260pub fn unframe_objects(mut bytes: &[u8]) -> R<Vec<&[u8]>> {
261 let mut out = Vec::new();
262 while !bytes.is_empty() {
263 let len = u32::from_le_bytes(bytes.get(..4).ok_or("short frame")?.try_into().unwrap()) as usize;
264 let body = bytes.get(4..4 + len).ok_or("short object")?;
265 out.push(body);
266 bytes = &bytes[4 + len..];
267 }
268 Ok(out)
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::model::Actor;
275 use crate::objects::{Entry, Kind};
276 use crate::store::{Content, Store};
277 use crate::testdir::TestDir;
278
279 #[test]
280 fn project_round_trips() {
281 let dir = TestDir::new("wire");
282 let mut store = Store::open(dir.path()).unwrap();
283 store.add_person("a", "A").unwrap();
284 let agent = store.add_agent("a", "claude").unwrap();
285 let me = Actor::agent("a", &agent);
286 store.create_project(&Actor::person("a"), "a", "p", true).unwrap();
287 let blob = store.objects.put_blob(b"x").unwrap();
288 let t = store.objects.put_tree(&[Entry { name: "f".into(), kind: Kind::File, id: blob }]).unwrap();
289 store.record_point(&Actor::person("a"), "a", "p", &t, "start", None).unwrap();
290 let c = store.open_change(&me, "a", "p", "do it").unwrap();
291 store.revise(&me, "a", "p", c, 1, &[Step { id: "s".into(), snapshot: t, message: "m".into() }], Some("note")).unwrap();
292 store.set_state(&me, "a", "p", c, ChangeState::Proposed).unwrap();
293 store.say(&me, "a", "p", c, "hi").unwrap();
294 store.attach(&me, "a", "p", c, 1, "proof", "text/plain", Content::Bytes(b"ok")).unwrap();
295 store.land(&Actor::person("a"), "a", "p", c).unwrap();
296
297 let p = store.project("a", "p").unwrap();
298 let json = project_json(store.state(), p).encode();
299 let (back, agents) = project_from_json(&crate::json::parse(&json).unwrap()).unwrap();
300 assert_eq!(agents, vec![(agent.clone(), "claude".to_string())]);
301 assert_eq!(back.points.len(), 2);
302 assert_eq!(back.changes[0].state, ChangeState::Landed);
303 assert_eq!(back.changes[0].revisions[0].steps, p.changes[0].revisions[0].steps);
304 assert_eq!(back.changes[0].timeline.len(), p.changes[0].timeline.len());
305 // Encoding the decoded project gives the same JSON.
306 assert_eq!(project_json(store.state(), &back).encode(), json);
307 }
308
309 #[test]
310 fn frames() {
311 let objs = vec![b"one".to_vec(), Vec::new(), b"three".to_vec()];
312 let framed = frame_objects(&objs);
313 assert_eq!(unframe_objects(&framed).unwrap(), vec![&b"one"[..], &b""[..], &b"three"[..]]);
314 assert!(unframe_objects(&framed[..framed.len() - 1]).is_err());
315 }
316}