changepacks_csharp/
package.rs1use 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 assert!(package.dependencies().is_empty());
305
306 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 package.add_dependency("Newtonsoft.Json");
317 assert_eq!(package.dependencies().len(), 2);
318 }
319}