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 default_publish_command(&self) -> String {
87        "dotnet pack -c Release && dotnet nuget push".to_string()
88    }
89
90    fn dependencies(&self) -> &HashSet<String> {
91        &self.dependencies
92    }
93
94    fn add_dependency(&mut self, dependency: &str) {
95        self.dependencies.insert(dependency.to_string());
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use std::fs;
103    use tempfile::TempDir;
104
105    #[tokio::test]
106    async fn test_new_with_name_and_version() {
107        let temp_dir = TempDir::new().unwrap();
108        let csproj_path = temp_dir.path().join("Test.csproj");
109        fs::write(
110            &csproj_path,
111            r#"<Project Sdk="Microsoft.NET.Sdk">
112  <PropertyGroup>
113    <Version>1.0.0</Version>
114  </PropertyGroup>
115</Project>
116"#,
117        )
118        .unwrap();
119
120        let workspace = CSharpWorkspace::new(
121            Some("Test".to_string()),
122            Some("1.0.0".to_string()),
123            csproj_path.clone(),
124            PathBuf::from("Test.csproj"),
125        );
126
127        assert_eq!(workspace.name(), Some("Test"));
128        assert_eq!(workspace.version(), Some("1.0.0"));
129        assert_eq!(workspace.path(), csproj_path);
130        assert_eq!(workspace.relative_path(), PathBuf::from("Test.csproj"));
131        assert!(!workspace.is_changed());
132        assert_eq!(workspace.language(), Language::CSharp);
133        assert_eq!(
134            workspace.default_publish_command(),
135            "dotnet pack -c Release && dotnet nuget push"
136        );
137
138        temp_dir.close().unwrap();
139    }
140
141    #[tokio::test]
142    async fn test_new_without_name_and_version() {
143        let temp_dir = TempDir::new().unwrap();
144        let csproj_path = temp_dir.path().join("Test.csproj");
145        fs::write(
146            &csproj_path,
147            r#"<Project Sdk="Microsoft.NET.Sdk">
148  <PropertyGroup>
149    <OutputType>Exe</OutputType>
150  </PropertyGroup>
151</Project>
152"#,
153        )
154        .unwrap();
155
156        let workspace = CSharpWorkspace::new(
157            None,
158            None,
159            csproj_path.clone(),
160            PathBuf::from("Test.csproj"),
161        );
162
163        assert_eq!(workspace.name(), None);
164        assert_eq!(workspace.version(), None);
165        assert_eq!(workspace.path(), csproj_path);
166        assert!(!workspace.is_changed());
167
168        temp_dir.close().unwrap();
169    }
170
171    #[tokio::test]
172    async fn test_set_changed() {
173        let temp_dir = TempDir::new().unwrap();
174        let csproj_path = temp_dir.path().join("Test.csproj");
175        fs::write(
176            &csproj_path,
177            r#"<Project Sdk="Microsoft.NET.Sdk">
178  <PropertyGroup>
179    <Version>1.0.0</Version>
180  </PropertyGroup>
181</Project>
182"#,
183        )
184        .unwrap();
185
186        let mut workspace = CSharpWorkspace::new(
187            Some("Test".to_string()),
188            Some("1.0.0".to_string()),
189            csproj_path.clone(),
190            PathBuf::from("Test.csproj"),
191        );
192
193        assert!(!workspace.is_changed());
194        workspace.set_changed(true);
195        assert!(workspace.is_changed());
196        workspace.set_changed(false);
197        assert!(!workspace.is_changed());
198
199        temp_dir.close().unwrap();
200    }
201
202    #[tokio::test]
203    async fn test_update_version_with_existing_version() {
204        let temp_dir = TempDir::new().unwrap();
205        let csproj_path = temp_dir.path().join("Test.csproj");
206        fs::write(
207            &csproj_path,
208            r#"<Project Sdk="Microsoft.NET.Sdk">
209  <PropertyGroup>
210    <Version>1.0.0</Version>
211  </PropertyGroup>
212</Project>
213"#,
214        )
215        .unwrap();
216
217        let mut workspace = CSharpWorkspace::new(
218            Some("Test".to_string()),
219            Some("1.0.0".to_string()),
220            csproj_path.clone(),
221            PathBuf::from("Test.csproj"),
222        );
223
224        workspace.update_version(UpdateType::Patch).await.unwrap();
225
226        let content = fs::read_to_string(&csproj_path).unwrap();
227        assert!(content.contains("<Version>1.0.1</Version>"));
228
229        temp_dir.close().unwrap();
230    }
231
232    #[tokio::test]
233    async fn test_update_version_without_version() {
234        let temp_dir = TempDir::new().unwrap();
235        let csproj_path = temp_dir.path().join("Test.csproj");
236        fs::write(
237            &csproj_path,
238            r#"<Project Sdk="Microsoft.NET.Sdk">
239  <PropertyGroup>
240    <OutputType>Exe</OutputType>
241  </PropertyGroup>
242</Project>
243"#,
244        )
245        .unwrap();
246
247        let mut workspace = CSharpWorkspace::new(
248            Some("Test".to_string()),
249            None,
250            csproj_path.clone(),
251            PathBuf::from("Test.csproj"),
252        );
253
254        workspace.update_version(UpdateType::Patch).await.unwrap();
255
256        let content = fs::read_to_string(&csproj_path).unwrap();
257        assert!(content.contains("<Version>0.0.1</Version>"));
258
259        temp_dir.close().unwrap();
260    }
261
262    #[tokio::test]
263    async fn test_update_version_minor() {
264        let temp_dir = TempDir::new().unwrap();
265        let csproj_path = temp_dir.path().join("Test.csproj");
266        fs::write(
267            &csproj_path,
268            r#"<Project Sdk="Microsoft.NET.Sdk">
269  <PropertyGroup>
270    <Version>1.0.0</Version>
271  </PropertyGroup>
272</Project>
273"#,
274        )
275        .unwrap();
276
277        let mut workspace = CSharpWorkspace::new(
278            Some("Test".to_string()),
279            Some("1.0.0".to_string()),
280            csproj_path.clone(),
281            PathBuf::from("Test.csproj"),
282        );
283
284        workspace.update_version(UpdateType::Minor).await.unwrap();
285
286        let content = fs::read_to_string(&csproj_path).unwrap();
287        assert!(content.contains("<Version>1.1.0</Version>"));
288
289        temp_dir.close().unwrap();
290    }
291
292    #[tokio::test]
293    async fn test_update_version_major() {
294        let temp_dir = TempDir::new().unwrap();
295        let csproj_path = temp_dir.path().join("Test.csproj");
296        fs::write(
297            &csproj_path,
298            r#"<Project Sdk="Microsoft.NET.Sdk">
299  <PropertyGroup>
300    <Version>1.0.0</Version>
301  </PropertyGroup>
302</Project>
303"#,
304        )
305        .unwrap();
306
307        let mut workspace = CSharpWorkspace::new(
308            Some("Test".to_string()),
309            Some("1.0.0".to_string()),
310            csproj_path.clone(),
311            PathBuf::from("Test.csproj"),
312        );
313
314        workspace.update_version(UpdateType::Major).await.unwrap();
315
316        let content = fs::read_to_string(&csproj_path).unwrap();
317        assert!(content.contains("<Version>2.0.0</Version>"));
318
319        temp_dir.close().unwrap();
320    }
321
322    #[test]
323    fn test_dependencies() {
324        let mut workspace = CSharpWorkspace::new(
325            Some("Test".to_string()),
326            Some("1.0.0".to_string()),
327            PathBuf::from("/test/Test.csproj"),
328            PathBuf::from("test/Test.csproj"),
329        );
330
331        // Initially empty
332        assert!(workspace.dependencies().is_empty());
333
334        // Add dependencies
335        workspace.add_dependency("Newtonsoft.Json");
336        workspace.add_dependency("CoreLib");
337
338        let deps = workspace.dependencies();
339        assert_eq!(deps.len(), 2);
340        assert!(deps.contains("Newtonsoft.Json"));
341        assert!(deps.contains("CoreLib"));
342
343        // Adding duplicate should not increase count
344        workspace.add_dependency("Newtonsoft.Json");
345        assert_eq!(workspace.dependencies().len(), 2);
346    }
347}