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