tilo_app/framework/
rows.rs1use tilo_actions::{ActionSpec, ResourcePresentation, ResourceSemanticIdentity, ResourceSpec};
7use tilo_resource::Effect;
8
9#[derive(Clone)]
29pub struct LiveRow {
30 resource: std::rc::Rc<ResourceSpec>,
31}
32
33impl LiveRow {
34 pub fn new(id: impl Into<String>, kind: impl Into<String>, label: impl Into<String>) -> Self {
38 Self {
39 resource: std::rc::Rc::new(ResourceSpec::new(id, kind, label)),
40 }
41 }
42
43 pub fn typed<R: tilo_resource::words::RowType>(
50 id: impl Into<String>,
51 label: impl Into<String>,
52 ) -> Self {
53 Self {
54 resource: std::rc::Rc::new(ResourceSpec::new(id, R::KIND, label).with_type(R::words())),
55 }
56 }
57
58 #[must_use]
61 pub fn with_type(mut self, words: tilo_actions::ResourceType) -> Self {
62 std::rc::Rc::make_mut(&mut self.resource).resource_type = Some(words);
63 self
64 }
65
66 #[must_use]
71 pub fn semantic_identity(mut self, identity: ResourceSemanticIdentity) -> Self {
72 std::rc::Rc::make_mut(&mut self.resource).semantic_identity =
73 identity.is_valid().then_some(identity);
74 self
75 }
76
77 pub fn from_resource(resource: ResourceSpec) -> Self {
82 Self {
83 resource: std::rc::Rc::new(resolved_outside_document(resource)),
84 }
85 }
86
87 #[must_use]
93 pub fn operation(mut self, operation: ActionSpec) -> Self {
94 if admits(&operation, self.id()) {
95 std::rc::Rc::make_mut(&mut self.resource)
96 .actions
97 .push(operation);
98 }
99 self
100 }
101
102 #[must_use]
106 pub fn presentation(mut self, presentation: ResourcePresentation) -> Self {
107 std::rc::Rc::make_mut(&mut self.resource).presentation = Some(presentation);
108 self
109 }
110
111 #[must_use]
115 pub fn effect<E: Effect>(self, effect: &E) -> Self {
116 self.operation(effect.bound())
117 }
118
119 #[must_use]
123 pub fn view(mut self, view: tilo_actions::ResourceViewSpec) -> Self {
124 std::rc::Rc::make_mut(&mut self.resource).views.push(view);
125 self
126 }
127
128 #[must_use]
130 pub fn row(mut self, row: LiveRow) -> Self {
131 std::rc::Rc::make_mut(&mut self.resource)
132 .children
133 .push(std::rc::Rc::unwrap_or_clone(row.resource));
134 self
135 }
136
137 pub fn id(&self) -> &str {
138 self.resource.id.as_str()
139 }
140
141 pub fn into_resource(self) -> ResourceSpec {
143 let mut resource = std::rc::Rc::unwrap_or_clone(self.resource);
144 tilo_resource::share::offer_tree(&mut resource);
145 resource
146 }
147
148 pub fn same_row(&self, other: &Self) -> bool {
151 std::rc::Rc::ptr_eq(&self.resource, &other.resource)
152 }
153}
154
155use ui_core::resources::admits_outside_document as admits;
156pub(crate) use ui_core::resources::{outside_document, resolved_outside_document};
157
158pub(crate) fn merge(
163 children: &mut Vec<ResourceSpec>,
164 rows: impl IntoIterator<Item = ResourceSpec>,
165) {
166 for row in rows {
167 match children.iter_mut().find(|child| child.id == row.id) {
168 Some(existing) => join(existing, row),
169 None => children.push(row),
170 }
171 }
172}
173
174pub(crate) fn join(existing: &mut ResourceSpec, row: ResourceSpec) {
177 if existing.presentation.is_none() {
178 existing.presentation = row.presentation;
179 }
180 if existing.semantic_identity.is_none() {
181 existing.semantic_identity = row
182 .semantic_identity
183 .filter(ResourceSemanticIdentity::is_valid);
184 }
185 for action in row.actions {
186 if !existing.actions.iter().any(|known| known.id == action.id) {
187 existing.actions.push(action);
188 }
189 }
190 let mut views_added = false;
191 for view in row.views {
192 if !existing.views.iter().any(|known| known.id == view.id) {
193 existing.views.push(view);
194 views_added = true;
195 }
196 }
197 if views_added {
198 tilo_resource::share::reoffer(existing);
200 }
201 merge(&mut existing.children, row.children);
202}
203
204pub(crate) fn find(resources: &[ResourceSpec], id: &str) -> Option<ResourceSpec> {
205 resources.iter().find_map(|resource| {
206 if resource.id.as_str() == id {
207 Some(resource.clone())
208 } else {
209 find(&resource.children, id)
210 }
211 })
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn merging_retains_the_selector_identity_without_overwriting_the_first_publisher() {
220 let identity = ResourceSemanticIdentity::new("chat.agent", "chat.agent:scribe").unwrap();
221 let row = || {
222 LiveRow::new("agent:1", "chat.agent", "Scribe")
223 .semantic_identity(identity.clone())
224 .into_resource()
225 };
226 let mut children = vec![ResourceSpec::new("agent:1", "chat.agent", "Document label")];
227 merge(&mut children, [row()]);
228 assert_eq!(children.len(), 1);
229 assert_eq!(children[0].semantic_identity, Some(identity.clone()));
230 assert_eq!(children[0].label, "Document label");
231
232 let first = ResourceSemanticIdentity::new("chat.agent", "chat.agent:reed").unwrap();
233 children[0].semantic_identity = Some(first.clone());
234 merge(&mut children, [row()]);
235 assert_eq!(children[0].semantic_identity, Some(first));
236 }
237
238 #[test]
239 fn live_row_retains_only_a_valid_typed_semantic_identity() {
240 let identity = ResourceSemanticIdentity::new("chat.agent", "chat.agent:scribe")
241 .expect("the bounded typed identity is valid");
242 let resource = LiveRow::new("opaque:1", "chat.agent", "Scribe")
243 .semantic_identity(identity.clone())
244 .into_resource();
245 assert_eq!(resource.semantic_identity, Some(identity));
246
247 let invalid = ResourceSemanticIdentity {
248 kind: "chat agent".into(),
249 key: "chat.agent:scribe".into(),
250 };
251 assert!(
252 LiveRow::new("opaque:2", "chat.agent", "Scribe")
253 .semantic_identity(invalid)
254 .into_resource()
255 .semantic_identity
256 .is_none(),
257 "a live row never publishes malformed selector metadata"
258 );
259 }
260}