changepacks_csharp/
package.rs

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