changepacks_csharp/
workspace.rs1use anyhow::Result;
2use async_trait::async_trait;
3use changepacks_core::{Language, UpdateType, Workspace};
4use changepacks_utils::next_version;
5use std::collections::HashSet;
6use std::path::{Path, PathBuf};
7use tokio::fs::{read_to_string, write};
8
9use crate::xml_utils::update_version_in_xml;
10
11#[derive(Debug)]
12pub struct CSharpWorkspace {
13 path: PathBuf,
14 relative_path: PathBuf,
15 version: Option<String>,
16 name: Option<String>,
17 is_changed: bool,
18 dependencies: HashSet<String>,
19}
20
21impl CSharpWorkspace {
22 pub fn new(
23 name: Option<String>,
24 version: Option<String>,
25 path: PathBuf,
26 relative_path: PathBuf,
27 ) -> Self {
28 Self {
29 path,
30 relative_path,
31 name,
32 version,
33 is_changed: false,
34 dependencies: HashSet::new(),
35 }
36 }
37}
38
39#[async_trait]
40impl Workspace for CSharpWorkspace {
41 fn name(&self) -> Option<&str> {
42 self.name.as_deref()
43 }
44
45 fn path(&self) -> &Path {
46 &self.path
47 }
48
49 fn version(&self) -> Option<&str> {
50 self.version.as_deref()
51 }
52
53 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
54 let next_version = next_version(
55 self.version.as_ref().unwrap_or(&String::from("0.0.0")),
56 update_type,
57 )?;
58
59 let csproj_raw = read_to_string(&self.path).await?;
60 let has_version = self.version.is_some();
61
62 let updated_content = update_version_in_xml(&csproj_raw, &next_version, has_version)?;
63
64 write(&self.path, updated_content).await?;
65 self.version = Some(next_version);
66 Ok(())
67 }
68
69 fn language(&self) -> Language {
70 Language::CSharp
71 }
72
73 fn is_changed(&self) -> bool {
74 self.is_changed
75 }
76
77 fn set_changed(&mut self, changed: bool) {
78 self.is_changed = changed;
79 }
80
81 fn relative_path(&self) -> &Path {
82 &self.relative_path
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_with_name_and_version() {
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 workspace = CSharpWorkspace::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!(workspace.name(), Some("Test"));
127 assert_eq!(workspace.version(), Some("1.0.0"));
128 assert_eq!(workspace.path(), csproj_path);
129 assert_eq!(workspace.relative_path(), PathBuf::from("Test.csproj"));
130 assert!(!workspace.is_changed());
131 assert_eq!(workspace.language(), Language::CSharp);
132 assert_eq!(
133 workspace.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_new_without_name_and_version() {
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 <OutputType>Exe</OutputType>
149 </PropertyGroup>
150</Project>
151"#,
152 )
153 .unwrap();
154
155 let workspace = CSharpWorkspace::new(
156 None,
157 None,
158 csproj_path.clone(),
159 PathBuf::from("Test.csproj"),
160 );
161
162 assert_eq!(workspace.name(), None);
163 assert_eq!(workspace.version(), None);
164 assert_eq!(workspace.path(), csproj_path);
165 assert!(!workspace.is_changed());
166
167 temp_dir.close().unwrap();
168 }
169
170 #[tokio::test]
171 async fn test_set_changed() {
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 workspace = CSharpWorkspace::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 assert!(!workspace.is_changed());
193 workspace.set_changed(true);
194 assert!(workspace.is_changed());
195 workspace.set_changed(false);
196 assert!(!workspace.is_changed());
197
198 temp_dir.close().unwrap();
199 }
200
201 #[tokio::test]
202 async fn test_update_version_with_existing_version() {
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 workspace = CSharpWorkspace::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 workspace.update_version(UpdateType::Patch).await.unwrap();
224
225 let content = fs::read_to_string(&csproj_path).unwrap();
226 assert!(content.contains("<Version>1.0.1</Version>"));
227
228 temp_dir.close().unwrap();
229 }
230
231 #[tokio::test]
232 async fn test_update_version_without_version() {
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 <OutputType>Exe</OutputType>
240 </PropertyGroup>
241</Project>
242"#,
243 )
244 .unwrap();
245
246 let mut workspace = CSharpWorkspace::new(
247 Some("Test".to_string()),
248 None,
249 csproj_path.clone(),
250 PathBuf::from("Test.csproj"),
251 );
252
253 workspace.update_version(UpdateType::Patch).await.unwrap();
254
255 let content = fs::read_to_string(&csproj_path).unwrap();
256 assert!(content.contains("<Version>0.0.1</Version>"));
257
258 temp_dir.close().unwrap();
259 }
260
261 #[tokio::test]
262 async fn test_update_version_minor() {
263 let temp_dir = TempDir::new().unwrap();
264 let csproj_path = temp_dir.path().join("Test.csproj");
265 fs::write(
266 &csproj_path,
267 r#"<Project Sdk="Microsoft.NET.Sdk">
268 <PropertyGroup>
269 <Version>1.0.0</Version>
270 </PropertyGroup>
271</Project>
272"#,
273 )
274 .unwrap();
275
276 let mut workspace = CSharpWorkspace::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 workspace.update_version(UpdateType::Minor).await.unwrap();
284
285 let content = fs::read_to_string(&csproj_path).unwrap();
286 assert!(content.contains("<Version>1.1.0</Version>"));
287
288 temp_dir.close().unwrap();
289 }
290
291 #[tokio::test]
292 async fn test_update_version_major() {
293 let temp_dir = TempDir::new().unwrap();
294 let csproj_path = temp_dir.path().join("Test.csproj");
295 fs::write(
296 &csproj_path,
297 r#"<Project Sdk="Microsoft.NET.Sdk">
298 <PropertyGroup>
299 <Version>1.0.0</Version>
300 </PropertyGroup>
301</Project>
302"#,
303 )
304 .unwrap();
305
306 let mut workspace = CSharpWorkspace::new(
307 Some("Test".to_string()),
308 Some("1.0.0".to_string()),
309 csproj_path.clone(),
310 PathBuf::from("Test.csproj"),
311 );
312
313 workspace.update_version(UpdateType::Major).await.unwrap();
314
315 let content = fs::read_to_string(&csproj_path).unwrap();
316 assert!(content.contains("<Version>2.0.0</Version>"));
317
318 temp_dir.close().unwrap();
319 }
320
321 #[test]
322 fn test_dependencies() {
323 let mut workspace = CSharpWorkspace::new(
324 Some("Test".to_string()),
325 Some("1.0.0".to_string()),
326 PathBuf::from("/test/Test.csproj"),
327 PathBuf::from("test/Test.csproj"),
328 );
329
330 assert!(workspace.dependencies().is_empty());
332
333 workspace.add_dependency("Newtonsoft.Json");
335 workspace.add_dependency("CoreLib");
336
337 let deps = workspace.dependencies();
338 assert_eq!(deps.len(), 2);
339 assert!(deps.contains("Newtonsoft.Json"));
340 assert!(deps.contains("CoreLib"));
341
342 workspace.add_dependency("Newtonsoft.Json");
344 assert_eq!(workspace.dependencies().len(), 2);
345 }
346}