1use 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 #[must_use]
23 pub fn new(
24 name: Option<String>,
25 version: Option<String>,
26 path: PathBuf,
27 relative_path: PathBuf,
28 ) -> Self {
29 Self {
30 path,
31 relative_path,
32 name,
33 version,
34 is_changed: false,
35 dependencies: HashSet::new(),
36 }
37 }
38}
39
40#[async_trait]
41impl Workspace for CSharpWorkspace {
42 fn name(&self) -> Option<&str> {
43 self.name.as_deref()
44 }
45
46 fn path(&self) -> &Path {
47 &self.path
48 }
49
50 fn version(&self) -> Option<&str> {
51 self.version.as_deref()
52 }
53
54 async fn update_version(&mut self, update_type: UpdateType) -> Result<()> {
55 let next_version = next_version(
56 self.version.as_ref().unwrap_or(&String::from("0.0.0")),
57 update_type,
58 )?;
59
60 let csproj_raw = read_to_string(&self.path).await?;
61 let has_version = self.version.is_some();
62
63 let updated_content = update_version_in_xml(&csproj_raw, &next_version, has_version)?;
64
65 write(&self.path, updated_content).await?;
66 self.version = Some(next_version);
67 Ok(())
68 }
69
70 fn language(&self) -> Language {
71 Language::CSharp
72 }
73
74 fn is_changed(&self) -> bool {
75 self.is_changed
76 }
77
78 fn set_changed(&mut self, changed: bool) {
79 self.is_changed = changed;
80 }
81
82 fn relative_path(&self) -> &Path {
83 &self.relative_path
84 }
85
86 fn set_name(&mut self, name: String) {
87 self.name = Some(name);
88 }
89
90 fn default_publish_command(&self) -> String {
91 "dotnet pack -c Release && dotnet nuget push".to_string()
92 }
93
94 fn dependencies(&self) -> &HashSet<String> {
95 &self.dependencies
96 }
97
98 fn add_dependency(&mut self, dependency: &str) {
99 self.dependencies.insert(dependency.to_string());
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use std::fs;
107 use tempfile::TempDir;
108
109 #[tokio::test]
110 async fn test_new_with_name_and_version() {
111 let temp_dir = TempDir::new().unwrap();
112 let csproj_path = temp_dir.path().join("Test.csproj");
113 fs::write(
114 &csproj_path,
115 r#"<Project Sdk="Microsoft.NET.Sdk">
116 <PropertyGroup>
117 <Version>1.0.0</Version>
118 </PropertyGroup>
119</Project>
120"#,
121 )
122 .unwrap();
123
124 let workspace = CSharpWorkspace::new(
125 Some("Test".to_string()),
126 Some("1.0.0".to_string()),
127 csproj_path.clone(),
128 PathBuf::from("Test.csproj"),
129 );
130
131 assert_eq!(workspace.name(), Some("Test"));
132 assert_eq!(workspace.version(), Some("1.0.0"));
133 assert_eq!(workspace.path(), csproj_path);
134 assert_eq!(workspace.relative_path(), PathBuf::from("Test.csproj"));
135 assert!(!workspace.is_changed());
136 assert_eq!(workspace.language(), Language::CSharp);
137 assert_eq!(
138 workspace.default_publish_command(),
139 "dotnet pack -c Release && dotnet nuget push"
140 );
141
142 temp_dir.close().unwrap();
143 }
144
145 #[tokio::test]
146 async fn test_new_without_name_and_version() {
147 let temp_dir = TempDir::new().unwrap();
148 let csproj_path = temp_dir.path().join("Test.csproj");
149 fs::write(
150 &csproj_path,
151 r#"<Project Sdk="Microsoft.NET.Sdk">
152 <PropertyGroup>
153 <OutputType>Exe</OutputType>
154 </PropertyGroup>
155</Project>
156"#,
157 )
158 .unwrap();
159
160 let workspace = CSharpWorkspace::new(
161 None,
162 None,
163 csproj_path.clone(),
164 PathBuf::from("Test.csproj"),
165 );
166
167 assert_eq!(workspace.name(), None);
168 assert_eq!(workspace.version(), None);
169 assert_eq!(workspace.path(), csproj_path);
170 assert!(!workspace.is_changed());
171
172 temp_dir.close().unwrap();
173 }
174
175 #[tokio::test]
176 async fn test_set_changed() {
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 workspace = CSharpWorkspace::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 assert!(!workspace.is_changed());
198 workspace.set_changed(true);
199 assert!(workspace.is_changed());
200 workspace.set_changed(false);
201 assert!(!workspace.is_changed());
202
203 temp_dir.close().unwrap();
204 }
205
206 #[tokio::test]
207 async fn test_update_version_with_existing_version() {
208 let temp_dir = TempDir::new().unwrap();
209 let csproj_path = temp_dir.path().join("Test.csproj");
210 fs::write(
211 &csproj_path,
212 r#"<Project Sdk="Microsoft.NET.Sdk">
213 <PropertyGroup>
214 <Version>1.0.0</Version>
215 </PropertyGroup>
216</Project>
217"#,
218 )
219 .unwrap();
220
221 let mut workspace = CSharpWorkspace::new(
222 Some("Test".to_string()),
223 Some("1.0.0".to_string()),
224 csproj_path.clone(),
225 PathBuf::from("Test.csproj"),
226 );
227
228 workspace.update_version(UpdateType::Patch).await.unwrap();
229
230 let content = fs::read_to_string(&csproj_path).unwrap();
231 assert!(content.contains("<Version>1.0.1</Version>"));
232
233 temp_dir.close().unwrap();
234 }
235
236 #[tokio::test]
237 async fn test_update_version_without_version() {
238 let temp_dir = TempDir::new().unwrap();
239 let csproj_path = temp_dir.path().join("Test.csproj");
240 fs::write(
241 &csproj_path,
242 r#"<Project Sdk="Microsoft.NET.Sdk">
243 <PropertyGroup>
244 <OutputType>Exe</OutputType>
245 </PropertyGroup>
246</Project>
247"#,
248 )
249 .unwrap();
250
251 let mut workspace = CSharpWorkspace::new(
252 Some("Test".to_string()),
253 None,
254 csproj_path.clone(),
255 PathBuf::from("Test.csproj"),
256 );
257
258 workspace.update_version(UpdateType::Patch).await.unwrap();
259
260 let content = fs::read_to_string(&csproj_path).unwrap();
261 assert!(content.contains("<Version>0.0.1</Version>"));
262
263 temp_dir.close().unwrap();
264 }
265
266 #[tokio::test]
267 async fn test_update_version_minor() {
268 let temp_dir = TempDir::new().unwrap();
269 let csproj_path = temp_dir.path().join("Test.csproj");
270 fs::write(
271 &csproj_path,
272 r#"<Project Sdk="Microsoft.NET.Sdk">
273 <PropertyGroup>
274 <Version>1.0.0</Version>
275 </PropertyGroup>
276</Project>
277"#,
278 )
279 .unwrap();
280
281 let mut workspace = CSharpWorkspace::new(
282 Some("Test".to_string()),
283 Some("1.0.0".to_string()),
284 csproj_path.clone(),
285 PathBuf::from("Test.csproj"),
286 );
287
288 workspace.update_version(UpdateType::Minor).await.unwrap();
289
290 let content = fs::read_to_string(&csproj_path).unwrap();
291 assert!(content.contains("<Version>1.1.0</Version>"));
292
293 temp_dir.close().unwrap();
294 }
295
296 #[tokio::test]
297 async fn test_update_version_major() {
298 let temp_dir = TempDir::new().unwrap();
299 let csproj_path = temp_dir.path().join("Test.csproj");
300 fs::write(
301 &csproj_path,
302 r#"<Project Sdk="Microsoft.NET.Sdk">
303 <PropertyGroup>
304 <Version>1.0.0</Version>
305 </PropertyGroup>
306</Project>
307"#,
308 )
309 .unwrap();
310
311 let mut workspace = CSharpWorkspace::new(
312 Some("Test".to_string()),
313 Some("1.0.0".to_string()),
314 csproj_path.clone(),
315 PathBuf::from("Test.csproj"),
316 );
317
318 workspace.update_version(UpdateType::Major).await.unwrap();
319
320 let content = fs::read_to_string(&csproj_path).unwrap();
321 assert!(content.contains("<Version>2.0.0</Version>"));
322
323 temp_dir.close().unwrap();
324 }
325
326 #[test]
327 fn test_dependencies() {
328 let mut workspace = CSharpWorkspace::new(
329 Some("Test".to_string()),
330 Some("1.0.0".to_string()),
331 PathBuf::from("/test/Test.csproj"),
332 PathBuf::from("test/Test.csproj"),
333 );
334
335 assert!(workspace.dependencies().is_empty());
337
338 workspace.add_dependency("Newtonsoft.Json");
340 workspace.add_dependency("CoreLib");
341
342 let deps = workspace.dependencies();
343 assert_eq!(deps.len(), 2);
344 assert!(deps.contains("Newtonsoft.Json"));
345 assert!(deps.contains("CoreLib"));
346
347 workspace.add_dependency("Newtonsoft.Json");
349 assert_eq!(workspace.dependencies().len(), 2);
350 }
351
352 #[test]
353 fn test_set_name() {
354 let mut workspace = CSharpWorkspace::new(
355 None,
356 Some("1.0.0".to_string()),
357 PathBuf::from("/test/Test.csproj"),
358 PathBuf::from("Test.csproj"),
359 );
360 assert_eq!(workspace.name(), None);
361 workspace.set_name("my-project".to_string());
362 assert_eq!(workspace.name(), Some("my-project"));
363 }
364}