use std::{path::Path, sync::Arc};
use schemars::JsonSchema;
use crate::{
FeatureFlag, GuideEntry, HumanViewDef, Middleware, OutputSchema, RuntimeGroupSpec,
SchemaRegistry, Stage, parse_guides_from_markdown,
};
pub type ModuleRegister = Arc<dyn Fn(&mut ModuleContext<'_>) -> RuntimeGroupSpec + Send + Sync>;
pub trait CommandModule: Send + Sync + std::fmt::Debug + 'static {
fn category(&self) -> String;
fn guides(&self) -> Vec<GuideEntry> {
Vec::new()
}
fn views(&self) -> Vec<HumanViewDef> {
Vec::new()
}
fn register(&self, context: &mut ModuleContext<'_>) -> RuntimeGroupSpec;
}
#[derive(Clone)]
#[non_exhaustive]
pub struct Module {
pub category: String,
pub guides: Vec<GuideEntry>,
pub views: Vec<HumanViewDef>,
pub feature_flag: Option<FeatureFlag>,
pub register: ModuleRegister,
}
impl Module {
#[must_use]
pub fn new<F>(category: impl Into<String>, register: F) -> Self
where
F: Fn(&mut ModuleContext<'_>) -> RuntimeGroupSpec + Send + Sync + 'static,
{
Self {
category: category.into(),
guides: Vec::new(),
views: Vec::new(),
feature_flag: None,
register: Arc::new(register),
}
}
#[must_use]
pub fn from_command_module<M>(module: M) -> Self
where
M: CommandModule,
{
let category = module.category();
let guides = module.guides();
let views = module.views();
let module = Arc::new(module);
Self {
category,
guides,
views,
feature_flag: None,
register: Arc::new(move |context| module.register(context)),
}
}
#[must_use]
pub fn with_guide(mut self, guide: GuideEntry) -> Self {
self.guides.push(guide);
self
}
#[must_use]
pub fn with_guides(mut self, guides: impl IntoIterator<Item = GuideEntry>) -> Self {
self.guides.extend(guides);
self
}
#[must_use]
pub fn with_guides_from_markdown(
self,
files: impl IntoIterator<Item = (impl AsRef<Path>, impl AsRef<[u8]>)>,
) -> Self {
self.with_guides(parse_guides_from_markdown(files))
}
#[must_use]
pub fn with_view(mut self, view: HumanViewDef) -> Self {
self.views.push(view);
self
}
#[must_use]
pub fn with_feature_flag(mut self, key: impl Into<String>, stage: Stage) -> Self {
self.feature_flag = Some(FeatureFlag::new(key, stage));
self
}
}
#[must_use]
pub fn build_module_group(module: &Module) -> RuntimeGroupSpec {
let mut middleware = Middleware::new();
let mut ctx = ModuleContext::new(&mut middleware);
(module.register)(&mut ctx)
}
impl std::fmt::Debug for Module {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Module")
.field("category", &self.category)
.field("guides", &self.guides)
.field("views", &self.views)
.field("feature_flag", &self.feature_flag)
.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct ModuleContext<'middleware> {
middleware: &'middleware mut Middleware,
guides: Vec<GuideEntry>,
views: Vec<HumanViewDef>,
}
impl<'middleware> ModuleContext<'middleware> {
pub(crate) fn new(middleware: &'middleware mut Middleware) -> Self {
Self {
middleware,
guides: Vec::new(),
views: Vec::new(),
}
}
pub fn middleware(&self) -> &Middleware {
self.middleware
}
pub fn middleware_mut(&mut self) -> &mut Middleware {
self.middleware
}
pub fn config(&self) -> &crate::config::ConfigFile {
&self.middleware.config
}
pub fn schema_registry(&mut self) -> &mut SchemaRegistry {
&mut self.middleware.schema_registry
}
pub fn register_schema<T: OutputSchema>(&mut self, command_path: impl Into<String>) {
self.middleware
.schema_registry
.register::<T>(command_path.into());
}
pub fn register_json_schema<T: JsonSchema>(&mut self, command_path: impl Into<String>) {
self.middleware
.schema_registry
.register_json_schema::<T>(command_path.into());
}
pub fn register_view(&mut self, view: HumanViewDef) {
self.middleware.human_views.register(view.clone());
self.views.push(view);
}
pub fn add_guide(&mut self, guide: GuideEntry) {
self.guides.push(guide);
}
pub fn add_guides(&mut self, guides: impl IntoIterator<Item = GuideEntry>) {
self.guides.extend(guides);
}
pub fn add_guides_from_markdown(
&mut self,
files: impl IntoIterator<Item = (impl AsRef<Path>, impl AsRef<[u8]>)>,
) {
self.add_guides(parse_guides_from_markdown(files));
}
pub(crate) fn into_parts(self) -> (Vec<GuideEntry>, Vec<HumanViewDef>) {
(self.guides, self.views)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::GroupSpec;
fn trivial_module(category: &str) -> Module {
Module::new(category.to_string(), |_ctx| {
RuntimeGroupSpec::new(GroupSpec::new("g", "short"))
})
}
#[test]
fn module_with_feature_flag_sets_key_and_stage() {
let module = trivial_module("cat").with_feature_flag("my-module-flag", Stage::Beta);
let flag = module.feature_flag.expect("feature flag should be set");
assert_eq!(flag.key, "my-module-flag");
assert_eq!(flag.stage, Stage::Beta);
}
#[test]
fn module_feature_flag_defaults_to_none() {
let module = trivial_module("cat");
assert!(module.feature_flag.is_none());
}
}