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}