use super::config::{Capability, ConfigModule, ConfigModuleMetadata};
use anyhow::Context;
use cargo_metadata::{Package, Target};
use std::fs;
use std::path::PathBuf;
use syn::parse::{Parse, ParseStream};
use syn::{Attribute, Item, Lit, Meta};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedModule {
pub name: String,
pub deps: Vec<String>,
pub capabilities: Vec<Capability>,
}
pub fn retrieve_module_rs(
package: &Package,
target: &Target,
) -> anyhow::Result<(String, ConfigModule)> {
let lib_rs = PathBuf::from(&target.src_path);
let src = lib_rs
.parent()
.with_context(|| format!("no source parent for {}", target.src_path))?;
let module_rs = src.join("module.rs");
let content = fs::read_to_string(&module_rs)
.with_context(|| format!("can't read module from {}", module_rs.display()))?;
let parsed_module = parse_module_rs_source(&content)
.with_context(|| format!("invalid {}", module_rs.display()))?;
let crate_root = PathBuf::from(&package.manifest_path)
.parent()
.map(|p| p.display().to_string());
let config_module = ConfigModule {
metadata: ConfigModuleMetadata {
package: Some(package.name.to_string()),
version: Some(package.version.to_string()),
features: vec![],
default_features: None,
path: crate_root,
deps: parsed_module.deps,
capabilities: parsed_module.capabilities,
},
};
Ok((parsed_module.name, config_module))
}
pub fn parse_module_rs_source(content: &str) -> anyhow::Result<ParsedModule> {
let ast = syn::parse_file(content)?;
for item in ast.items {
if let Item::Struct(struct_item) = item
&& let Some(module_info) = parse_modkit_module_attribute(&struct_item.attrs)?
{
return Ok(ParsedModule {
name: module_info.name,
deps: module_info.deps,
capabilities: module_info.capabilities,
});
}
}
Err(anyhow::anyhow!("no module found"))
}
struct ModuleInfo {
name: String,
deps: Vec<String>,
capabilities: Vec<Capability>,
}
fn parse_modkit_module_attribute(attrs: &[Attribute]) -> anyhow::Result<Option<ModuleInfo>> {
for attr in attrs {
if is_modkit_module_path(attr) {
return parse_module_args(attr).map(Some);
}
}
Ok(None)
}
fn is_modkit_module_path(attr: &Attribute) -> bool {
let path = attr.path();
let segments: Vec<_> = path.segments.iter().map(|s| s.ident.to_string()).collect();
(segments.len() == 1 && segments[0] == "module")
|| (segments.len() == 2 && segments[0] == "modkit" && segments[1] == "module")
}
fn parse_module_args(attr: &Attribute) -> anyhow::Result<ModuleInfo> {
let mut name = None;
let mut deps = Vec::new();
let mut capabilities = Vec::new();
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value = meta.value()?;
let lit: Lit = value.parse()?;
if let Lit::Str(lit_str) = lit {
name = Some(lit_str.value());
}
} else if meta.path.is_ident("deps") {
let value = meta.value()?;
let expr: syn::Expr = value.parse()?;
if let syn::Expr::Array(array) = expr {
for element in array.elems {
if let syn::Expr::Lit(syn::ExprLit {
lit: Lit::Str(lit_str),
..
}) = element
{
deps.push(lit_str.value());
}
}
}
} else if meta.path.is_ident("capabilities") {
let value = meta.value()?;
let expr: syn::Expr = value.parse()?;
if let syn::Expr::Array(array) = expr {
for element in array.elems {
let capability = match element {
syn::Expr::Path(path_expr) => {
let Some(ident) = path_expr.path.get_ident() else {
return Err(syn::Error::new_spanned(
path_expr.path,
"capability must be a simple identifier",
));
};
parse_capability_name(&ident.to_string()).ok_or_else(|| {
syn::Error::new_spanned(
ident,
"unknown capability, expected one of: db, rest, rest_host, stateful, system, grpc_hub, grpc",
)
})?
}
syn::Expr::Lit(syn::ExprLit {
lit: Lit::Str(lit_str),
..
}) => parse_capability_name(&lit_str.value()).ok_or_else(|| {
syn::Error::new_spanned(
lit_str,
"unknown capability, expected one of: db, rest, rest_host, stateful, system, grpc_hub, grpc",
)
})?,
other => {
return Err(syn::Error::new_spanned(
other,
"capability must be an identifier or string literal",
));
}
};
capabilities.push(capability);
}
} else {
return Err(syn::Error::new_spanned(
expr,
"capabilities must be an array, e.g. capabilities = [db, rest]",
));
}
} else {
consume_unknown_meta(meta.input)?;
}
Ok(())
})?;
let name = name.context("module attribute must have a name")?;
Ok(ModuleInfo {
name,
deps,
capabilities,
})
}
fn parse_capability_name(name: &str) -> Option<Capability> {
match name {
"db" => Some(Capability::Db),
"rest" => Some(Capability::Rest),
"rest_host" => Some(Capability::RestHost),
"stateful" => Some(Capability::Stateful),
"system" => Some(Capability::System),
"grpc_hub" => Some(Capability::GrpcHub),
"grpc" => Some(Capability::Grpc),
_ => None,
}
}
fn consume_unknown_meta(input: ParseStream<'_>) -> syn::Result<()> {
if input.peek(syn::Token![=]) {
let _: syn::Token![=] = input.parse()?;
let _expr: syn::Expr = input.parse()?;
} else if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
let _nested: syn::punctuated::Punctuated<Meta, syn::Token![,]> =
content.parse_terminated(Meta::parse, syn::Token![,])?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::parse_module_rs_source;
use crate::module_parser::Capability;
#[test]
fn parses_module_with_lifecycle_meta() {
let content = r#"
#[modkit::module(
name = "grpc-hub",
capabilities = [stateful, system, grpc_hub],
lifecycle(entry = "serve", await_ready)
)]
pub struct GrpcHub;
"#;
let parsed = parse_module_rs_source(content).expect("module should parse");
assert_eq!(parsed.name, "grpc-hub");
assert!(parsed.deps.is_empty());
assert_eq!(
parsed.capabilities,
vec![
Capability::Stateful,
Capability::System,
Capability::GrpcHub
]
);
}
#[test]
fn parses_deps_list() {
let content = r#"
#[module(name = "demo", deps = ["authz", "tenant-resolver"])]
pub struct Demo;
"#;
let parsed = parse_module_rs_source(content).expect("module should parse");
assert_eq!(parsed.name, "demo");
assert_eq!(parsed.deps, vec!["authz", "tenant-resolver"]);
assert!(parsed.capabilities.is_empty());
}
#[test]
fn parses_capabilities_from_strings() {
let content = r#"
#[module(name = "demo", capabilities = ["db", "rest_host"])]
pub struct Demo;
"#;
let parsed = parse_module_rs_source(content).expect("module should parse");
assert_eq!(parsed.name, "demo");
assert_eq!(
parsed.capabilities,
vec![Capability::Db, Capability::RestHost]
);
}
}