clawspec_core/split/
fragment.rs

1//! Fragment types for split OpenAPI specifications.
2
3use std::path::PathBuf;
4
5use serde::Serialize;
6use utoipa::openapi::OpenApi;
7
8/// A fragment extracted from an OpenAPI specification.
9///
10/// Represents a piece of the original specification that should be written to a separate file.
11/// The content can be any serializable type, typically [`Components`](utoipa::openapi::Components),
12/// [`OpenApi`], or a custom subset of schemas.
13///
14/// # Type Parameters
15///
16/// * `T` - The type of content in this fragment. Must implement [`Serialize`] for file output.
17///
18/// # Example
19///
20/// ```rust,ignore
21/// use clawspec_core::split::Fragment;
22/// use std::path::PathBuf;
23/// use utoipa::openapi::Components;
24///
25/// let fragment = Fragment {
26///     path: PathBuf::from("schemas/common.yaml"),
27///     content: Components::new(),
28/// };
29/// ```
30#[derive(Debug, Clone)]
31pub struct Fragment<T: Serialize> {
32    /// Relative path where this fragment should be written.
33    ///
34    /// This path is relative to the main OpenAPI specification file.
35    /// The main spec will use `$ref` pointing to this path.
36    pub path: PathBuf,
37
38    /// The content to serialize into the fragment file.
39    pub content: T,
40}
41
42impl<T: Serialize> Fragment<T> {
43    /// Creates a new fragment with the given path and content.
44    pub fn new(path: impl Into<PathBuf>, content: T) -> Self {
45        Self {
46            path: path.into(),
47            content,
48        }
49    }
50
51    /// Serializes the fragment content to a YAML string.
52    ///
53    /// *Requires the `yaml` feature.*
54    ///
55    /// # Example
56    ///
57    /// ```rust,ignore
58    /// use clawspec_core::split::Fragment;
59    ///
60    /// let fragment = Fragment::new("common.yaml", components);
61    /// let yaml = fragment.to_yaml()?;
62    /// std::fs::write(&fragment.path, yaml)?;
63    /// ```
64    ///
65    /// # Errors
66    ///
67    /// Returns a [`YamlError`](crate::YamlError) if serialization fails.
68    #[cfg(feature = "yaml")]
69    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
70    pub fn to_yaml(&self) -> Result<String, crate::YamlError> {
71        crate::ToYaml::to_yaml(&self.content)
72    }
73}
74
75/// The result of splitting an OpenAPI specification.
76///
77/// Contains the main specification (with `$ref` references to external files)
78/// and a collection of fragments to be written to separate files.
79///
80/// # Type Parameters
81///
82/// * `T` - The type of content in the fragments. Must implement [`Serialize`].
83///
84/// # Example
85///
86/// ```rust,ignore
87/// use clawspec_core::split::{OpenApiSplitter, SplitSchemasByTag, SplitResult};
88///
89/// let splitter = SplitSchemasByTag::new("common.yaml");
90/// let result: SplitResult<_> = splitter.split(spec);
91///
92/// // Write fragments to files
93/// for fragment in &result.fragments {
94///     let yaml = serde_yaml::to_string(&fragment.content)?;
95///     std::fs::write(&fragment.path, yaml)?;
96/// }
97///
98/// // Write main spec
99/// let main_yaml = serde_yaml::to_string(&result.main)?;
100/// std::fs::write("openapi.yaml", main_yaml)?;
101/// ```
102#[derive(Debug, Clone)]
103pub struct SplitResult<T: Serialize> {
104    /// The main OpenAPI specification with `$ref` references to extracted fragments.
105    pub main: OpenApi,
106
107    /// Extracted fragments to be written to separate files.
108    pub fragments: Vec<Fragment<T>>,
109}
110
111impl<T: Serialize> SplitResult<T> {
112    /// Creates a new split result with no fragments.
113    pub fn new(main: OpenApi) -> Self {
114        Self {
115            main,
116            fragments: Vec::new(),
117        }
118    }
119
120    /// Adds a fragment to the result.
121    pub fn add_fragment(&mut self, fragment: Fragment<T>) {
122        self.fragments.push(fragment);
123    }
124
125    /// Returns `true` if there are no fragments (no splitting occurred).
126    pub fn is_unsplit(&self) -> bool {
127        self.fragments.is_empty()
128    }
129
130    /// Returns the number of fragments.
131    pub fn fragment_count(&self) -> usize {
132        self.fragments.len()
133    }
134
135    /// Serializes the main OpenAPI specification to a YAML string.
136    ///
137    /// *Requires the `yaml` feature.*
138    ///
139    /// # Example
140    ///
141    /// ```rust,ignore
142    /// use clawspec_core::split::{OpenApiSplitter, SplitSchemasByTag};
143    ///
144    /// let result = splitter.split(spec);
145    /// let main_yaml = result.main_to_yaml()?;
146    /// std::fs::write("openapi.yaml", main_yaml)?;
147    /// ```
148    ///
149    /// # Errors
150    ///
151    /// Returns a [`YamlError`](crate::YamlError) if serialization fails.
152    #[cfg(feature = "yaml")]
153    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
154    pub fn main_to_yaml(&self) -> Result<String, crate::YamlError> {
155        crate::ToYaml::to_yaml(&self.main)
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use utoipa::openapi::{Components, OpenApiBuilder};
163
164    #[test]
165    fn should_create_fragment() {
166        let components = Components::new();
167        let fragment = Fragment::new("schemas/common.yaml", components);
168
169        assert_eq!(fragment.path, PathBuf::from("schemas/common.yaml"));
170    }
171
172    #[test]
173    fn should_create_split_result() {
174        let spec = OpenApiBuilder::new().build();
175        let result: SplitResult<Components> = SplitResult::new(spec);
176
177        assert!(result.is_unsplit());
178        assert_eq!(result.fragment_count(), 0);
179    }
180
181    #[test]
182    fn should_add_fragments() {
183        let spec = OpenApiBuilder::new().build();
184        let mut result: SplitResult<Components> = SplitResult::new(spec);
185
186        result.add_fragment(Fragment::new("common.yaml", Components::new()));
187        result.add_fragment(Fragment::new("errors.yaml", Components::new()));
188
189        assert!(!result.is_unsplit());
190        assert_eq!(result.fragment_count(), 2);
191    }
192}
193
194#[cfg(all(test, feature = "yaml"))]
195mod yaml_tests {
196    use super::*;
197    use utoipa::openapi::{Components, InfoBuilder, OpenApiBuilder};
198
199    #[test]
200    fn should_serialize_fragment_to_yaml() {
201        let components = Components::new();
202        let fragment = Fragment::new("schemas/common.yaml", components);
203
204        let yaml = fragment.to_yaml().expect("should serialize to YAML");
205
206        // serde_saphyr serializes empty objects without braces
207        assert!(yaml.trim().is_empty() || yaml.trim() == "{}");
208    }
209
210    #[test]
211    fn should_serialize_main_spec_to_yaml() {
212        let spec = OpenApiBuilder::new()
213            .info(
214                InfoBuilder::new()
215                    .title("Test API")
216                    .version("1.0.0")
217                    .build(),
218            )
219            .build();
220        let result: SplitResult<Components> = SplitResult::new(spec);
221
222        let yaml = result.main_to_yaml().expect("should serialize to YAML");
223
224        // Verify the essential structure is present
225        assert!(yaml.contains("openapi: 3.1.0"));
226        assert!(yaml.contains("title: Test API"));
227        assert!(yaml.contains("version: 1.0.0"));
228        assert!(yaml.contains("paths"));
229    }
230
231    #[test]
232    fn should_serialize_split_result_with_fragments() {
233        let spec = OpenApiBuilder::new()
234            .info(
235                InfoBuilder::new()
236                    .title("Split API")
237                    .version("2.0.0")
238                    .build(),
239            )
240            .build();
241        let mut result: SplitResult<Components> = SplitResult::new(spec);
242        result.add_fragment(Fragment::new("common.yaml", Components::new()));
243
244        let main_yaml = result
245            .main_to_yaml()
246            .expect("should serialize main to YAML");
247        let fragment_yaml = result.fragments[0]
248            .to_yaml()
249            .expect("should serialize fragment to YAML");
250
251        // Verify the essential structure is present
252        assert!(main_yaml.contains("openapi: 3.1.0"));
253        assert!(main_yaml.contains("title: Split API"));
254        assert!(main_yaml.contains("version: 2.0.0"));
255        assert!(main_yaml.contains("paths"));
256
257        // serde_saphyr serializes empty objects without braces
258        assert!(fragment_yaml.trim().is_empty() || fragment_yaml.trim() == "{}");
259    }
260}