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