1use alloc::string::String;
12use alloc::vec::Vec;
13use core::fmt;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum RightsClass {
22 Read,
23 Create,
24 Write,
25 Delete,
26 Send,
27}
28
29impl RightsClass {
30 pub const ALL: [RightsClass; 5] = [
32 RightsClass::Read,
33 RightsClass::Create,
34 RightsClass::Write,
35 RightsClass::Delete,
36 RightsClass::Send,
37 ];
38
39 pub fn as_str(self) -> &'static str {
41 match self {
42 RightsClass::Read => "read",
43 RightsClass::Create => "create",
44 RightsClass::Write => "write",
45 RightsClass::Delete => "delete",
46 RightsClass::Send => "send",
47 }
48 }
49
50 pub fn from_token(token: &str) -> Option<Self> {
52 Self::ALL.into_iter().find(|class| class.as_str() == token)
53 }
54}
55
56impl fmt::Display for RightsClass {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str(self.as_str())
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum LifecycleClass {
72 Primary,
73 Corpus { store: String },
74 Blob,
75}
76
77impl LifecycleClass {
78 pub fn from_token(token: &str, store: Option<&str>) -> Option<Self> {
83 match token {
84 "primary" => Some(LifecycleClass::Primary),
85 "corpus" | "representation" => Some(LifecycleClass::Corpus {
86 store: store.unwrap_or_default().into(),
87 }),
88 "blob" => Some(LifecycleClass::Blob),
89 _ => None,
90 }
91 }
92
93 pub fn as_str(&self) -> &'static str {
95 match self {
96 LifecycleClass::Primary => "primary",
97 LifecycleClass::Corpus { .. } => "corpus",
98 LifecycleClass::Blob => "blob",
99 }
100 }
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum Residency {
107 None,
108 Linked,
109 Process,
110}
111
112impl Residency {
113 pub fn as_str(self) -> &'static str {
115 match self {
116 Residency::None => "none",
117 Residency::Linked => "linked",
118 Residency::Process => "process",
119 }
120 }
121}
122
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub enum RowSubstrate {
126 Note,
127 Entity,
128}
129
130impl RowSubstrate {
131 pub fn as_str(self) -> &'static str {
133 match self {
134 RowSubstrate::Note => "note",
135 RowSubstrate::Entity => "entity",
136 }
137 }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct AppVerb {
143 pub name: String,
144 pub rights: RightsClass,
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct RowTypeDecl {
150 pub substrate: RowSubstrate,
151 pub kind: String,
152 pub lifecycle: LifecycleClass,
153}
154
155#[derive(Clone, Debug, PartialEq, Eq)]
162pub struct AppManifest {
163 pub name: String,
164 pub version: String,
165 pub signer: String,
166 pub verbs: Vec<AppVerb>,
167 pub entity_kinds: Vec<String>,
168 pub note_kinds: Vec<String>,
169 pub row_types: Vec<RowTypeDecl>,
170 pub capabilities: Vec<String>,
171 pub residency: Residency,
172}
173
174#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct OwnedNames<'a> {
178 pub owner: &'a str,
179 pub verbs: Vec<&'a str>,
180 pub entity_kinds: Vec<&'a str>,
181 pub note_kinds: Vec<&'a str>,
182}
183
184#[derive(Clone, Debug, PartialEq, Eq)]
186pub enum InstallRefusal {
187 EmptyIdentity { field: &'static str },
189 DuplicateVerb { verb: String },
191 UndeclaredRowType {
193 substrate: RowSubstrate,
194 kind: String,
195 },
196 RowTypeOutsideSlice {
198 substrate: RowSubstrate,
199 kind: String,
200 },
201 CorpusWithoutStore {
203 substrate: RowSubstrate,
204 kind: String,
205 },
206 VerbOwned {
208 verb: String,
209 owner: String,
210 candidate: String,
211 },
212 KindOwned {
214 substrate: RowSubstrate,
215 kind: String,
216 owner: String,
217 candidate: String,
218 },
219}
220
221impl fmt::Display for InstallRefusal {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 match self {
224 InstallRefusal::EmptyIdentity { field } => {
225 write!(f, "manifest field {field:?} is empty")
226 }
227 InstallRefusal::DuplicateVerb { verb } => {
228 write!(f, "manifest declares verb {verb:?} twice")
229 }
230 InstallRefusal::UndeclaredRowType { substrate, kind } => write!(
231 f,
232 "{} kind {kind:?} is in the ontology slice but has no lifecycle class; every row type the app writes declares primary or corpus",
233 substrate.as_str()
234 ),
235 InstallRefusal::RowTypeOutsideSlice { substrate, kind } => write!(
236 f,
237 "{} kind {kind:?} has a lifecycle declaration but is not in the ontology slice",
238 substrate.as_str()
239 ),
240 InstallRefusal::CorpusWithoutStore { substrate, kind } => write!(
241 f,
242 "{} kind {kind:?} is a corpus row type and names no store",
243 substrate.as_str()
244 ),
245 InstallRefusal::VerbOwned {
246 verb,
247 owner,
248 candidate,
249 } => write!(
250 f,
251 "verb {verb:?} is already registered by {owner:?}; app {candidate:?} cannot register it"
252 ),
253 InstallRefusal::KindOwned {
254 substrate,
255 kind,
256 owner,
257 candidate,
258 } => write!(
259 f,
260 "{} kind {kind:?} is already contributed by {owner:?}; app {candidate:?} cannot contribute it",
261 substrate.as_str()
262 ),
263 }
264 }
265}
266
267impl AppManifest {
268 pub fn validate(&self) -> Result<(), InstallRefusal> {
272 for (field, value) in [
273 ("name", &self.name),
274 ("version", &self.version),
275 ("signer", &self.signer),
276 ] {
277 if value.is_empty() {
278 return Err(InstallRefusal::EmptyIdentity { field });
279 }
280 }
281 for (index, verb) in self.verbs.iter().enumerate() {
282 if self.verbs[..index]
283 .iter()
284 .any(|seen| seen.name == verb.name)
285 {
286 return Err(InstallRefusal::DuplicateVerb {
287 verb: verb.name.clone(),
288 });
289 }
290 }
291 for (substrate, kinds) in [
292 (RowSubstrate::Entity, &self.entity_kinds),
293 (RowSubstrate::Note, &self.note_kinds),
294 ] {
295 for kind in kinds {
296 if !self
297 .row_types
298 .iter()
299 .any(|row| row.substrate == substrate && row.kind == *kind)
300 {
301 return Err(InstallRefusal::UndeclaredRowType {
302 substrate,
303 kind: kind.clone(),
304 });
305 }
306 }
307 }
308 for row in &self.row_types {
309 let slice = match row.substrate {
310 RowSubstrate::Entity => &self.entity_kinds,
311 RowSubstrate::Note => &self.note_kinds,
312 };
313 if !slice.contains(&row.kind) {
314 return Err(InstallRefusal::RowTypeOutsideSlice {
315 substrate: row.substrate,
316 kind: row.kind.clone(),
317 });
318 }
319 if matches!(&row.lifecycle, LifecycleClass::Corpus { store } if store.is_empty()) {
320 return Err(InstallRefusal::CorpusWithoutStore {
321 substrate: row.substrate,
322 kind: row.kind.clone(),
323 });
324 }
325 }
326 Ok(())
327 }
328
329 pub fn owned_names(&self) -> OwnedNames<'_> {
331 OwnedNames {
332 owner: &self.name,
333 verbs: self.verbs.iter().map(|verb| verb.name.as_str()).collect(),
334 entity_kinds: self.entity_kinds.iter().map(String::as_str).collect(),
335 note_kinds: self.note_kinds.iter().map(String::as_str).collect(),
336 }
337 }
338
339 pub fn admit(&self, owners: &[OwnedNames<'_>]) -> Result<(), InstallRefusal> {
344 self.validate()?;
345 for owner in owners {
346 if let Some(verb) = self
347 .verbs
348 .iter()
349 .find(|verb| owner.verbs.iter().any(|held| *held == verb.name))
350 {
351 return Err(InstallRefusal::VerbOwned {
352 verb: verb.name.clone(),
353 owner: owner.owner.into(),
354 candidate: self.name.clone(),
355 });
356 }
357 for (substrate, kinds, held) in [
358 (
359 RowSubstrate::Entity,
360 &self.entity_kinds,
361 &owner.entity_kinds,
362 ),
363 (RowSubstrate::Note, &self.note_kinds, &owner.note_kinds),
364 ] {
365 if let Some(kind) = kinds.iter().find(|kind| held.contains(&kind.as_str())) {
366 return Err(InstallRefusal::KindOwned {
367 substrate,
368 kind: kind.clone(),
369 owner: owner.owner.into(),
370 candidate: self.name.clone(),
371 });
372 }
373 }
374 }
375 Ok(())
376 }
377}
378
379#[cfg(test)]
380#[path = "app_tests.rs"]
381mod tests;