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