mockforge_plugin_sdk/
builders.rs

1//! Builder patterns for plugin manifests and configurations
2//!
3//! This module provides fluent builder APIs for creating plugin manifests,
4//! making it easier to configure plugins without dealing with raw structs.
5
6use mockforge_plugin_core::*;
7
8/// Builder for plugin manifests
9///
10/// # Example
11///
12/// ```rust
13/// use mockforge_plugin_sdk::builders::ManifestBuilder;
14///
15/// let manifest = ManifestBuilder::new("my-plugin", "1.0.0")
16///     .name("My Plugin")
17///     .description("A custom plugin for authentication")
18///     .author("Your Name", "your.email@example.com")
19///     .capability("network")
20///     .capability("filesystem.read")
21///     .build();
22/// ```
23pub struct ManifestBuilder {
24    manifest: PluginManifest,
25}
26
27impl ManifestBuilder {
28    /// Create a new manifest builder
29    pub fn new(id: &str, version: &str) -> Self {
30        let version = PluginVersion::parse(version).unwrap_or_else(|_| PluginVersion::new(0, 1, 0));
31        let info = PluginInfo {
32            id: PluginId::new(id),
33            version,
34            name: String::new(),
35            description: String::new(),
36            author: PluginAuthor::new("Unknown"),
37        };
38
39        Self {
40            manifest: PluginManifest::new(info),
41        }
42    }
43
44    /// Set plugin name
45    pub fn name(mut self, name: &str) -> Self {
46        self.manifest.info.name = name.to_string();
47        self
48    }
49
50    /// Set plugin description
51    pub fn description(mut self, description: &str) -> Self {
52        self.manifest.info.description = description.to_string();
53        self
54    }
55
56    /// Set plugin author
57    pub fn author(mut self, name: &str, email: &str) -> Self {
58        self.manifest.info.author = PluginAuthor::with_email(name, email);
59        self
60    }
61
62    /// Set plugin author (name only)
63    pub fn author_name(mut self, name: &str) -> Self {
64        self.manifest.info.author = PluginAuthor::new(name);
65        self
66    }
67
68    /// Add a capability
69    ///
70    /// Common capabilities: "network", "filesystem.read", "filesystem.write"
71    pub fn capability(mut self, capability: &str) -> Self {
72        self.manifest.capabilities.push(capability.to_string());
73        self
74    }
75
76    /// Add multiple capabilities
77    pub fn capabilities(mut self, capabilities: &[&str]) -> Self {
78        for cap in capabilities {
79            self.manifest.capabilities.push(cap.to_string());
80        }
81        self
82    }
83
84    /// Add a dependency
85    pub fn dependency(mut self, plugin_id: &str, version: &str) -> Self {
86        if let Ok(parsed_version) = PluginVersion::parse(version) {
87            self.manifest.dependencies.insert(PluginId::new(plugin_id), parsed_version);
88        }
89        self
90    }
91
92    /// Build the manifest
93    pub fn build(self) -> PluginManifest {
94        self.manifest
95    }
96
97    /// Build and save to file
98    pub fn build_and_save(self, path: &str) -> std::result::Result<PluginManifest, std::io::Error> {
99        let manifest = self.manifest;
100        let yaml = serde_yaml::to_string(&manifest)
101            .map_err(|e| std::io::Error::other(format!("YAML error: {}", e)))?;
102        std::fs::write(path, yaml)?;
103        Ok(manifest)
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_manifest_builder() {
113        let manifest = ManifestBuilder::new("test-plugin", "1.0.0")
114            .name("Test Plugin")
115            .description("A test plugin")
116            .author("Test Author", "test@example.com")
117            .capability("network")
118            .capability("filesystem.read")
119            .build();
120
121        assert_eq!(manifest.info.id, PluginId::new("test-plugin"));
122        assert_eq!(manifest.info.name, "Test Plugin");
123        assert_eq!(manifest.info.description, "A test plugin");
124        assert_eq!(manifest.capabilities.len(), 2);
125        assert!(manifest.capabilities.contains(&"network".to_string()));
126        assert!(manifest.capabilities.contains(&"filesystem.read".to_string()));
127    }
128
129    #[test]
130    fn test_manifest_with_dependencies() {
131        let manifest = ManifestBuilder::new("test-plugin", "2.0.0")
132            .name("Test Plugin")
133            .dependency("dep1", "1.0.0")
134            .dependency("dep2", "1.5.0")
135            .build();
136
137        assert_eq!(manifest.dependencies.len(), 2);
138    }
139
140    #[test]
141    fn test_manifest_save() {
142        use tempfile::NamedTempFile;
143
144        let manifest = ManifestBuilder::new("test-plugin", "1.0.0")
145            .name("Test Plugin")
146            .description("A test plugin")
147            .author("Test", "test@example.com")
148            .build();
149
150        let temp_file = NamedTempFile::new().unwrap();
151        let path = temp_file.path().to_str().unwrap();
152
153        let yaml = serde_yaml::to_string(&manifest).unwrap();
154        std::fs::write(path, yaml).unwrap();
155
156        let loaded = PluginManifest::from_file(path).unwrap();
157        assert_eq!(loaded.info.id, manifest.info.id);
158        assert_eq!(loaded.info.name, manifest.info.name);
159    }
160
161    #[test]
162    fn test_manifest_builder_new() {
163        let manifest = ManifestBuilder::new("my-plugin", "1.2.3").build();
164        assert_eq!(manifest.info.id.as_str(), "my-plugin");
165        assert_eq!(manifest.info.version.major, 1);
166        assert_eq!(manifest.info.version.minor, 2);
167        assert_eq!(manifest.info.version.patch, 3);
168    }
169
170    #[test]
171    fn test_manifest_builder_invalid_version() {
172        // Invalid version defaults to 0.1.0
173        let manifest = ManifestBuilder::new("my-plugin", "invalid").build();
174        assert_eq!(manifest.info.version.major, 0);
175        assert_eq!(manifest.info.version.minor, 1);
176        assert_eq!(manifest.info.version.patch, 0);
177    }
178
179    #[test]
180    fn test_manifest_builder_default_author() {
181        let manifest = ManifestBuilder::new("my-plugin", "1.0.0").build();
182        assert_eq!(manifest.info.author.name, "Unknown");
183    }
184
185    #[test]
186    fn test_manifest_builder_author_name() {
187        let manifest = ManifestBuilder::new("test", "1.0.0").author_name("John Doe").build();
188        assert_eq!(manifest.info.author.name, "John Doe");
189    }
190
191    #[test]
192    fn test_manifest_builder_author_with_email() {
193        let manifest = ManifestBuilder::new("test", "1.0.0")
194            .author("Jane Doe", "jane@example.com")
195            .build();
196        assert_eq!(manifest.info.author.name, "Jane Doe");
197        assert_eq!(manifest.info.author.email, Some("jane@example.com".to_string()));
198    }
199
200    #[test]
201    fn test_manifest_builder_capabilities_batch() {
202        let manifest = ManifestBuilder::new("test", "1.0.0")
203            .capabilities(&["network", "filesystem.read", "filesystem.write"])
204            .build();
205        assert_eq!(manifest.capabilities.len(), 3);
206        assert!(manifest.capabilities.contains(&"network".to_string()));
207        assert!(manifest.capabilities.contains(&"filesystem.read".to_string()));
208        assert!(manifest.capabilities.contains(&"filesystem.write".to_string()));
209    }
210
211    #[test]
212    fn test_manifest_builder_empty_capabilities() {
213        let manifest = ManifestBuilder::new("test", "1.0.0").capabilities(&[]).build();
214        assert!(manifest.capabilities.is_empty());
215    }
216
217    #[test]
218    fn test_manifest_builder_dependency_invalid_version() {
219        // Invalid dependency version is ignored
220        let manifest = ManifestBuilder::new("test", "1.0.0")
221            .dependency("dep1", "invalid-version")
222            .build();
223        assert!(manifest.dependencies.is_empty());
224    }
225
226    #[test]
227    fn test_manifest_builder_chaining() {
228        let manifest = ManifestBuilder::new("chain-test", "1.0.0")
229            .name("Chain Test Plugin")
230            .description("Testing method chaining")
231            .author_name("Test Author")
232            .capability("network")
233            .capability("database")
234            .dependency("base-plugin", "2.0.0")
235            .build();
236
237        assert_eq!(manifest.info.name, "Chain Test Plugin");
238        assert_eq!(manifest.info.description, "Testing method chaining");
239        assert_eq!(manifest.info.author.name, "Test Author");
240        assert_eq!(manifest.capabilities.len(), 2);
241        assert_eq!(manifest.dependencies.len(), 1);
242    }
243
244    #[test]
245    fn test_manifest_builder_overwrite_name() {
246        let manifest = ManifestBuilder::new("test", "1.0.0")
247            .name("First Name")
248            .name("Second Name")
249            .build();
250        assert_eq!(manifest.info.name, "Second Name");
251    }
252
253    #[test]
254    fn test_manifest_builder_overwrite_description() {
255        let manifest = ManifestBuilder::new("test", "1.0.0")
256            .description("First description")
257            .description("Second description")
258            .build();
259        assert_eq!(manifest.info.description, "Second description");
260    }
261
262    #[test]
263    fn test_manifest_builder_overwrite_author() {
264        let manifest = ManifestBuilder::new("test", "1.0.0")
265            .author("First Author", "first@example.com")
266            .author("Second Author", "second@example.com")
267            .build();
268        assert_eq!(manifest.info.author.name, "Second Author");
269        assert_eq!(manifest.info.author.email, Some("second@example.com".to_string()));
270    }
271
272    #[test]
273    fn test_build_and_save() {
274        use tempfile::NamedTempFile;
275
276        let temp_file = NamedTempFile::new().unwrap();
277        let path = temp_file.path().to_str().unwrap();
278
279        let result = ManifestBuilder::new("test-plugin", "1.0.0")
280            .name("Test Plugin")
281            .description("A test plugin")
282            .author("Test", "test@example.com")
283            .build_and_save(path);
284
285        assert!(result.is_ok());
286        let manifest = result.unwrap();
287        assert_eq!(manifest.info.name, "Test Plugin");
288
289        // Verify file was written
290        let content = std::fs::read_to_string(path).unwrap();
291        assert!(content.contains("test-plugin"));
292        assert!(content.contains("Test Plugin"));
293    }
294
295    #[test]
296    fn test_build_and_save_invalid_path() {
297        let result =
298            ManifestBuilder::new("test", "1.0.0").build_and_save("/nonexistent/path/manifest.yaml");
299        assert!(result.is_err());
300    }
301
302    #[test]
303    fn test_manifest_builder_empty_strings() {
304        let manifest = ManifestBuilder::new("", "").name("").description("").build();
305        assert_eq!(manifest.info.id.as_str(), "");
306        assert_eq!(manifest.info.name, "");
307        assert_eq!(manifest.info.description, "");
308    }
309
310    #[test]
311    fn test_manifest_builder_special_characters() {
312        let manifest = ManifestBuilder::new("plugin-with-dashes", "1.0.0")
313            .name("Plugin with Special Characters: !@#$%")
314            .description("Description with\nnewlines and\ttabs")
315            .build();
316        assert_eq!(manifest.info.id.as_str(), "plugin-with-dashes");
317        assert!(manifest.info.name.contains("Special Characters"));
318        assert!(manifest.info.description.contains("\n"));
319    }
320}