Skip to main content

cli_engine/
module.rs

1use std::{path::Path, sync::Arc};
2
3use schemars::JsonSchema;
4
5use crate::{
6    GuideEntry, HumanViewDef, Middleware, OutputSchema, RuntimeGroupSpec, SchemaRegistry,
7    parse_guides_from_markdown,
8};
9
10/// Function used by closure-based modules to register a runtime command group.
11pub type ModuleRegister = Arc<dyn Fn(&mut ModuleContext<'_>) -> RuntimeGroupSpec + Send + Sync>;
12
13/// Trait-based module API for larger command domains.
14///
15/// Implement this when a module has dependencies or enough setup logic that a
16/// named type is clearer than a closure.
17pub trait CommandModule: Send + Sync + std::fmt::Debug + 'static {
18    /// Help category used in root command long help.
19    fn category(&self) -> String;
20
21    /// Guide entries contributed by this module.
22    fn guides(&self) -> Vec<GuideEntry> {
23        Vec::new()
24    }
25
26    /// Human views contributed by this module.
27    fn views(&self) -> Vec<HumanViewDef> {
28        Vec::new()
29    }
30
31    /// Registers the module's top-level runtime group.
32    fn register(&self, context: &mut ModuleContext<'_>) -> RuntimeGroupSpec;
33}
34
35/// Domain-bounded unit of CLI functionality.
36///
37/// A module usually maps to a product, platform, resource family, or team
38/// ownership boundary. It contributes one top-level group plus optional guides
39/// and human output views.
40#[derive(Clone)]
41pub struct Module {
42    /// Root help category.
43    pub category: String,
44    /// Guide entries merged into the CLI-wide guide command.
45    pub guides: Vec<GuideEntry>,
46    /// Human output views registered before command execution.
47    pub views: Vec<HumanViewDef>,
48    /// Registration function that returns the module's runtime group.
49    pub register: ModuleRegister,
50}
51
52impl Module {
53    /// Creates a closure-based module.
54    #[must_use]
55    pub fn new<F>(category: impl Into<String>, register: F) -> Self
56    where
57        F: Fn(&mut ModuleContext<'_>) -> RuntimeGroupSpec + Send + Sync + 'static,
58    {
59        Self {
60            category: category.into(),
61            guides: Vec::new(),
62            views: Vec::new(),
63            register: Arc::new(register),
64        }
65    }
66
67    /// Converts a trait-based module into the runtime module type.
68    #[must_use]
69    pub fn from_command_module<M>(module: M) -> Self
70    where
71        M: CommandModule,
72    {
73        let category = module.category();
74        let guides = module.guides();
75        let views = module.views();
76        let module = Arc::new(module);
77        Self {
78            category,
79            guides,
80            views,
81            register: Arc::new(move |context| module.register(context)),
82        }
83    }
84
85    /// Adds one guide entry.
86    #[must_use]
87    pub fn with_guide(mut self, guide: GuideEntry) -> Self {
88        self.guides.push(guide);
89        self
90    }
91
92    /// Adds several guide entries.
93    #[must_use]
94    pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
95        self.guides.extend(guides);
96        self
97    }
98
99    /// Parses markdown guide entries from embedded `(path, bytes)` pairs.
100    #[must_use]
101    pub fn with_guides_from_markdown(
102        self,
103        files: impl IntoIterator<Item = (impl AsRef<Path>, impl AsRef<[u8]>)>,
104    ) -> Self {
105        self.with_guides(parse_guides_from_markdown(files))
106    }
107
108    /// Adds one human output view.
109    #[must_use]
110    pub fn with_view(mut self, view: HumanViewDef) -> Self {
111        self.views.push(view);
112        self
113    }
114}
115
116impl std::fmt::Debug for Module {
117    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        formatter
119            .debug_struct("Module")
120            .field("category", &self.category)
121            .field("guides", &self.guides)
122            .field("views", &self.views)
123            .finish_non_exhaustive()
124    }
125}
126
127/// Context available while a module registers itself.
128///
129/// The context gives module code access to shared registries without exposing
130/// parser internals. This keeps module registration declarative and easy to
131/// copy for new teams.
132#[derive(Debug)]
133pub struct ModuleContext<'middleware> {
134    middleware: &'middleware mut Middleware,
135    guides: Vec<GuideEntry>,
136    views: Vec<HumanViewDef>,
137}
138
139impl<'middleware> ModuleContext<'middleware> {
140    pub(crate) fn new(middleware: &'middleware mut Middleware) -> Self {
141        Self {
142            middleware,
143            guides: Vec::new(),
144            views: Vec::new(),
145        }
146    }
147
148    /// Returns a shared view of middleware while registering the module.
149    pub fn middleware(&self) -> &Middleware {
150        self.middleware
151    }
152
153    /// Returns mutable middleware for module-specific setup.
154    pub fn middleware_mut(&mut self) -> &mut Middleware {
155        self.middleware
156    }
157
158    /// Returns the per-application config file as loaded at startup.
159    ///
160    /// Read a consumer-owned section with
161    /// [`ConfigFile::section`](crate::config::ConfigFile::section). This is
162    /// the same startup snapshot surfaced via
163    /// [`CommandContext::config`](crate::command::CommandContext::config); see
164    /// its documentation for snapshot-semantics caveats.
165    pub fn config(&self) -> &crate::config::ConfigFile {
166        &self.middleware.config
167    }
168
169    /// Returns the schema registry for direct registration.
170    pub fn schema_registry(&mut self) -> &mut SchemaRegistry {
171        &mut self.middleware.schema_registry
172    }
173
174    /// Registers a compact framework schema for a command path.
175    pub fn register_schema<T: OutputSchema>(&mut self, command_path: impl Into<String>) {
176        self.middleware
177            .schema_registry
178            .register::<T>(command_path.into());
179    }
180
181    /// Registers JSON Schema generated with `schemars` for a command path.
182    pub fn register_json_schema<T: JsonSchema>(&mut self, command_path: impl Into<String>) {
183        self.middleware
184            .schema_registry
185            .register_json_schema::<T>(command_path.into());
186    }
187
188    /// Registers a human output view and keeps it with the module.
189    pub fn register_view(&mut self, view: HumanViewDef) {
190        self.middleware.human_views.register(view.clone());
191        self.views.push(view);
192    }
193
194    /// Adds one guide entry.
195    pub fn add_guide(&mut self, guide: GuideEntry) {
196        self.guides.push(guide);
197    }
198
199    /// Adds several guide entries.
200    pub fn add_guides(&mut self, guides: impl IntoIterator<Item = GuideEntry>) {
201        self.guides.extend(guides);
202    }
203
204    /// Parses and adds markdown guides from embedded `(path, bytes)` pairs.
205    pub fn add_guides_from_markdown(
206        &mut self,
207        files: impl IntoIterator<Item = (impl AsRef<Path>, impl AsRef<[u8]>)>,
208    ) {
209        self.add_guides(parse_guides_from_markdown(files));
210    }
211
212    pub(crate) fn into_parts(self) -> (Vec<GuideEntry>, Vec<HumanViewDef>) {
213        (self.guides, self.views)
214    }
215}