changepacks_csharp/
workspace.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, UpdateType, Workspace};
4use changepacks_utils::next_version;
5use std::collections::HashSet;
6use std::path::{Path, PathBuf};
7use tokio::fs::{read_to_string, write};
8
9use crate::xml_utils::update_version_in_xml;
10
11#[derive(Debug)]
12pub struct CSharpWorkspace {
13    path: PathBuf,
14    relative_path: PathBuf,
15    version: Option<String>,
16    name: Option<String>,
17    is_changed: bool,
18    dependencies: HashSet<String>,
19}
20
21impl CSharpWorkspace {
22    pub fn new(
23        name: Option<String>,
24        version: Option<String>,
25        path: PathBuf,
26        relative_path: PathBuf,
27    ) -> Self {
28        Self {
29            path,
30            relative_path,
31            name,
32            version,
33            is_changed: false,
34            dependencies: HashSet::new(),
35        }
36    }
37}
38
39#[async_trait]
40impl Workspace for CSharpWorkspace {
41    fn name(&self) -> Option<&str> {
42        self.name.as_deref()
43    }
44
45    fn path(&self) -> &Path {
46        &self.path
47    }
48
49    fn version(&self) -> Option<&str> {
50        self.version.as_deref()
51    }
52
53    async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
54        let next_version = next_version(
55            self.version.as_ref().unwrap_or(&String::from("0.0.0")),
56            update_type,
57        )?;
58
59        let csproj_raw = read_to_string(&self.path).await?;
60        let has_version = self.version.is_some();
61
62        let updated_content = update_version_in_xml(&csproj_raw, &next_version, has_version)?;
63
64        write(&self.path, updated_content).await?;
65        self.version = Some(next_version);
66        Ok(())
67    }
68
69    fn language(&self) -> Language {
70        Language::CSharp
71    }
72
73    fn is_changed(&self) -> bool {
74        self.is_changed
75    }
76
77    fn set_changed(&mut self, changed: bool) {
78        self.is_changed = changed;
79    }
80
81    fn relative_path(&self) -> &Path {
82        &self.relative_path
83    }
84
85    fn default_publish_command(&self) -> String {
86        "dotnet pack -c Release && dotnet nuget push".to_string()
87    }
88
89    fn dependencies(&self) -> &HashSet<String> {
90        &self.dependencies
91    }
92
93    fn add_dependency(&mut self, dependency: &str) {
94        self.dependencies.insert(dependency.to_string());
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use std::fs;
102    use tempfile::TempDir;
103
104    #[tokio::test]
105    async fn test_new_with_name_and_version() {
106        let temp_dir = TempDir::new().unwrap();
107        let csproj_path = temp_dir.path().join("Test.csproj");
108        fs::write(
109            &csproj_path,
110            r#"<Project Sdk="Microsoft.NET.Sdk">
111  <PropertyGroup>
112    <Version>1.0.0</Version>
113  </PropertyGroup>
114</Project>
115"#,
116        )
117        .unwrap();
118
119        let workspace = CSharpWorkspace::new(
120            Some("Test".to_string()),
121            Some("1.0.0".to_string()),
122            csproj_path.clone(),
123            PathBuf::from("Test.csproj"),
124        );
125
126        assert_eq!(workspace.name(), Some("Test"));
127        assert_eq!(workspace.version(), Some("1.0.0"));
128        assert_eq!(workspace.path(), csproj_path);
129        assert_eq!(workspace.relative_path(), PathBuf::from("Test.csproj"));
130        assert!(!workspace.is_changed());
131        assert_eq!(workspace.language(), Language::CSharp);
132        assert_eq!(
133            workspace.default_publish_command(),
134            "dotnet pack -c Release && dotnet nuget push"
135        );
136
137        temp_dir.close().unwrap();
138    }
139
140    #[tokio::test]
141    async fn test_new_without_name_and_version() {
142        let temp_dir = TempDir::new().unwrap();
143        let csproj_path = temp_dir.path().join("Test.csproj");
144        fs::write(
145            &csproj_path,
146            r#"<Project Sdk="Microsoft.NET.Sdk">
147  <PropertyGroup>
148    <OutputType>Exe</OutputType>
149  </PropertyGroup>
150</Project>
151"#,
152        )
153        .unwrap();
154
155        let workspace = CSharpWorkspace::new(
156            None,
157            None,
158            csproj_path.clone(),
159            PathBuf::from("Test.csproj"),
160        );
161
162        assert_eq!(workspace.name(), None);
163        assert_eq!(workspace.version(), None);
164        assert_eq!(workspace.path(), csproj_path);
165        assert!(!workspace.is_changed());
166
167        temp_dir.close().unwrap();
168    }
169
170    #[tokio::test]
171    async fn test_set_changed() {
172        let temp_dir = TempDir::new().unwrap();
173        let csproj_path = temp_dir.path().join("Test.csproj");
174        fs::write(
175            &csproj_path,
176            r#"<Project Sdk="Microsoft.NET.Sdk">
177  <PropertyGroup>
178    <Version>1.0.0</Version>
179  </PropertyGroup>
180</Project>
181"#,
182        )
183        .unwrap();
184
185        let mut workspace = CSharpWorkspace::new(
186            Some("Test".to_string()),
187            Some("1.0.0".to_string()),
188            csproj_path.clone(),
189            PathBuf::from("Test.csproj"),
190        );
191
192        assert!(!workspace.is_changed());
193        workspace.set_changed(true);
194        assert!(workspace.is_changed());
195        workspace.set_changed(false);
196        assert!(!workspace.is_changed());
197
198        temp_dir.close().unwrap();
199    }
200
201    #[tokio::test]
202    async fn test_update_version_with_existing_version() {
203        let temp_dir = TempDir::new().unwrap();
204        let csproj_path = temp_dir.path().join("Test.csproj");
205        fs::write(
206            &csproj_path,
207            r#"<Project Sdk="Microsoft.NET.Sdk">
208  <PropertyGroup>
209    <Version>1.0.0</Version>
210  </PropertyGroup>
211</Project>
212"#,
213        )
214        .unwrap();
215
216        let mut workspace = CSharpWorkspace::new(
217            Some("Test".to_string()),
218            Some("1.0.0".to_string()),
219            csproj_path.clone(),
220            PathBuf::from("Test.csproj"),
221        );
222
223        workspace.update_version(UpdateType::Patch).await.unwrap();
224
225        let content = fs::read_to_string(&csproj_path).unwrap();
226        assert!(content.contains("<Version>1.0.1</Version>"));
227
228        temp_dir.close().unwrap();
229    }
230
231    #[tokio::test]
232    async fn test_update_version_without_version() {
233        let temp_dir = TempDir::new().unwrap();
234        let csproj_path = temp_dir.path().join("Test.csproj");
235        fs::write(
236            &csproj_path,
237            r#"<Project Sdk="Microsoft.NET.Sdk">
238  <PropertyGroup>
239    <OutputType>Exe</OutputType>
240  </PropertyGroup>
241</Project>
242"#,
243        )
244        .unwrap();
245
246        let mut workspace = CSharpWorkspace::new(
247            Some("Test".to_string()),
248            None,
249            csproj_path.clone(),
250            PathBuf::from("Test.csproj"),
251        );
252
253        workspace.update_version(UpdateType::Patch).await.unwrap();
254
255        let content = fs::read_to_string(&csproj_path).unwrap();
256        assert!(content.contains("<Version>0.0.1</Version>"));
257
258        temp_dir.close().unwrap();
259    }
260
261    #[tokio::test]
262    async fn test_update_version_minor() {
263        let temp_dir = TempDir::new().unwrap();
264        let csproj_path = temp_dir.path().join("Test.csproj");
265        fs::write(
266            &csproj_path,
267            r#"<Project Sdk="Microsoft.NET.Sdk">
268  <PropertyGroup>
269    <Version>1.0.0</Version>
270  </PropertyGroup>
271</Project>
272"#,
273        )
274        .unwrap();
275
276        let mut workspace = CSharpWorkspace::new(
277            Some("Test".to_string()),
278            Some("1.0.0".to_string()),
279            csproj_path.clone(),
280            PathBuf::from("Test.csproj"),
281        );
282
283        workspace.update_version(UpdateType::Minor).await.unwrap();
284
285        let content = fs::read_to_string(&csproj_path).unwrap();
286        assert!(content.contains("<Version>1.1.0</Version>"));
287
288        temp_dir.close().unwrap();
289    }
290
291    #[tokio::test]
292    async fn test_update_version_major() {
293        let temp_dir = TempDir::new().unwrap();
294        let csproj_path = temp_dir.path().join("Test.csproj");
295        fs::write(
296            &csproj_path,
297            r#"<Project Sdk="Microsoft.NET.Sdk">
298  <PropertyGroup>
299    <Version>1.0.0</Version>
300  </PropertyGroup>
301</Project>
302"#,
303        )
304        .unwrap();
305
306        let mut workspace = CSharpWorkspace::new(
307            Some("Test".to_string()),
308            Some("1.0.0".to_string()),
309            csproj_path.clone(),
310            PathBuf::from("Test.csproj"),
311        );
312
313        workspace.update_version(UpdateType::Major).await.unwrap();
314
315        let content = fs::read_to_string(&csproj_path).unwrap();
316        assert!(content.contains("<Version>2.0.0</Version>"));
317
318        temp_dir.close().unwrap();
319    }
320
321    #[test]
322    fn test_dependencies() {
323        let mut workspace = CSharpWorkspace::new(
324            Some("Test".to_string()),
325            Some("1.0.0".to_string()),
326            PathBuf::from("/test/Test.csproj"),
327            PathBuf::from("test/Test.csproj"),
328        );
329
330        // Initially empty
331        assert!(workspace.dependencies().is_empty());
332
333        // Add dependencies
334        workspace.add_dependency("Newtonsoft.Json");
335        workspace.add_dependency("CoreLib");
336
337        let deps = workspace.dependencies();
338        assert_eq!(deps.len(), 2);
339        assert!(deps.contains("Newtonsoft.Json"));
340        assert!(deps.contains("CoreLib"));
341
342        // Adding duplicate should not increase count
343        workspace.add_dependency("Newtonsoft.Json");
344        assert_eq!(workspace.dependencies().len(), 2);
345    }
346}