grove
at @7
1//! What the CLI works against: a store on this machine, or a grove server.
2//!
3//! Both keep objects locally (the remote one as a cache), because snapshots are taken and
4//! files written here. The remote backend uploads the objects a request refers to before
5//! sending it, and downloads what it needs on demand.
6
7use crate::client::Client;
8use crate::hex;
9use crate::json::{obj, Value};
10use crate::model::{step_to_json, Actor, ChangeState, Project, Step};
11use crate::objects::{Id, Objects};
12use crate::store::{Content, Store};
13use crate::wire;
14
15pub type R<T> = Result<T, String>;
16
17pub trait Backend {
18 fn objects(&self) -> &Objects;
19 /// Make sure every object under `tree` is available locally.
20 fn need(&mut self, tree: &Id) -> R<()>;
21 fn project(&mut self, owner: &str, name: &str) -> R<Project>;
22 fn agent_name(&self, id: &str) -> Option<String>;
23
24 fn create_project(&mut self, by: &Actor, owner: &str, name: &str, public: bool) -> R<()>;
25 fn record_point(&mut self, by: &Actor, owner: &str, name: &str, snapshot: &Id, message: &str) -> R<u64>;
26 fn open_change(&mut self, by: &Actor, owner: &str, name: &str, intent: &str) -> R<u64>;
27 fn revise(&mut self, by: &Actor, owner: &str, name: &str, change: u64, base: u64, steps: &[Step], note: Option<&str>) -> R<u32>;
28 fn set_state(&mut self, by: &Actor, owner: &str, name: &str, change: u64, to: ChangeState) -> R<()>;
29 fn land(&mut self, by: &Actor, owner: &str, name: &str, change: u64) -> R<(u64, u64)>;
30 fn say(&mut self, by: &Actor, owner: &str, name: &str, change: u64, body: &str) -> R<()>;
31 fn attach(&mut self, by: &Actor, owner: &str, name: &str, change: u64, revision: u32, title: &str, media_type: &str, content: Content) -> R<()>;
32 fn create_group(&mut self, by: &Actor, name: &str) -> R<()>;
33 fn add_member(&mut self, by: &Actor, group: &str, person: &str) -> R<()>;
34 fn move_project(&mut self, by: &Actor, owner: &str, name: &str, to: &str) -> R<()>;
35}
36
37// ── On this machine ────────────────────────────────────────
38
39pub struct Local(pub Store);
40
41fn s<T>(r: crate::store::Result<T>) -> R<T> {
42 r.map_err(|e| e.to_string())
43}
44
45impl Backend for Local {
46 fn objects(&self) -> &Objects {
47 &self.0.objects
48 }
49 fn need(&mut self, _: &Id) -> R<()> {
50 Ok(())
51 }
52 fn project(&mut self, owner: &str, name: &str) -> R<Project> {
53 s(self.0.project(owner, name)).cloned()
54 }
55 fn agent_name(&self, id: &str) -> Option<String> {
56 self.0.state().agent(id).map(|a| a.name.clone())
57 }
58 fn create_project(&mut self, by: &Actor, owner: &str, name: &str, public: bool) -> R<()> {
59 s(self.0.create_project(by, owner, name, public))
60 }
61 fn record_point(&mut self, by: &Actor, owner: &str, name: &str, snapshot: &Id, message: &str) -> R<u64> {
62 s(self.0.record_point(by, owner, name, snapshot, message, None))
63 }
64 fn open_change(&mut self, by: &Actor, owner: &str, name: &str, intent: &str) -> R<u64> {
65 s(self.0.open_change(by, owner, name, intent))
66 }
67 fn revise(&mut self, by: &Actor, owner: &str, name: &str, change: u64, base: u64, steps: &[Step], note: Option<&str>) -> R<u32> {
68 s(self.0.revise(by, owner, name, change, base, steps, note))
69 }
70 fn set_state(&mut self, by: &Actor, owner: &str, name: &str, change: u64, to: ChangeState) -> R<()> {
71 s(self.0.set_state(by, owner, name, change, to))
72 }
73 fn land(&mut self, by: &Actor, owner: &str, name: &str, change: u64) -> R<(u64, u64)> {
74 s(self.0.land(by, owner, name, change))
75 }
76 fn say(&mut self, by: &Actor, owner: &str, name: &str, change: u64, body: &str) -> R<()> {
77 s(self.0.say(by, owner, name, change, body))
78 }
79 fn attach(&mut self, by: &Actor, owner: &str, name: &str, change: u64, revision: u32, title: &str, media_type: &str, content: Content) -> R<()> {
80 s(self.0.attach(by, owner, name, change, revision, title, media_type, content))
81 }
82 fn create_group(&mut self, by: &Actor, name: &str) -> R<()> {
83 s(self.0.create_group(by, name))
84 }
85 fn add_member(&mut self, by: &Actor, group: &str, person: &str) -> R<()> {
86 s(self.0.add_member(by, group, person))
87 }
88 fn move_project(&mut self, by: &Actor, owner: &str, name: &str, to: &str) -> R<()> {
89 s(self.0.move_project(by, owner, name, to))
90 }
91}
92
93// ── A grove server ─────────────────────────────────────────
94
95pub struct Remote {
96 /// Acts as the agent (or the person, if no agent is configured).
97 agent: Client,
98 /// The person's own token, for decisions made in person. Optional.
99 person: Option<Client>,
100 cache: Objects,
101 names: Vec<(String, String)>,
102}
103
104/// Objects per upload or download request.
105const BATCH: usize = 2000;
106
107impl Remote {
108 pub fn new(server: &str, token: &str, person_token: Option<&str>, cache: Objects) -> Remote {
109 Remote {
110 agent: Client::new(server, Some(token.to_string())),
111 person: person_token.map(|t| Client::new(server, Some(t.to_string()))),
112 cache,
113 names: Vec::new(),
114 }
115 }
116
117 /// The client for an actor: in person needs the person's token.
118 fn client(&self, by: &Actor) -> R<&Client> {
119 if by.agent.is_none() {
120 return self.person.as_ref().ok_or_else(|| "this is a decision made in person, and no person token is configured".to_string());
121 }
122 Ok(&self.agent)
123 }
124
125 /// Send every object under `tree` the server doesn't have.
126 fn upload(&self, tree: &Id) -> R<()> {
127 let all = self.cache.ids_under(tree).map_err(|e| e.to_string())?;
128 for chunk in all.chunks(BATCH) {
129 let ids: Vec<Value> = chunk.iter().map(|i| Value::Str(hex::encode(i))).collect();
130 let resp = self.agent.post("/api/objects/missing", &obj([("ids", Value::Array(ids))]))?;
131 let missing = resp.get("missing").and_then(Value::as_array).unwrap_or(&[]);
132 if missing.is_empty() {
133 continue;
134 }
135 let raws = missing
136 .iter()
137 .map(|m| {
138 let id: Id = hex::decode(m.as_str().unwrap_or("")).and_then(|b| b.try_into().ok()).ok_or("bad id from server")?;
139 self.cache.get_raw(&id).map_err(|e| e.to_string())
140 })
141 .collect::<R<Vec<_>>>()?;
142 self.agent.post_bytes("/api/objects", &wire::frame_objects(&raws), "application/octet-stream")?;
143 }
144 Ok(())
145 }
146
147 fn path(owner: &str, name: &str, rest: &str) -> String {
148 format!("/api/projects/{owner}/{name}{rest}")
149 }
150
151 fn num(v: &Value, k: &str) -> R<u64> {
152 v.get(k).and_then(Value::as_i64).map(|n| n as u64).ok_or_else(|| format!("server sent no {k}"))
153 }
154}
155
156impl Backend for Remote {
157 fn objects(&self) -> &Objects {
158 &self.cache
159 }
160
161 fn need(&mut self, tree: &Id) -> R<()> {
162 if self.cache.has(tree) && self.cache.ids_under(tree).is_ok() {
163 return Ok(());
164 }
165 let list = self.agent.post("/api/objects/list", &obj([("root", Value::Str(hex::encode(tree)))]))?;
166 let wanted: Vec<Value> = list
167 .get("ids")
168 .and_then(Value::as_array)
169 .unwrap_or(&[])
170 .iter()
171 .filter(|v| hex::decode(v.as_str().unwrap_or("")).and_then(|b| <[u8; 32]>::try_from(b).ok()).is_some_and(|id| !self.cache.has(&id)))
172 .cloned()
173 .collect();
174 for chunk in wanted.chunks(BATCH) {
175 let (status, bytes) = self.agent.call("POST", "/api/objects/fetch", Some((obj([("ids", Value::Array(chunk.to_vec()))]).encode().as_bytes(), "application/json")))?;
176 if status != 200 {
177 return Err(format!("fetching objects failed ({status})"));
178 }
179 for raw in wire::unframe_objects(&bytes)? {
180 self.cache.put_raw(raw).map_err(|e| e.to_string())?;
181 }
182 }
183 Ok(())
184 }
185
186 fn project(&mut self, owner: &str, name: &str) -> R<Project> {
187 let v = self.agent.get(&Self::path(owner, name, ""))?;
188 let (p, names) = wire::project_from_json(&v)?;
189 self.names = names;
190 Ok(p)
191 }
192
193 fn agent_name(&self, id: &str) -> Option<String> {
194 self.names.iter().find(|(k, _)| k == id).map(|(_, v)| v.clone())
195 }
196
197 fn create_project(&mut self, by: &Actor, owner: &str, name: &str, public: bool) -> R<()> {
198 self.client(by)?.post("/api/projects", &obj([("owner", owner.into()), ("name", name.into()), ("public", public.into())]))?;
199 Ok(())
200 }
201
202 fn record_point(&mut self, by: &Actor, owner: &str, name: &str, snapshot: &Id, message: &str) -> R<u64> {
203 self.upload(snapshot)?;
204 let v = self.client(by)?.post(&Self::path(owner, name, "/points"), &obj([("snapshot", Value::Str(hex::encode(snapshot))), ("message", message.into())]))?;
205 Self::num(&v, "n")
206 }
207
208 fn open_change(&mut self, by: &Actor, owner: &str, name: &str, intent: &str) -> R<u64> {
209 let v = self.client(by)?.post(&Self::path(owner, name, "/changes"), &obj([("intent", intent.into())]))?;
210 Self::num(&v, "number")
211 }
212
213 fn revise(&mut self, by: &Actor, owner: &str, name: &str, change: u64, base: u64, steps: &[Step], note: Option<&str>) -> R<u32> {
214 for st in steps {
215 self.upload(&st.snapshot)?;
216 }
217 let body = obj([
218 ("base", base.into()),
219 ("steps", Value::Array(steps.iter().map(step_to_json).collect())),
220 ("note", note.map(str::to_string).into()),
221 ]);
222 let v = self.client(by)?.post(&Self::path(owner, name, &format!("/changes/{change}/revisions")), &body)?;
223 Self::num(&v, "number").map(|n| n as u32)
224 }
225
226 fn set_state(&mut self, by: &Actor, owner: &str, name: &str, change: u64, to: ChangeState) -> R<()> {
227 self.client(by)?.post(&Self::path(owner, name, &format!("/changes/{change}/state")), &obj([("state", to.name().into())]))?;
228 Ok(())
229 }
230
231 fn land(&mut self, by: &Actor, owner: &str, name: &str, change: u64) -> R<(u64, u64)> {
232 let v = self.client(by)?.post(&Self::path(owner, name, &format!("/changes/{change}/land")), &obj([]))?;
233 Ok((Self::num(&v, "first")?, Self::num(&v, "last")?))
234 }
235
236 fn say(&mut self, by: &Actor, owner: &str, name: &str, change: u64, body: &str) -> R<()> {
237 self.client(by)?.post(&Self::path(owner, name, &format!("/changes/{change}/messages")), &obj([("body", body.into())]))?;
238 Ok(())
239 }
240
241 fn attach(&mut self, by: &Actor, owner: &str, name: &str, change: u64, revision: u32, title: &str, media_type: &str, content: Content) -> R<()> {
242 let (blob, url) = match content {
243 Content::Bytes(bytes) => {
244 let id = self.cache.put_blob(bytes).map_err(|e| e.to_string())?;
245 let raw = self.cache.get_raw(&id).map_err(|e| e.to_string())?;
246 self.agent.post_bytes("/api/objects", &wire::frame_objects(&[raw]), "application/octet-stream")?;
247 (Value::Str(hex::encode(&id)), Value::Null)
248 }
249 Content::Blob(id) => (Value::Str(hex::encode(&id)), Value::Null),
250 Content::Url(u) => (Value::Null, Value::from(u)),
251 };
252 let body = obj([("revision", (revision as u64).into()), ("title", title.into()), ("type", media_type.into()), ("blob", blob), ("url", url)]);
253 self.client(by)?.post(&Self::path(owner, name, &format!("/changes/{change}/evidence")), &body)?;
254 Ok(())
255 }
256
257 fn create_group(&mut self, by: &Actor, name: &str) -> R<()> {
258 self.client(by)?.post("/api/groups", &obj([("name", name.into())]))?;
259 Ok(())
260 }
261
262 fn add_member(&mut self, by: &Actor, group: &str, person: &str) -> R<()> {
263 self.client(by)?.post(&format!("/api/groups/{group}/members"), &obj([("person", person.into())]))?;
264 Ok(())
265 }
266
267 fn move_project(&mut self, by: &Actor, owner: &str, name: &str, to: &str) -> R<()> {
268 self.client(by)?.post(&Self::path(owner, name, "/move"), &obj([("to", to.into())]))?;
269 Ok(())
270 }
271}