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