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}