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}