Skip to main content

apcore_toolkit/output/
registry_writer.rs

1// Registry writer for direct module registration.
2//
3// Converts ScannedModule instances into apcore Module implementations
4// and registers them directly into an apcore Registry.
5//
6// Framework adapters provide a `HandlerFactory` to resolve targets to real
7// async handlers. Without a factory, modules are registered with a passthrough
8// handler that echoes inputs (useful for schema-only registration).
9
10use std::pin::Pin;
11use std::sync::Arc;
12
13use tracing::{debug, warn};
14
15use apcore::context::Context;
16use apcore::errors::ModuleError;
17use apcore::Registry;
18
19use crate::output::types::{Verifier, WriteResult};
20use crate::output::verifiers::{run_verifier_chain, RegistryVerifier};
21use crate::types::ScannedModule;
22
23// TODO(release-gate): deep-chain parity with Python/TypeScript RegistryWriter — manual
24// review required. RegistryWriter is the primary candidate for missing-registration bugs
25// (audit D11 was inconclusive). Verify that all three SDKs perform equivalent registry
26// mutations and handle the same error paths before tagging 0.5.0.
27
28/// Async handler function type for registered modules.
29pub type HandlerFn = Arc<
30    dyn for<'a> Fn(
31            serde_json::Value,
32            &'a Context<serde_json::Value>,
33        ) -> Pin<
34            Box<
35                dyn std::future::Future<Output = Result<serde_json::Value, ModuleError>>
36                    + Send
37                    + 'a,
38            >,
39        > + Send
40        + Sync,
41>;
42
43/// Factory that resolves a `target` string to an async handler.
44///
45/// Framework adapters implement this to map target strings (e.g., `"myapp:get_user"`)
46/// to actual handler functions. For example, an Axum adapter might look up the
47/// handler in a route table; a generic adapter might use a dynamic dispatch map.
48///
49/// ```ignore
50/// let factory: HandlerFactory = Arc::new(|target: &str| {
51///     let handler = lookup_handler(target);
52///     Some(Arc::new(move |inputs, _ctx| {
53///         let h = handler.clone();
54///         Box::pin(async move { h.call(inputs).await })
55///     }))
56/// });
57/// let writer = RegistryWriter::with_handler_factory(factory);
58/// ```
59pub type HandlerFactory = Arc<dyn Fn(&str) -> Option<HandlerFn> + Send + Sync>;
60
61/// Registers ScannedModule instances directly into an apcore Registry.
62///
63/// This is the default writer used when no output_format is specified.
64/// Instead of writing files, it registers modules directly for immediate use.
65///
66/// ## Handler Resolution
67///
68/// By default (`RegistryWriter::new()`), modules are registered with a passthrough
69/// handler that returns inputs unchanged — useful for schema-only registration
70/// where execution is handled elsewhere.
71///
72/// For executable modules, use `RegistryWriter::with_handler_factory(factory)` to
73/// provide a [`HandlerFactory`] that resolves target strings to real handlers.
74pub struct RegistryWriter {
75    handler_factory: Option<HandlerFactory>,
76}
77
78impl Default for RegistryWriter {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl RegistryWriter {
85    /// Create a RegistryWriter with passthrough handlers (schema-only registration).
86    ///
87    /// # Panics
88    ///
89    /// This constructor does not panic. However, note that without a `HandlerFactory`,
90    /// all registered modules will use a passthrough handler that echoes inputs unchanged.
91    /// This is suitable for schema-only registration. For real execution, use
92    /// [`RegistryWriter::with_handler_factory`] to supply a factory that resolves targets
93    /// to actual async handlers.
94    pub fn new() -> Self {
95        Self {
96            handler_factory: None,
97        }
98    }
99
100    /// Create a RegistryWriter with a custom handler factory for target resolution.
101    pub fn with_handler_factory(factory: HandlerFactory) -> Self {
102        Self {
103            handler_factory: Some(factory),
104        }
105    }
106}
107
108impl RegistryWriter {
109    /// Register scanned modules into the registry.
110    ///
111    /// - `registry`: The apcore Registry to register modules into.
112    /// - `dry_run`: If true, skip registration and return results only.
113    /// - `verify`: If true, verify modules are retrievable after registration.
114    /// - `verifiers`: Optional custom verifiers run after the built-in check.
115    ///
116    /// # Verifier contract for registry-based modules
117    ///
118    /// Registry modules have no output file, so custom verifiers receive
119    /// `path = ""`. Built-in file-based verifiers (`YAMLVerifier`, `JSONVerifier`,
120    /// etc.) skip gracefully when path is empty. Custom verifiers must also
121    /// handle `path = ""` without erroring — use `module_id` for any
122    /// registry-based checks.
123    pub fn write(
124        &self,
125        modules: &[ScannedModule],
126        registry: &mut Registry,
127        dry_run: bool,
128        verify: bool,
129        verifiers: Option<&[&dyn Verifier]>,
130    ) -> Vec<WriteResult> {
131        let mut results: Vec<WriteResult> = Vec::new();
132
133        for module in modules {
134            if dry_run {
135                results.push(WriteResult::new(module.module_id.clone()));
136                continue;
137            }
138
139            let fm = self.to_function_module(module);
140            // Register with a descriptor
141            let descriptor = apcore::registry::registry::ModuleDescriptor {
142                module_id: module.module_id.clone(),
143                name: Some(module.module_id.clone()),
144                description: module.description.clone(),
145                documentation: module.documentation.clone(),
146                input_schema: module.input_schema.clone(),
147                output_schema: module.output_schema.clone(),
148                version: module.version.clone(),
149                tags: module.tags.clone(),
150                annotations: module.annotations.clone(),
151                examples: module.examples.clone(),
152                metadata: module.metadata.clone(),
153                display: module.display.clone(),
154                sunset_date: None,
155                dependencies: vec![],
156                enabled: true,
157            };
158            // Note: unlike Python/TypeScript, Rust collects per-module registration errors
159            // rather than aborting. This is intentional — partial registration is preferred
160            // over a hard stop, giving callers the opportunity to inspect and handle each failure.
161            if let Err(e) = registry.register(&module.module_id, Box::new(fm), descriptor) {
162                warn!(
163                    module_id = %module.module_id,
164                    error = %e,
165                    "RegistryWriter registration failed"
166                );
167                results.push(WriteResult::failed(
168                    module.module_id.clone(),
169                    None,
170                    format!("Registration failed: {e}"),
171                ));
172                continue;
173            }
174            debug!("Registered module: {}", module.module_id);
175
176            let mut result = WriteResult::new(module.module_id.clone());
177            if verify {
178                result = verify_registry(&result, &module.module_id, registry);
179            }
180            if result.verified {
181                if let Some(vs) = verifiers {
182                    let chain_result = run_verifier_chain(vs, "", &module.module_id);
183                    if !chain_result.ok {
184                        result = WriteResult::failed(
185                            result.module_id,
186                            result.path,
187                            chain_result.error.unwrap_or_default(),
188                        );
189                    }
190                }
191            }
192            results.push(result);
193        }
194
195        results
196    }
197}
198
199impl RegistryWriter {
200    /// Convert a ScannedModule to an apcore FunctionModule.
201    ///
202    /// If a handler factory is configured and resolves the target, uses the
203    /// resolved handler. Otherwise falls back to a passthrough handler that
204    /// returns inputs unchanged.
205    fn to_function_module(&self, module: &ScannedModule) -> apcore::decorator::FunctionModule {
206        let annotations = module.annotations.clone().unwrap_or_default();
207        let input_schema = module.input_schema.clone();
208        let output_schema = module.output_schema.clone();
209
210        // Try to resolve the target via the handler factory
211        if let Some(factory) = &self.handler_factory {
212            if let Some(handler) = factory(&module.target) {
213                return apcore::decorator::FunctionModule::new::<_, ()>(
214                    annotations,
215                    input_schema,
216                    output_schema,
217                    move |inputs: serde_json::Value,
218                          ctx: &Context<serde_json::Value>|
219                          -> Pin<
220                        Box<
221                            dyn std::future::Future<Output = Result<serde_json::Value, ModuleError>>
222                                + Send
223                                + '_,
224                        >,
225                    > { handler(inputs, ctx) },
226                );
227            }
228        }
229
230        // Fallback: passthrough handler (schema-only registration)
231        debug!(
232            module_id = %module.module_id,
233            "RegistryWriter using passthrough handler (no HandlerFactory configured)",
234        );
235        fn passthrough<'a>(
236            inputs: serde_json::Value,
237            _ctx: &'a Context<serde_json::Value>,
238        ) -> Pin<
239            Box<
240                dyn std::future::Future<Output = Result<serde_json::Value, ModuleError>>
241                    + Send
242                    + 'a,
243            >,
244        > {
245            Box::pin(async move { Ok(inputs) })
246        }
247
248        apcore::decorator::FunctionModule::new::<_, ()>(
249            annotations,
250            input_schema,
251            output_schema,
252            passthrough,
253        )
254    }
255}
256
257/// Verify that a module was successfully registered and is retrievable.
258fn verify_registry(result: &WriteResult, module_id: &str, registry: &Registry) -> WriteResult {
259    let verifier = RegistryVerifier::new(registry);
260    let vr = verifier.verify("", module_id);
261    if vr.ok {
262        result.clone()
263    } else {
264        WriteResult::failed(module_id.into(), None, vr.error.unwrap_or_default())
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use serde_json::json;
272
273    fn sample_module() -> ScannedModule {
274        ScannedModule::new(
275            "users.get".into(),
276            "Get user".into(),
277            json!({"type": "object"}),
278            json!({"type": "object"}),
279            vec!["users".into()],
280            "app:get_user".into(),
281        )
282    }
283
284    #[test]
285    fn test_write_dry_run() {
286        let writer = RegistryWriter::new();
287        let mut registry = Registry::new();
288        let modules = vec![sample_module()];
289        let results = writer.write(&modules, &mut registry, true, false, None);
290        assert_eq!(results.len(), 1);
291        assert_eq!(results[0].module_id, "users.get");
292        assert!(!registry.has("users.get"));
293    }
294
295    #[test]
296    fn test_write_registers_module() {
297        let writer = RegistryWriter::new();
298        let mut registry = Registry::new();
299        let modules = vec![sample_module()];
300        let results = writer.write(&modules, &mut registry, false, false, None);
301        assert_eq!(results.len(), 1);
302        assert!(registry.has("users.get"));
303    }
304
305    #[test]
306    fn test_write_with_verify() {
307        let writer = RegistryWriter::new();
308        let mut registry = Registry::new();
309        let modules = vec![sample_module()];
310        let results = writer.write(&modules, &mut registry, false, true, None);
311        assert_eq!(results.len(), 1);
312        assert!(results[0].verified);
313    }
314
315    #[test]
316    fn test_write_empty_list() {
317        let writer = RegistryWriter::new();
318        let mut registry = Registry::new();
319        let results = writer.write(&[], &mut registry, false, false, None);
320        assert!(results.is_empty());
321    }
322
323    #[test]
324    fn test_write_multiple_modules() {
325        let writer = RegistryWriter::new();
326        let mut registry = Registry::new();
327        let modules = vec![
328            ScannedModule::new(
329                "mod.a".into(),
330                "A".into(),
331                json!({"type": "object"}),
332                json!({"type": "object"}),
333                vec![],
334                "app:a".into(),
335            ),
336            ScannedModule::new(
337                "mod.b".into(),
338                "B".into(),
339                json!({"type": "object"}),
340                json!({"type": "object"}),
341                vec![],
342                "app:b".into(),
343            ),
344        ];
345        let results = writer.write(&modules, &mut registry, false, false, None);
346        assert_eq!(results.len(), 2);
347        assert!(registry.has("mod.a"));
348        assert!(registry.has("mod.b"));
349        assert!(results[0].verified);
350        assert!(results[1].verified);
351    }
352}