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 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 assert!(package.dependencies().is_empty());
309
310 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 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}