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 schema registry for direct registration.
159    pub fn schema_registry(&mut self) -> &mut SchemaRegistry {
160        &mut self.middleware.schema_registry
161    }
162
163    /// Registers a compact framework schema for a command path.
164    pub fn register_schema<T: OutputSchema>(&mut self, command_path: impl Into<String>) {
165        self.middleware
166            .schema_registry
167            .register::<T>(command_path.into());
168    }
169
170    /// Registers JSON Schema generated with `schemars` for a command path.
171    pub fn register_json_schema<T: JsonSchema>(&mut self, command_path: impl Into<String>) {
172        self.middleware
173            .schema_registry
174            .register_json_schema::<T>(command_path.into());
175    }
176
177    /// Registers a human output view and keeps it with the module.
178    pub fn register_view(&mut self, view: HumanViewDef) {
179        self.middleware.human_views.register(view.clone());
180        self.views.push(view);
181    }
182
183    /// Adds one guide entry.
184    pub fn add_guide(&mut self, guide: GuideEntry) {
185        self.guides.push(guide);
186    }
187
188    /// Adds several guide entries.
189    pub fn add_guides(&mut self, guides: impl IntoIterator<Item = GuideEntry>) {
190        self.guides.extend(guides);
191    }
192
193    /// Parses and adds markdown guides from embedded `(path, bytes)` pairs.
194    pub fn add_guides_from_markdown(
195        &mut self,
196        files: impl IntoIterator<Item = (impl AsRef<Path>, impl AsRef<[u8]>)>,
197    ) {
198        self.add_guides(parse_guides_from_markdown(files));
199    }
200
201    pub(crate) fn into_parts(self) -> (Vec<GuideEntry>, Vec<HumanViewDef>) {
202        (self.guides, self.views)
203    }
204}