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