feat(manifest): app manifest + decision crate (strict TOML subset, checked newtypes, capability catalog); docs/apps.md spec
This commit is contained in:
129
crates/manifest/src/manifest.rs
Normal file
129
crates/manifest/src/manifest.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
//! An app's manifest: who it is and what it asks for.
|
||||
|
||||
use crate::Error;
|
||||
use crate::capability::{Capabilities, Capability};
|
||||
use crate::fields::{Accent, AppId, Command, FileName, Text, Version};
|
||||
use crate::toml::{self, Entry, Value};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Manifest {
|
||||
pub id: AppId,
|
||||
pub name: Text<64>,
|
||||
pub version: Version,
|
||||
pub binary: FileName,
|
||||
/// What the shell runs it as.
|
||||
pub command: Command,
|
||||
pub author: Option<Text<64>>,
|
||||
pub icon: Option<FileName>,
|
||||
/// Denied = not installed.
|
||||
pub required: Capabilities,
|
||||
/// The owner decides each.
|
||||
pub optional: Capabilities,
|
||||
pub accent: Option<Accent>,
|
||||
}
|
||||
|
||||
/// Fields as they are read, before the required ones are known present.
|
||||
#[derive(Default)]
|
||||
struct Draft {
|
||||
id: Option<AppId>,
|
||||
name: Option<Text<64>>,
|
||||
version: Option<Version>,
|
||||
binary: Option<FileName>,
|
||||
command: Option<Command>,
|
||||
author: Option<Text<64>>,
|
||||
icon: Option<FileName>,
|
||||
required: Option<Capabilities>,
|
||||
optional: Option<Capabilities>,
|
||||
accent: Option<Accent>,
|
||||
}
|
||||
|
||||
/// Sets `slot` once; a second value for the same key is an error.
|
||||
fn set<T>(slot: &mut Option<T>, value: Option<T>, line: usize) -> Result<(), Error> {
|
||||
if slot.is_some() {
|
||||
return Err(Error::Duplicate { line });
|
||||
}
|
||||
*slot = Some(value.ok_or(Error::Invalid { line })?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn text<'a>(value: Value<'a>) -> Option<&'a str> {
|
||||
match value {
|
||||
Value::Str(s) => Some(s),
|
||||
Value::List(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of capability names, each known and listed once.
|
||||
pub(crate) fn capabilities(value: Value, line: usize) -> Result<Capabilities, Error> {
|
||||
let Value::List(list) = value else {
|
||||
return Err(Error::Invalid { line });
|
||||
};
|
||||
let mut set = Capabilities::NONE;
|
||||
for name in list.iter() {
|
||||
let c = Capability::from_name(name).ok_or(Error::Unknown { line })?;
|
||||
if set.contains(c) {
|
||||
return Err(Error::Duplicate { line });
|
||||
}
|
||||
set = set.with(c);
|
||||
}
|
||||
Ok(set)
|
||||
}
|
||||
|
||||
impl Draft {
|
||||
fn take(&mut self, e: Entry) -> Result<(), Error> {
|
||||
let l = e.line;
|
||||
let s = text(e.value);
|
||||
match (e.table, e.key) {
|
||||
("app", "id") => set(&mut self.id, s.and_then(AppId::new), l),
|
||||
("app", "name") => set(&mut self.name, s.and_then(Text::new), l),
|
||||
("app", "version") => set(&mut self.version, s.and_then(Version::parse), l),
|
||||
("app", "binary") => set(&mut self.binary, s.and_then(FileName::new), l),
|
||||
("app", "command") => set(&mut self.command, s.and_then(Command::new), l),
|
||||
("app", "author") => set(&mut self.author, s.and_then(Text::new), l),
|
||||
("app", "icon") => set(&mut self.icon, s.and_then(FileName::new), l),
|
||||
("capabilities", "required") => {
|
||||
set(&mut self.required, Some(capabilities(e.value, l)?), l)
|
||||
}
|
||||
("capabilities", "optional") => {
|
||||
set(&mut self.optional, Some(capabilities(e.value, l)?), l)
|
||||
}
|
||||
("appearance", "accent") => set(&mut self.accent, s.and_then(Accent::parse), l),
|
||||
_ => Err(Error::Unknown { line: l }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Manifest {
|
||||
pub fn parse(text: &str) -> Result<Self, Error> {
|
||||
let mut d = Draft::default();
|
||||
toml::parse(text, |e| d.take(e))?;
|
||||
let binary = d.binary.ok_or(Error::Missing("binary"))?;
|
||||
let command = match d.command {
|
||||
Some(c) => c,
|
||||
// A binary name is a command name when it fits the stricter rules.
|
||||
None => Command::new(binary.as_str()).ok_or(Error::Missing("command"))?,
|
||||
};
|
||||
let required = d.required.unwrap_or_default();
|
||||
let optional = d.optional.unwrap_or_default();
|
||||
if !required.intersect(optional).is_empty() {
|
||||
return Err(Error::Conflict);
|
||||
}
|
||||
Ok(Self {
|
||||
id: d.id.ok_or(Error::Missing("id"))?,
|
||||
name: d.name.ok_or(Error::Missing("name"))?,
|
||||
version: d.version.ok_or(Error::Missing("version"))?,
|
||||
binary,
|
||||
command,
|
||||
author: d.author,
|
||||
icon: d.icon,
|
||||
required,
|
||||
optional,
|
||||
accent: d.accent,
|
||||
})
|
||||
}
|
||||
|
||||
/// Everything it asks for.
|
||||
pub fn requested(&self) -> Capabilities {
|
||||
self.required.union(self.optional)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user