Skip to main content

apcore_toolkit/output/
mod.rs

1// Output writers for ScannedModule data.
2//
3// Provides writers for different output formats (YAML, registry, HTTP proxy).
4
5pub mod errors;
6pub mod registry_writer;
7pub mod rust_writer;
8pub mod types;
9pub mod verifiers;
10pub mod yaml_writer;
11
12#[cfg(feature = "http-proxy")]
13pub mod http_proxy_writer;
14
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18/// Error returned by [`get_writer`] when an unrecognised format string is supplied.
19///
20/// Named to mirror the Python `InvalidFormatError` and TypeScript `InvalidFormatError`
21/// exports for cross-SDK symbol parity.
22#[derive(Debug, Error, PartialEq)]
23pub enum InvalidFormatError {
24    /// The format string does not map to a known [`OutputFormat`] variant.
25    #[error("Unknown output format: {0}")]
26    Unknown(String),
27}
28
29/// Supported output format variants.
30///
31/// Used by `get_writer` to select the appropriate writer implementation.
32/// Each variant corresponds to a distinct writer struct with its own `write()`
33/// signature, so the factory returns the enum itself rather than a trait object.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub enum OutputFormat {
36    /// Write `.binding.yaml` files to disk.
37    Yaml,
38    /// Register modules directly into an apcore Registry.
39    Registry,
40    /// Register modules as HTTP proxy modules (requires `http-proxy` feature).
41    #[cfg(feature = "http-proxy")]
42    HTTPProxy,
43}
44
45/// Convenience factory that returns the `OutputFormat` variant for a given
46/// format string.
47///
48/// # Accepted values
49///
50/// Canonical formats are matched case-sensitively to mirror the Python and
51/// TypeScript SDKs. The HTTP-proxy aliases keep their case-insensitive,
52/// underscore/hyphen-tolerant matching as documented in
53/// `apcore-toolkit/docs/features/output-writers.md`.
54///
55/// | Input | Variant | Match style |
56/// |-------|---------|-------------|
57/// | `"yaml"` | `OutputFormat::Yaml` | exact |
58/// | `"registry"` | `OutputFormat::Registry` | exact |
59/// | `"http_proxy"` / `"http-proxy"` / `"httpproxy"` | `OutputFormat::HTTPProxy` | case-insensitive |
60///
61/// Returns `Err` for unrecognised strings.
62///
63/// # Usage
64///
65/// ```rust
66/// use apcore_toolkit::output::get_writer;
67/// use apcore_toolkit::output::OutputFormat;
68///
69/// let fmt = get_writer("yaml").unwrap();
70/// assert_eq!(fmt, OutputFormat::Yaml);
71///
72/// // Then instantiate the concrete writer:
73/// match fmt {
74///     OutputFormat::Yaml => { /* use YAMLWriter */ }
75///     OutputFormat::Registry => { /* use RegistryWriter */ }
76///     // OutputFormat::HTTPProxy (feature "http-proxy") => use HTTPProxyRegistryWriter
77///     #[allow(unreachable_patterns)]
78///     _ => { /* other variants (e.g. HTTPProxy when the `http-proxy` feature is enabled) */ }
79/// }
80/// ```
81pub fn get_writer(format: &str) -> Result<OutputFormat, InvalidFormatError> {
82    match format {
83        "yaml" => return Ok(OutputFormat::Yaml),
84        "registry" => return Ok(OutputFormat::Registry),
85        _ => {}
86    }
87    #[cfg(feature = "http-proxy")]
88    {
89        if matches!(
90            format.to_ascii_lowercase().as_str(),
91            "http_proxy" | "http-proxy" | "httpproxy"
92        ) {
93            return Ok(OutputFormat::HTTPProxy);
94        }
95    }
96    Err(InvalidFormatError::Unknown(format.to_string()))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_get_writer_yaml() {
105        assert_eq!(get_writer("yaml"), Ok(OutputFormat::Yaml));
106    }
107
108    #[test]
109    fn test_get_writer_registry() {
110        assert_eq!(get_writer("registry"), Ok(OutputFormat::Registry));
111    }
112
113    #[cfg(feature = "http-proxy")]
114    #[test]
115    fn test_get_writer_http_proxy_variants() {
116        assert_eq!(get_writer("http_proxy"), Ok(OutputFormat::HTTPProxy));
117        assert_eq!(get_writer("http-proxy"), Ok(OutputFormat::HTTPProxy));
118        assert_eq!(get_writer("httpproxy"), Ok(OutputFormat::HTTPProxy));
119    }
120
121    #[test]
122    fn test_get_writer_canonical_formats_are_case_sensitive() {
123        // Canonical "yaml" / "registry" must match exactly to mirror Python and
124        // TypeScript SDKs. Mixed-case input is rejected.
125        assert!(get_writer("YAML").is_err());
126        assert!(get_writer("Yaml").is_err());
127        assert!(get_writer("Registry").is_err());
128        assert!(get_writer("REGISTRY").is_err());
129    }
130
131    #[cfg(feature = "http-proxy")]
132    #[test]
133    fn test_get_writer_case_insensitive_http_proxy() {
134        // Only the http-proxy aliases stay case-insensitive — this is documented
135        // in apcore-toolkit/docs/features/output-writers.md.
136        assert_eq!(get_writer("HTTP_PROXY"), Ok(OutputFormat::HTTPProxy));
137        assert_eq!(get_writer("Http-Proxy"), Ok(OutputFormat::HTTPProxy));
138        assert_eq!(get_writer("HTTPPROXY"), Ok(OutputFormat::HTTPProxy));
139    }
140
141    #[test]
142    fn test_get_writer_unknown() {
143        assert!(get_writer("xml").is_err());
144        assert!(get_writer("").is_err());
145        assert!(get_writer("xml")
146            .unwrap_err()
147            .to_string()
148            .contains("Unknown output format"));
149    }
150
151    #[test]
152    fn test_output_format_serde_roundtrip() {
153        let fmt = OutputFormat::Yaml;
154        let json = serde_json::to_string(&fmt).unwrap();
155        let deserialized: OutputFormat = serde_json::from_str(&json).unwrap();
156        assert_eq!(deserialized, fmt);
157    }
158}