use std::pin::Pin;
use std::sync::Arc;
use tracing::{debug, warn};
use apcore::context::Context;
use apcore::errors::ModuleError;
use apcore::Registry;
use crate::output::types::{Verifier, WriteResult};
use crate::output::verifiers::{run_verifier_chain, RegistryVerifier};
use crate::types::ScannedModule;
pub type HandlerFn = Arc<
dyn for<'a> Fn(
serde_json::Value,
&'a Context<serde_json::Value>,
) -> Pin<
Box<
dyn std::future::Future<Output = Result<serde_json::Value, ModuleError>>
+ Send
+ 'a,
>,
> + Send
+ Sync,
>;
pub type HandlerFactory = Arc<dyn Fn(&str) -> Option<HandlerFn> + Send + Sync>;
pub struct RegistryWriter {
handler_factory: Option<HandlerFactory>,
allowed_prefixes: Option<Vec<String>>,
}
impl Default for RegistryWriter {
fn default() -> Self {
Self::new()
}
}
impl RegistryWriter {
pub fn new() -> Self {
Self {
handler_factory: None,
allowed_prefixes: None,
}
}
pub fn with_handler_factory(factory: HandlerFactory) -> Self {
Self {
handler_factory: Some(factory),
allowed_prefixes: None,
}
}
pub fn with_allowed_prefixes(mut self, prefixes: Vec<String>) -> Self {
self.allowed_prefixes = Some(prefixes);
self
}
fn target_allowed(&self, target: &str) -> bool {
match self.allowed_prefixes.as_ref() {
None => true,
Some(prefixes) => {
let module_path = target.split(':').next().unwrap_or(target);
prefixes
.iter()
.any(|p| module_path_matches_prefix(module_path, p))
}
}
}
}
fn module_path_matches_prefix(module_path: &str, prefix: &str) -> bool {
let normalized = prefix.trim_end_matches('.');
if normalized.is_empty() {
return false;
}
if module_path == normalized {
return true;
}
let mut boundary = String::with_capacity(normalized.len() + 1);
boundary.push_str(normalized);
boundary.push('.');
module_path.starts_with(&boundary)
}
impl RegistryWriter {
pub fn write(
&self,
modules: &[ScannedModule],
registry: &mut Registry,
dry_run: bool,
verify: bool,
verifiers: Option<&[&dyn Verifier]>,
) -> Vec<WriteResult> {
let mut results: Vec<WriteResult> = Vec::new();
for module in modules {
if dry_run {
results.push(WriteResult::new(module.module_id.clone()));
continue;
}
if !self.target_allowed(&module.target) {
warn!(
module_id = %module.module_id,
target = %module.target,
"RegistryWriter: target rejected by allowed_prefixes"
);
results.push(WriteResult::failed(
module.module_id.clone(),
None,
format!(
"target '{}' is not in allowed_prefixes — registration refused",
module.target
),
));
continue;
}
let fm = self.to_function_module(module);
let descriptor = apcore::registry::registry::ModuleDescriptor {
module_id: module.module_id.clone(),
name: Some(module.module_id.clone()),
description: module.description.clone(),
documentation: module.documentation.clone(),
input_schema: module.input_schema.clone(),
output_schema: module.output_schema.clone(),
version: module.version.clone(),
tags: module.tags.clone(),
annotations: module.annotations.clone(),
examples: module.examples.clone(),
metadata: module.metadata.clone(),
display: module.display.clone(),
sunset_date: None,
dependencies: vec![],
enabled: true,
};
if let Err(e) = registry.register(&module.module_id, Box::new(fm), descriptor) {
warn!(
module_id = %module.module_id,
error = %e,
"RegistryWriter registration failed"
);
results.push(WriteResult::failed(
module.module_id.clone(),
None,
format!("Registration failed: {e}"),
));
continue;
}
debug!("Registered module: {}", module.module_id);
let mut result = WriteResult::new(module.module_id.clone());
if verify {
result = verify_registry(&result, &module.module_id, registry);
}
if result.verified {
if let Some(vs) = verifiers {
let chain_result = run_verifier_chain(vs, "", &module.module_id);
if !chain_result.ok {
result = WriteResult::failed(
result.module_id,
result.path,
chain_result.error.unwrap_or_default(),
);
}
}
}
results.push(result);
}
results
}
}
impl RegistryWriter {
fn to_function_module(&self, module: &ScannedModule) -> apcore::decorator::FunctionModule {
let annotations = module.annotations.clone().unwrap_or_default();
let input_schema = module.input_schema.clone();
let output_schema = module.output_schema.clone();
if let Some(factory) = &self.handler_factory {
if let Some(handler) = factory(&module.target) {
return apcore::decorator::FunctionModule::new::<_, ()>(
annotations,
input_schema,
output_schema,
move |inputs: serde_json::Value,
ctx: &Context<serde_json::Value>|
-> Pin<
Box<
dyn std::future::Future<Output = Result<serde_json::Value, ModuleError>>
+ Send
+ '_,
>,
> { handler(inputs, ctx) },
);
}
}
debug!(
module_id = %module.module_id,
"RegistryWriter using passthrough handler (no HandlerFactory configured)",
);
fn passthrough<'a>(
inputs: serde_json::Value,
_ctx: &'a Context<serde_json::Value>,
) -> Pin<
Box<
dyn std::future::Future<Output = Result<serde_json::Value, ModuleError>>
+ Send
+ 'a,
>,
> {
Box::pin(async move { Ok(inputs) })
}
apcore::decorator::FunctionModule::new::<_, ()>(
annotations,
input_schema,
output_schema,
passthrough,
)
}
}
fn verify_registry(result: &WriteResult, module_id: &str, registry: &Registry) -> WriteResult {
let verifier = RegistryVerifier::new(registry);
let vr = verifier.verify("", module_id);
if vr.ok {
result.clone()
} else {
WriteResult::failed(module_id.into(), None, vr.error.unwrap_or_default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_module() -> ScannedModule {
ScannedModule::new(
"users.get".into(),
"Get user".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec!["users".into()],
"app:get_user".into(),
)
}
#[test]
fn test_write_dry_run() {
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let modules = vec![sample_module()];
let results = writer.write(&modules, &mut registry, true, false, None);
assert_eq!(results.len(), 1);
assert_eq!(results[0].module_id, "users.get");
assert!(!registry.has("users.get"));
}
#[test]
fn test_write_registers_module() {
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let modules = vec![sample_module()];
let results = writer.write(&modules, &mut registry, false, false, None);
assert_eq!(results.len(), 1);
assert!(registry.has("users.get"));
}
#[test]
fn test_write_with_verify() {
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let modules = vec![sample_module()];
let results = writer.write(&modules, &mut registry, false, true, None);
assert_eq!(results.len(), 1);
assert!(results[0].verified);
}
#[test]
fn test_write_empty_list() {
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let results = writer.write(&[], &mut registry, false, false, None);
assert!(results.is_empty());
}
#[test]
fn test_custom_verifier_runs_even_when_verify_false() {
use crate::output::types::{Verifier, VerifyResult};
struct AlwaysFail;
impl Verifier for AlwaysFail {
fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
VerifyResult::fail("custom verifier failed".into())
}
}
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let modules = vec![sample_module()];
let failing_verifier = AlwaysFail;
let verifiers: &[&dyn Verifier] = &[&failing_verifier];
let results = writer.write(&modules, &mut registry, false, false, Some(verifiers));
assert_eq!(results.len(), 1);
assert!(registry.has("users.get"));
assert!(
!results[0].verified,
"custom verifier must run even when verify=false; result: {:?}",
results[0]
);
assert!(
results[0]
.verification_error
.as_deref()
.unwrap_or("")
.contains("custom verifier failed"),
"verification_error should contain the custom verifier message"
);
}
#[test]
fn test_write_multiple_modules() {
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let modules = vec![
ScannedModule::new(
"mod.a".into(),
"A".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec![],
"app:a".into(),
),
ScannedModule::new(
"mod.b".into(),
"B".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec![],
"app:b".into(),
),
];
let results = writer.write(&modules, &mut registry, false, false, None);
assert_eq!(results.len(), 2);
assert!(registry.has("mod.a"));
assert!(registry.has("mod.b"));
assert!(results[0].verified);
assert!(results[1].verified);
}
#[test]
fn test_allowed_prefixes_rejects_non_matching_target() {
let writer =
RegistryWriter::new().with_allowed_prefixes(vec!["app".into(), "myapp".into()]);
let mut registry = Registry::new();
let allowed = sample_module(); let denied = ScannedModule::new(
"evil.module".into(),
"Forged target".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec![],
"evil:run_attacker_code".into(),
);
let results = writer.write(&[allowed, denied], &mut registry, false, false, None);
assert_eq!(results.len(), 2);
assert!(registry.has("users.get"));
assert!(results[0].verified);
assert!(!registry.has("evil.module"));
assert!(!results[1].verified);
let err = results[1].verification_error.as_deref().unwrap_or("");
assert!(
err.contains("allowed_prefixes"),
"rejection message should mention allowed_prefixes: got {err:?}"
);
}
#[test]
fn test_target_allowed_boundary_aware() {
let writer = RegistryWriter::new().with_allowed_prefixes(vec!["myapp".into()]);
assert!(writer.target_allowed("myapp:fn"));
assert!(writer.target_allowed("myapp.foo:fn"));
assert!(writer.target_allowed("myapp.foo.bar:fn"));
assert!(!writer.target_allowed("myappx.evil:fn"));
assert!(!writer.target_allowed("myappx:fn"));
assert!(!writer.target_allowed("other:fn"));
let writer2 = RegistryWriter::new().with_allowed_prefixes(vec!["myapp.foo".into()]);
assert!(writer2.target_allowed("myapp.foo:fn"));
assert!(writer2.target_allowed("myapp.foo.bar:fn"));
assert!(!writer2.target_allowed("myapp.foobar:fn"));
assert!(!writer2.target_allowed("myapp:fn"));
let writer3 = RegistryWriter::new().with_allowed_prefixes(vec!["myapp.".into()]);
assert!(writer3.target_allowed("myapp:fn"));
let writer4 = RegistryWriter::new().with_allowed_prefixes(vec!["".into()]);
assert!(!writer4.target_allowed("anything:fn"));
}
#[test]
fn test_allowed_prefixes_default_none_admits_everything() {
let writer = RegistryWriter::new();
let mut registry = Registry::new();
let module = ScannedModule::new(
"any.module".into(),
"Any target".into(),
json!({"type": "object"}),
json!({"type": "object"}),
vec![],
"anything-goes:func".into(),
);
let results = writer.write(&[module], &mut registry, false, false, None);
assert_eq!(results.len(), 1);
assert!(registry.has("any.module"));
}
}