Skip to main content

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