majordomus_cli/capability/module.rs
1//! Modules compose capabilities; the application composes modules. A module is one Rust
2//! module with a `module()` function built by [`module!`](crate::module), owning its id,
3//! its metadata and the executables it ships; [`compose_modules!`](crate::compose_modules)
4//! names the modules the application is made of, and that list of modules is the only
5//! composition a person writes at the root. Adding a capability to an existing module
6//! touches that module alone.
7//!
8//! The macros build plain values. Nothing is registered behind the caller's back: the
9//! result is handed to the registry builder explicitly in `app.rs`.
10
11use super::handler::Executable;
12use super::model::{ModuleId, Stability};
13
14/// One module: its identity, what it is for, and the executables it composes. The module
15/// stamps its id on every capability it carries, and the registry refuses a capability
16/// whose id namespace is not its module.
17pub struct ModuleDescriptor {
18 /// The identity, `[a-z][a-z0-9_-]*`; the namespace of every capability in it.
19 pub id: ModuleId,
20 /// The short name.
21 pub title: String,
22 /// One paragraph.
23 pub description: String,
24 /// Where the module stands, as a whole.
25 pub stability: Stability,
26 /// The executables, each already stamped with the module id.
27 pub capabilities: Vec<Executable>,
28}
29
30impl ModuleDescriptor {
31 /// A module over its executables; stamps the module id on each of them.
32 pub fn new(
33 id: &str,
34 title: &str,
35 description: &str,
36 stability: Stability,
37 mut capabilities: Vec<Executable>,
38 ) -> Self {
39 let id = ModuleId::unchecked(id);
40 for e in &mut capabilities {
41 e.capability.module = id.clone();
42 }
43 ModuleDescriptor {
44 id,
45 title: title.into(),
46 description: description.into(),
47 stability,
48 capabilities,
49 }
50 }
51}
52
53impl std::fmt::Debug for ModuleDescriptor {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("ModuleDescriptor")
56 .field("id", &self.id)
57 .field("capabilities", &self.capabilities.len())
58 .finish()
59 }
60}
61
62/// Build a [`ModuleDescriptor`]: the module's identity and metadata once, and the
63/// executables it composes, each a `capability!` value.
64///
65/// ```
66/// use majordomus_cli::{capability, module};
67/// use majordomus_cli::capability::{BenchmarkCases, CaseContext, Context, CapabilityError, Exposure, NamedCase, Stability};
68/// #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
69/// struct In {}
70/// impl BenchmarkCases for In {
71/// fn benchmark_cases(_: &CaseContext<'_>) -> Vec<NamedCase<Self>> { vec![NamedCase::new("default", In {})] }
72/// }
73/// #[derive(serde::Serialize, schemars::JsonSchema)]
74/// struct Out { ok: bool }
75/// fn ping(_: &Context, _: In) -> Result<Out, CapabilityError> { Ok(Out { ok: true }) }
76/// let m = module! {
77/// id: "demo", title: "Demo", description: "A module of one.", stability: Stability::Experimental,
78/// capabilities: [
79/// capability! { id: "demo.ping", title: "Ping", description: "Answers.", input: In, output: Out,
80/// stability: Stability::Experimental, exposure: Exposure::default(), tags: [], handler: ping },
81/// ],
82/// };
83/// assert_eq!(m.capabilities[0].capability.module.as_str(), "demo");
84/// ```
85#[macro_export]
86macro_rules! module {
87 (
88 id: $id:expr,
89 title: $title:expr,
90 description: $description:expr,
91 stability: $stability:expr,
92 capabilities: [ $($cap:expr),* $(,)? ] $(,)?
93 ) => {
94 $crate::capability::ModuleDescriptor::new($id, $title, $description, $stability, vec![$($cap),*])
95 };
96}
97
98/// Compose the application from its modules: each name is a Rust module with a
99/// `module()` function. This list is the one composition a person maintains at the root;
100/// a capability added inside a module needs no edit here.
101///
102/// ```
103/// use majordomus_cli::compose_modules;
104/// use majordomus_cli::capability::builtin;
105/// let modules = compose_modules![builtin::repository, builtin::objects];
106/// assert_eq!(modules.len(), 2);
107/// assert_eq!(modules[0].id.as_str(), "repository");
108/// ```
109#[macro_export]
110macro_rules! compose_modules {
111 ( $( $($segment:ident)::+ ),* $(,)? ) => {
112 vec![$( $($segment)::+::module() ),*]
113 };
114}