Skip to main content

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    #[must_use]
24    pub fn new(
25        name: Option<String>,
26        version: Option<String>,
27        path: PathBuf,
28        relative_path: PathBuf,
29    ) -> Self {
30        Self {
31            name,
32            version,
33            path,
34            relative_path,
35            is_changed: false,
36            dependencies: HashSet::new(),
37        }
38    }
39}
40
41#[async_trait]
42impl Package for CSharpPackage {
43    fn name(&self) -> Option<&str> {
44        self.name.as_deref()
45    }
46
47    fn version(&self) -> Option<&str> {
48        self.version.as_deref()
49    }
50
51    fn path(&self) -> &Path {
52        &self.path
53    }
54
55    fn relative_path(&self) -> &Path {
56        &self.relative_path
57    }
58
59    async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
60        let current_version = self.version.as_deref().unwrap_or("0.0.0");
61        let new_version = next_version(current_version, update_type)?;
62
63        let csproj_raw = read_to_string(&self.path).await?;
64        let has_version = self.version.is_some();
65
66        let updated_content = update_version_in_xml(&csproj_raw, &new_version, has_version)?;
67
68        write(&self.path, updated_content).await?;
69        self.version = Some(new_version);
70        Ok(())
71    }
72
73    fn language(&self) -> Language {
74        Language::CSharp
75    }
76
77    fn is_changed(&self) -> bool {
78        self.is_changed
79    }
80
81    fn set_changed(&mut self, changed: bool) {
82        self.is_changed = changed;
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() {
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 package = CSharpPackage::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!(package.name(), Some("Test"));
127        assert_eq!(package.version(), Some("1.0.0"));
128        assert_eq!(package.path(), csproj_path);
129        assert_eq!(package.relative_path(), PathBuf::from("Test.csproj"));
130        assert!(!package.is_changed());
131        assert_eq!(package.language(), Language::CSharp);
132        assert_eq!(
133            package.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_set_changed() {
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    <Version>1.0.0</Version>
149  </PropertyGroup>
150</Project>
151"#,
152        )
153        .unwrap();
154
155        let mut package = CSharpPackage::new(
156            Some("Test".to_string()),
157            Some("1.0.0".to_string()),
158            csproj_path.clone(),
159            PathBuf::from("Test.csproj"),
160        );
161
162        assert!(!package.is_changed());
163        package.set_changed(true);
164        assert!(package.is_changed());
165        package.set_changed(false);
166        assert!(!package.is_changed());
167
168        temp_dir.close().unwrap();
169    }
170
171    #[tokio::test]
172    async fn test_update_version_patch() {
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 package = CSharpPackage::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        package.update_version(UpdateType::Patch).await.unwrap();
194
195        let content = fs::read_to_string(&csproj_path).unwrap();
196        assert!(content.contains("<Version>1.0.1</Version>"));
197
198        temp_dir.close().unwrap();
199    }
200
201    #[tokio::test]
202    async fn test_update_version_minor() {
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 package = CSharpPackage::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        package.update_version(UpdateType::Minor).await.unwrap();
224
225        let content = fs::read_to_string(&csproj_path).unwrap();
226        assert!(content.contains("<Version>1.1.0</Version>"));
227
228        temp_dir.close().unwrap();
229    }
230
231    #[tokio::test]
232    async fn test_update_version_major() {
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    <Version>1.0.0</Version>
240  </PropertyGroup>
241</Project>
242"#,
243        )
244        .unwrap();
245
246        let mut package = CSharpPackage::new(
247            Some("Test".to_string()),
248            Some("1.0.0".to_string()),
249            csproj_path.clone(),
250            PathBuf::from("Test.csproj"),
251        );
252
253        package.update_version(UpdateType::Major).await.unwrap();
254
255        let content = fs::read_to_string(&csproj_path).unwrap();
256        assert!(content.contains("<Version>2.0.0</Version>"));
257
258        temp_dir.close().unwrap();
259    }
260
261    #[tokio::test]
262    async fn test_update_version_preserves_other_elements() {
263        let temp_dir = TempDir::new().unwrap();
264        let csproj_path = temp_dir.path().join("Test.csproj");
265        let original_content = r#"<Project Sdk="Microsoft.NET.Sdk">
266  <PropertyGroup>
267    <OutputType>Exe</OutputType>
268    <TargetFramework>net8.0</TargetFramework>
269    <Version>1.0.0</Version>
270    <PackageId>MyPackage</PackageId>
271  </PropertyGroup>
272</Project>
273"#;
274        fs::write(&csproj_path, original_content).unwrap();
275
276        let mut package = CSharpPackage::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        package.update_version(UpdateType::Patch).await.unwrap();
284
285        let content = fs::read_to_string(&csproj_path).unwrap();
286        assert!(content.contains("<Version>1.0.1</Version>"));
287        assert!(content.contains("<OutputType>Exe</OutputType>"));
288        assert!(content.contains("<TargetFramework>net8.0</TargetFramework>"));
289        assert!(content.contains("<PackageId>MyPackage</PackageId>"));
290
291        temp_dir.close().unwrap();
292    }
293
294    #[test]
295    fn test_dependencies() {
296        let mut package = CSharpPackage::new(
297            Some("Test".to_string()),
298            Some("1.0.0".to_string()),
299            PathBuf::from("/test/Test.csproj"),
300            PathBuf::from("test/Test.csproj"),
301        );
302
303        // Initially empty
304        assert!(package.dependencies().is_empty());
305
306        // Add dependencies
307        package.add_dependency("Newtonsoft.Json");
308        package.add_dependency("CoreLib");
309
310        let deps = package.dependencies();
311        assert_eq!(deps.len(), 2);
312        assert!(deps.contains("Newtonsoft.Json"));
313        assert!(deps.contains("CoreLib"));
314
315        // Adding duplicate should not increase count
316        package.add_dependency("Newtonsoft.Json");
317        assert_eq!(package.dependencies().len(), 2);
318    }
319}