Skip to main content

citum_engine/api/
style_input.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus
4*/
5
6//! Style resolution input type for interactive APIs.
7
8use citum_schema::Style;
9use serde::{Deserialize, Serialize};
10
11/// A style reference that can be resolved locally or by an external resolver.
12///
13/// This union type allows callers to supply a style by local path, inline YAML,
14/// or as an identifier that requires a remote resolver. Only `Path` and `Yaml`
15/// can be resolved locally; `Id` and `Uri` require the citum-server resolver chain.
16#[derive(Debug, Clone, Deserialize, Serialize)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[serde(tag = "kind", content = "value", rename_all = "lowercase")]
19pub enum StyleInput {
20    /// A style identifier to be resolved from registries (builtin, store, remote).
21    /// Requires external resolver chain — local resolution returns UnresolvedInput error.
22    Id(String),
23    /// A remote URI to be resolved (requires HTTP access).
24    /// Requires external resolver chain — local resolution returns UnresolvedInput error.
25    Uri(String),
26    /// A local filesystem path to a YAML style file.
27    Path(String),
28    /// Inline YAML style definition.
29    Yaml(String),
30}
31
32impl StyleInput {
33    /// Resolve the style locally from Path or Yaml variants.
34    ///
35    /// This method handles local filesystem paths and inline YAML content.
36    /// For `Id` and `Uri` variants, which require a resolver chain, this returns
37    /// an UnresolvedInput error.
38    ///
39    /// # Errors
40    ///
41    /// Returns `FormatDocumentError::UnresolvedInput` for `Id` and `Uri` variants.
42    /// Returns `FormatDocumentError::StylePath` for filesystem errors.
43    /// Returns `FormatDocumentError::StyleParse` for YAML parsing errors.
44    pub fn resolve_local(&self) -> Result<Style, crate::api::FormatDocumentError> {
45        match self {
46            StyleInput::Path(path) => {
47                let yaml_bytes = std::fs::read(path).map_err(|e| {
48                    crate::api::FormatDocumentError::StylePath(format!(
49                        "Failed to read style from '{}': {}",
50                        path, e
51                    ))
52                })?;
53                Style::from_yaml_bytes(&yaml_bytes).map_err(|e| {
54                    crate::api::FormatDocumentError::StyleParse(format!(
55                        "Failed to parse style from '{}': {}",
56                        path, e
57                    ))
58                })
59            }
60            StyleInput::Yaml(yaml_str) => {
61                Style::from_yaml_bytes(yaml_str.as_bytes()).map_err(|e| {
62                    crate::api::FormatDocumentError::StyleParse(format!(
63                        "Failed to parse inline YAML style: {}",
64                        e
65                    ))
66                })
67            }
68            StyleInput::Id(id) => Err(crate::api::FormatDocumentError::UnresolvedInput(format!(
69                "Style ID '{}' requires resolver chain (not available in engine)",
70                id
71            ))),
72            StyleInput::Uri(uri) => Err(crate::api::FormatDocumentError::UnresolvedInput(format!(
73                "Style URI '{}' requires resolver chain (not available in engine)",
74                uri
75            ))),
76        }
77    }
78}
79
80#[cfg(test)]
81#[allow(
82    clippy::unwrap_used,
83    clippy::expect_used,
84    clippy::panic,
85    reason = "test code uses assertions and panic"
86)]
87mod tests {
88    use super::*;
89    use std::io::Write;
90    use tempfile::NamedTempFile;
91
92    #[test]
93    fn style_input_yaml_resolves_locally() {
94        let yaml_content = r#"---
95info:
96  title: Test Style
97  default-locale: en-us
98"#;
99        let input = StyleInput::Yaml(yaml_content.to_string());
100        let result = input.resolve_local();
101        assert!(result.is_ok());
102    }
103
104    #[test]
105    fn style_input_id_returns_unresolved_error() {
106        let input = StyleInput::Id("apa-7th".to_string());
107        let result = input.resolve_local();
108        match result {
109            Err(crate::api::FormatDocumentError::UnresolvedInput(msg)) => {
110                assert!(msg.contains("apa-7th"));
111            }
112            _ => panic!("Expected UnresolvedInput error"),
113        }
114    }
115
116    #[test]
117    fn style_input_uri_returns_unresolved_error() {
118        let input = StyleInput::Uri("https://example.com/style.yaml".to_string());
119        let result = input.resolve_local();
120        match result {
121            Err(crate::api::FormatDocumentError::UnresolvedInput(msg)) => {
122                assert!(msg.contains("https://example.com/style.yaml"));
123            }
124            _ => panic!("Expected UnresolvedInput error"),
125        }
126    }
127
128    #[test]
129    fn style_input_path_reads_and_parses() {
130        let mut tmp = NamedTempFile::new().expect("Failed to create temp file");
131        let yaml_content = r#"---
132info:
133  title: Test Style
134  default-locale: en-us
135"#;
136        tmp.write_all(yaml_content.as_bytes())
137            .expect("Failed to write temp file");
138        tmp.flush().expect("Failed to flush temp file");
139
140        let input = StyleInput::Path(tmp.path().to_string_lossy().to_string());
141        let result = input.resolve_local();
142        assert!(result.is_ok());
143    }
144
145    #[test]
146    fn style_input_path_missing_returns_error() {
147        let input = StyleInput::Path("/nonexistent/path/style.yaml".to_string());
148        let result = input.resolve_local();
149        match result {
150            Err(crate::api::FormatDocumentError::StylePath(msg)) => {
151                assert!(msg.contains("Failed to read"));
152            }
153            _ => panic!("Expected StylePath error"),
154        }
155    }
156
157    #[test]
158    fn style_input_invalid_yaml_returns_parse_error() {
159        let input = StyleInput::Yaml("{ invalid yaml: [".to_string());
160        let result = input.resolve_local();
161        match result {
162            Err(crate::api::FormatDocumentError::StyleParse(msg)) => {
163                assert!(msg.contains("Failed to parse"));
164            }
165            _ => panic!("Expected StyleParse error"),
166        }
167    }
168}