1use std::cell::{Cell, RefCell};
15use std::rc::Rc;
16
17use tilo_actions::{Action, ActionError, ActionInvoker, EncodedAction};
18use tilo_resource::{Doc, Fields, Reader, Resource};
19use ui_core::history_source::{ThreadWords, steps};
20use ui_core::storage::{
21 ChangeOrigin, InvokingAction, Settings, SettingsDocument, SettingsRevision, SettingsSchema,
22};
23
24use super::live::Live;
25use super::record::{Held, RecordDoor};
26
27#[derive(Clone, Copy)]
29enum SectionOp<'a> {
30 Apply(&'a dyn Action, &'a ChangeOrigin),
33 Publish(&'a str, &'a str),
35 Decode(&'a EncodedAction),
36 Stored,
38}
39
40enum SectionOut {
42 Done,
43 Published(Box<tilo_actions::ResourceSpec>),
44 Decoded(Box<dyn Action>),
45 Value(serde_json::Value),
46}
47
48pub(crate) struct SectionKind {
52 pub(crate) kind: &'static str,
53 pub(crate) label: &'static str,
54 action_type: &'static str,
55 section: &'static str,
57 app_id: String,
59 document: SettingsDocument,
60 ops: fn(&Self, SectionOp<'_>) -> Result<SectionOut, ActionError>,
61}
62
63impl SectionKind {
64 pub(crate) fn of<T>(settings: &Settings<T>) -> Rc<Self>
65 where
66 T: Resource<State = T> + SettingsSchema + Fields,
67 {
68 Rc::new(Self {
69 kind: T::KIND,
70 label: T::LABEL,
71 action_type: T::ACTION_TYPE,
72 section: T::SCOPE.section(),
73 app_id: settings.app_id().to_owned(),
74 document: settings.document().clone(),
75 ops: section_ops::<T>,
76 })
77 }
78
79 fn settings<T: SettingsSchema>(&self) -> Settings<T> {
81 Settings::for_app(&self.app_id).in_document(self.document.clone())
82 }
83
84 fn revision(&self) -> SettingsRevision {
85 self.document.revision()
86 }
87
88 fn apply(
92 &self,
93 action: &dyn Action,
94 id: &str,
95 by: &str,
96 invoked_by: Option<&InvokingAction>,
97 ) -> Result<(), ActionError> {
98 let origin = ChangeOrigin::action(format!("{}/{id}", self.kind))
99 .by(by.to_owned())
100 .invoked_by(invoked_by.cloned());
101 (self.ops)(self, SectionOp::Apply(action, &origin)).map(drop)
102 }
103
104 fn publish(&self, id: &str) -> Result<tilo_actions::ResourceSpec, ActionError> {
105 match (self.ops)(self, SectionOp::Publish(id, self.label))? {
106 SectionOut::Published(resource) => Ok(*resource),
107 _ => Err(ActionError::Refused("the section published nothing".into())),
108 }
109 }
110
111 fn decode(&self, action: &EncodedAction) -> Result<Box<dyn Action>, ActionError> {
112 match (self.ops)(self, SectionOp::Decode(action))? {
113 SectionOut::Decoded(action) => Ok(action),
114 _ => Err(ActionError::Refused("the section decoded nothing".into())),
115 }
116 }
117
118 fn stored(&self) -> Result<serde_json::Value, ActionError> {
120 match (self.ops)(self, SectionOp::Stored)? {
121 SectionOut::Value(value) => Ok(value),
122 _ => Err(ActionError::Refused("the section read nothing".into())),
123 }
124 }
125
126 fn step(&self, undo: bool, by: &str, aliases: &[String]) -> Result<bool, String> {
132 let verb = if undo { "undo" } else { "redo" };
133 let origin = ChangeOrigin::action(format!("{}/{verb}", self.kind)).by(by.to_owned());
134 let actions = format!("{}/", self.kind);
135 let owns = |version: &tilo_resource::history::Version| {
136 version.message.as_deref().is_some_and(|message| {
137 ui_core::storage::made_by(message, &actions, by)
138 || aliases
139 .iter()
140 .any(|alias| ui_core::storage::made_by(message, &actions, alias))
141 })
142 };
143 let mut stepped = false;
144 self.document
145 .edit_document(self.section, &origin, |doc| {
146 let steps = if undo {
149 steps::undo_where(doc, &owns, 1, |_| Ok(None), &ThreadWords)
150 } else {
151 steps::redo_where(doc, &owns, 1, |_| Ok(None), &ThreadWords)
152 }?;
153 stepped = !steps.is_empty();
154 Ok(())
155 })
156 .map_err(|error| error.to_string())?;
157 Ok(stepped)
158 }
159}
160
161fn section_ops<T>(kind: &SectionKind, op: SectionOp<'_>) -> Result<SectionOut, ActionError>
163where
164 T: Resource<State = T> + SettingsSchema + Fields,
165{
166 let refused = |error: &dyn std::fmt::Display| ActionError::Refused(error.to_string());
167 Ok(match op {
168 SectionOp::Apply(action, origin) => {
169 let root = &tilo_resource::__private::ROOT;
173 let settings = kind.settings::<T>();
174 let mut record = Doc::new();
175 T::initialize(&mut record).map_err(|error| refused(&error))?;
176 settings
177 .get()
178 .write_fields(&mut record, root)
179 .map_err(|error| refused(&error))?;
180 record.commit();
181 action.apply(&mut record)?;
182 let value = T::read_fields(&record, root).map_err(|error| refused(&error))?;
183 settings
184 .set_by(&value, Some(origin))
185 .map_err(|error| refused(&error))?;
186 SectionOut::Done
187 }
188 SectionOp::Publish(id, label) => SectionOut::Published(Box::new(
189 tilo_resource::publish::<T>(id, label, &kind.settings::<T>().get()),
190 )),
191 SectionOp::Decode(action) => SectionOut::Decoded(Box::new(
192 tilo_actions::decode_json::<T::Action>(&action.payload)
193 .map_err(|error| refused(&error))?,
194 )),
195 SectionOp::Stored => {
196 SectionOut::Value(tilo_resource::Model::to_json(&kind.settings::<T>().get()))
197 }
198 })
199}
200
201pub(crate) struct SectionDoor {
203 id: String,
204 kind: Rc<SectionKind>,
205 me: String,
209 earlier: Vec<String>,
210 seen: Cell<Option<SettingsRevision>>,
212 revision: Cell<u64>,
214 published: RefCell<Option<((u64, u64), tilo_actions::ResourceSpec)>>,
217}
218
219impl SectionDoor {
220 pub(crate) fn new(kind: Rc<SectionKind>, me: String, earlier: Vec<String>) -> Self {
221 Self {
222 id: format!("settings:{}", kind.kind),
223 kind,
224 me,
225 earlier,
226 seen: Cell::new(None),
227 revision: Cell::new(0),
228 published: RefCell::new(None),
229 }
230 }
231
232 pub(crate) fn label(&self) -> &'static str {
233 self.kind.label
234 }
235
236 fn moved(&self) {
237 self.revision.set(self.revision.get().wrapping_add(1));
238 }
239
240 fn notice(&self) {
243 let now = Some(self.kind.revision());
244 if self.seen.get() != now {
245 self.seen.set(now);
246 self.moved();
247 }
248 }
249
250 fn apply(
254 &self,
255 encoded: &EncodedAction,
256 invoker: &ActionInvoker,
257 invoked_by: Option<&InvokingAction>,
258 ) -> Result<(), ActionError> {
259 let action = self.kind.decode(encoded)?;
260 let id = action_id(&encoded.payload);
261 self.kind
262 .apply(action.as_ref(), &id, &invoker.actor, invoked_by)?;
263 self.notice();
264 Ok(())
265 }
266
267 pub(crate) fn step(&self, undo: bool, by: &str) -> bool {
270 let aliases: &[String] = if by == self.me { &self.earlier } else { &[] };
273 match self.kind.step(undo, by, aliases) {
274 Ok(stepped) => {
275 self.notice();
276 stepped
277 }
278 Err(error) => {
279 tracing::warn!(kind = self.kind.kind, %error, "settings section not stepped");
280 false
281 }
282 }
283 }
284
285 pub(crate) fn held<T>(self: &Rc<Self>) -> Held<T>
288 where
289 T: Resource<State = T> + SettingsSchema + Fields,
290 {
291 let settings = self.kind.settings::<T>();
292 let seen = Cell::new(settings.revision());
293 let live = Live::new(Reader::with_state(settings.get(), None));
294 let applying = Rc::clone(self);
295 Held::new(
296 self.id.clone(),
297 self.kind.label.to_owned(),
298 true,
299 live,
300 move |live: &Live<T>| {
301 let now = settings.revision();
302 if now == seen.get() {
303 return;
304 }
305 seen.set(now);
306 let stored = settings.get();
307 if tilo_resource::Model::to_json(&stored)
308 != live.with(tilo_resource::Model::to_json)
309 {
310 live.replace(stored);
311 }
312 },
313 move |action: &T::Action, invoker: &ActionInvoker| {
314 applying.apply(&tilo_resource::encode::<T>(action)?, invoker, None)
315 },
316 )
317 }
318}
319
320impl RecordDoor for SectionDoor {
321 fn id(&self) -> &str {
322 &self.id
323 }
324
325 fn editable(&self) -> bool {
326 true
327 }
328
329 fn revision(&self) -> u64 {
330 self.notice();
331 self.revision.get()
332 }
333
334 fn publish(&self) -> tilo_actions::ResourceSpec {
335 self.notice();
336 let revision = (self.revision.get(), ui_core::i18n::I18nManager::revision());
337 if let Some((at, published)) = self.published.borrow().as_ref()
338 && *at == revision
339 {
340 return published.clone();
341 }
342 let published = self.kind.publish(&self.id).unwrap_or_else(|error| {
343 tracing::warn!(kind = self.kind.kind, %error, "settings section unreadable");
344 tilo_actions::ResourceSpec::new(self.id.as_str(), self.kind.kind, self.kind.label)
345 });
346 let published = super::record::route(published, &self.id, true);
347 *self.published.borrow_mut() = Some((revision, published.clone()));
348 published
349 }
350
351 fn apply_encoded(&self, action: &EncodedAction, invoker: &ActionInvoker) -> Result<(), String> {
352 self.apply_invoked(action, invoker, None)
353 }
354}
355
356impl SectionDoor {
357 fn apply_invoked(
360 &self,
361 action: &EncodedAction,
362 invoker: &ActionInvoker,
363 invoked_by: Option<&InvokingAction>,
364 ) -> Result<(), String> {
365 if action.type_id != self.kind.action_type {
366 return Err(format!(
367 "{} takes no `{}` action",
368 self.kind.label, action.type_id
369 ));
370 }
371 self.apply(action, invoker, invoked_by)
372 .map_err(|error| error.to_string())
373 }
374}
375
376pub struct SettingsRecords {
384 doors: Vec<Rc<SectionDoor>>,
385 person: Option<Rc<super::person::PersonState>>,
389}
390
391impl SettingsRecords {
392 #[must_use]
395 pub fn new(app_id: &'static str, declare: super::Declare) -> Self {
396 Self::in_document(app_id, &[declare], SettingsDocument::Session)
397 }
398
399 #[must_use]
402 pub fn in_document(
403 app_id: &'static str,
404 declares: &[super::Declare],
405 document: SettingsDocument,
406 ) -> Self {
407 Self::declared(
408 super::record::Sections {
409 app_id,
410 document,
411 person: None,
412 doors: Vec::new(),
413 },
414 declares,
415 )
416 }
417
418 #[must_use]
424 pub fn of_person(
425 app_id: &'static str,
426 app_title: &'static str,
427 declares: &[super::Declare],
428 records: ui_core::record::person::PersonRecords,
429 ) -> Self {
430 Self::declared(
431 super::record::Sections {
432 app_id,
433 document: SettingsDocument::Session,
434 person: Some(Rc::new(super::person::PersonState::new(
435 records, None, app_id, app_title,
436 ))),
437 doors: Vec::new(),
438 },
439 declares,
440 )
441 }
442
443 #[must_use]
452 pub fn of_person_named(
453 app_id: &'static str,
454 app_title: &'static str,
455 person: &str,
456 earlier: &[String],
457 declares: &[super::Declare],
458 ) -> Self {
459 Self::declared(
460 super::record::Sections {
461 app_id,
462 document: SettingsDocument::Session,
463 person: Some(Rc::new(super::person::PersonState::without_records(
464 person.to_owned(),
465 earlier.to_vec(),
466 app_id,
467 app_title,
468 ))),
469 doors: Vec::new(),
470 },
471 declares,
472 )
473 }
474
475 fn declared(sections: super::record::Sections, declares: &[super::Declare]) -> Self {
476 let person = sections.person.clone();
477 let mut records = super::Records::sections(sections);
478 for declare in declares {
479 declare(&mut records);
480 }
481 Self {
482 doors: records.into_sections(),
483 person,
484 }
485 }
486
487 #[must_use]
491 pub fn history_owners(&self) -> Vec<ui_core::history_mode::HistoryOwner> {
492 self.person
493 .as_ref()
494 .map(|person| person.owners())
495 .unwrap_or_default()
496 }
497
498 #[must_use]
503 pub fn shared<P: tilo_resource::SharedRecord>(&self) -> Option<super::PersonRecord<P>> {
504 self.person.as_ref()?.hold_shared::<P>()
505 }
506
507 #[must_use]
510 pub fn resources(&self) -> Vec<tilo_actions::ResourceSpec> {
511 match &self.person {
512 Some(person) => person.resources(),
513 None => self.doors.iter().map(|door| door.publish()).collect(),
514 }
515 }
516
517 #[must_use]
521 pub fn stored(&self, kind: &str) -> Option<serde_json::Value> {
522 let door = self.doors.iter().find(|door| door.kind.kind == kind)?;
523 door.kind
524 .stored()
525 .map_err(|error| tracing::warn!(kind, %error, "settings section unreadable"))
526 .ok()
527 }
528
529 pub fn invoke(
536 &self,
537 address: &tilo_actions::ActionAddress,
538 arguments: &serde_json::Map<String, serde_json::Value>,
539 confirmed: bool,
540 invoker: &ActionInvoker,
541 invoked_by: Option<&InvokingAction>,
542 ) -> Result<tilo_actions::ActionOutput, tilo_actions::DoorError> {
543 let resources = self.resources();
544 let spec = tilo_actions::door::resolve(&resources, address)
545 .ok_or_else(|| tilo_actions::DoorError::Unknown(address.clone()))?;
546 if !confirmed
547 && spec.confirmation_for_json(arguments) != tilo_actions::ConfirmationPolicy::None
548 {
549 return Err(tilo_actions::DoorError::ConfirmationRequired);
550 }
551 let encoded = tilo_actions::door::encode_json(&resources, address, arguments)?;
552 if encoded.type_id == super::person::HISTORY_TYPE
553 && let Some(person) = &self.person
554 {
555 return person
556 .answer_for(&encoded.payload, invoker)
557 .map_err(tilo_actions::DoorError::Failed);
558 }
559 let request: super::record::RecordAction = tilo_actions::decode_json(&encoded.payload)
560 .map_err(|error| tilo_actions::DoorError::InvalidArguments(error.to_string()))?;
561 let door = self
562 .doors
563 .iter()
564 .find(|door| door.id() == request.record)
565 .ok_or_else(|| tilo_actions::DoorError::Unknown(address.clone()))?;
566 door.apply_invoked(&request.action, invoker, invoked_by)
567 .map(|()| tilo_actions::ActionOutput::default())
568 .map_err(tilo_actions::DoorError::Failed)
569 }
570}
571
572fn action_id(payload: &[u8]) -> String {
574 serde_json::from_slice::<serde_json::Value>(payload)
575 .ok()
577 .and_then(|value| {
578 let operation = value.get("op").unwrap_or(&value);
579 operation
580 .get("action")
581 .and_then(serde_json::Value::as_str)
582 .map(str::to_owned)
583 })
584 .unwrap_or_else(|| "action".to_owned())
585}