1#![allow(clippy::module_name_repetitions)]
17
18use std::collections::HashMap;
19
20use crate::service::facet::{FacetSetParamsGenerator, RootFacetService};
21
22use skootrs_model::skootrs::{
23 facet::{CommonFacetCreateParams, InitializedFacet, SourceFile},
24 FacetGetParams, FacetMapKey, InitializedProject, InitializedSource, ProjectArchiveParams,
25 ProjectCreateParams, ProjectGetParams, ProjectOutput, ProjectOutputGetParams,
26 ProjectOutputReference, ProjectOutputsListParams, ProjectUpdateParams, SkootError,
27};
28
29use super::{
30 ecosystem::EcosystemService, output::OutputService, repo::RepoService, source::SourceService,
31};
32use tracing::{debug, error, info};
33
34pub trait ProjectService {
36 fn initialize(
42 &self,
43 params: ProjectCreateParams,
44 ) -> impl std::future::Future<Output = Result<InitializedProject, SkootError>> + Send;
45
46 fn get(
52 &self,
53 params: ProjectGetParams,
54 ) -> impl std::future::Future<Output = Result<InitializedProject, SkootError>> + Send;
55
56 fn get_facet_with_content(
62 &self,
63 params: FacetGetParams,
64 ) -> impl std::future::Future<Output = Result<InitializedFacet, SkootError>> + Send;
65
66 fn list_facets(
72 &self,
73 params: ProjectGetParams,
74 ) -> impl std::future::Future<Output = Result<Vec<FacetMapKey>, SkootError>> + Send;
75
76 fn outputs_list(
82 &self,
83 params: ProjectOutputsListParams,
84 ) -> impl std::future::Future<Output = Result<Vec<ProjectOutputReference>, SkootError>> + Send;
85
86 fn output_get(
87 &self,
88 _params: ProjectOutputGetParams,
89 ) -> impl std::future::Future<Output = Result<ProjectOutput, SkootError>> + Send;
90
91 fn update(
92 &self,
93 params: ProjectUpdateParams,
94 ) -> impl std::future::Future<Output = Result<InitializedProject, SkootError>> + Send;
95
96 fn archive(
102 &self,
103 _params: ProjectArchiveParams,
104 ) -> impl std::future::Future<Output = Result<String, SkootError>> + Send;
105}
106
107#[derive(Debug)]
110pub struct LocalProjectService<
111 RS: RepoService,
112 ES: EcosystemService,
113 SS: SourceService,
114 FS: RootFacetService,
115 OS: OutputService,
116> {
117 pub repo_service: RS,
118 pub ecosystem_service: ES,
119 pub source_service: SS,
120 pub facet_service: FS,
121 pub output_service: OS,
122}
123
124impl<RS, ES, SS, FS, OS> ProjectService for LocalProjectService<RS, ES, SS, FS, OS>
125where
126 RS: RepoService + Send + Sync,
127 ES: EcosystemService + Send + Sync,
128 SS: SourceService + Send + Sync,
129 FS: RootFacetService + Send + Sync,
130 OS: OutputService + Send + Sync,
131{
132 async fn initialize(
133 &self,
134 params: ProjectCreateParams,
135 ) -> Result<InitializedProject, SkootError> {
136 debug!("Starting repo initialization");
137 let initialized_repo = self
138 .repo_service
139 .initialize(params.repo_params.clone())
140 .await?;
141 debug!("Starting source initialization");
142 let initialized_source: InitializedSource = self
143 .source_service
144 .initialize(params.source_params.clone(), initialized_repo.clone())?;
145 debug!("Starting ecosystem initialization");
146 let initialized_ecosystem = self
147 .ecosystem_service
148 .initialize(params.ecosystem_params.clone(), initialized_source.clone())?;
149 debug!("Starting facet initialization");
150 let facet_set_params_generator = FacetSetParamsGenerator {};
152 let common_params = CommonFacetCreateParams {
153 project_name: params.name.clone(),
154 source: initialized_source.clone(),
155 repo: initialized_repo.clone(),
156 ecosystem: initialized_ecosystem.clone(),
157 };
158 let source_facet_set_params = facet_set_params_generator
159 .generate_default_source_bundle_facet_params(&common_params)?;
160 let api_facet_set_params =
161 facet_set_params_generator.generate_default_api_bundle(&common_params)?;
162 let initialized_source_facets = self
163 .facet_service
164 .initialize_all(source_facet_set_params)
165 .await?;
166 self.source_service.commit_and_push_changes(
168 initialized_source.clone(),
169 "Initialized project".to_string(),
170 )?;
171 let initialized_api_facets = self
172 .facet_service
173 .initialize_all(api_facet_set_params)
174 .await?;
175 let initialized_facets = [initialized_source_facets, initialized_api_facets]
177 .concat()
178 .into_iter()
179 .map(|f| (FacetMapKey::Type(f.facet_type()), f))
180 .collect::<HashMap<FacetMapKey, InitializedFacet>>();
181
182 info!("Completed project initialization");
183
184 Ok(InitializedProject {
185 repo: initialized_repo,
186 ecosystem: initialized_ecosystem,
187 source: initialized_source,
188 facets: initialized_facets,
189 name: params.name.clone(),
190 })
191 }
192
193 async fn get(&self, params: ProjectGetParams) -> Result<InitializedProject, SkootError> {
194 let get_repo_params = skootrs_model::skootrs::InitializedRepoGetParams {
195 repo_url: params.project_url.clone(),
196 };
197 debug!("Getting repo: {get_repo_params:?}");
198 let repo = self.repo_service.get(get_repo_params).await?;
199 let skootrs_file = self
201 .repo_service
202 .fetch_file_content(&repo, ".skootrs")
203 .await?;
204 debug!("Skootrs file: {skootrs_file}");
205 let initialized_project: InitializedProject = serde_json::from_str(&skootrs_file)?;
206 Ok(initialized_project)
207 }
208
209 async fn get_facet_with_content(
210 &self,
211 params: FacetGetParams,
212 ) -> Result<InitializedFacet, SkootError> {
213 let initialized_project = self.get(params.project_get_params.clone()).await?;
214 let facet = initialized_project
215 .facets
216 .get(¶ms.facet_map_key)
217 .ok_or(SkootError::from("Facet not found"))?;
218
219 match facet {
220 InitializedFacet::SourceBundle(s) => {
221 if let Some(source_files) = s.source_files.clone() {
222 let source_files_content_futures = source_files.into_iter().map(|sf| async {
223 let path = std::path::Path::new(&sf.path).join(&sf.name);
224 let stripped_path = path.strip_prefix("./").unwrap_or(&path);
226
227 let content = self
228 .repo_service
229 .fetch_file_content(&initialized_project.repo, stripped_path)
230 .await;
231 match content {
232 Ok(c) => Ok((sf, c)),
233 Err(e) => {
234 error!(
235 "Error fetching file content for path: {stripped_path:#?}, error: {e}"
236 );
237 Err(e)
238 }
239 }
240 });
241 let source_files_content_results =
242 futures::future::join_all(source_files_content_futures)
243 .await
244 .into_iter()
245 .collect::<Result<Vec<(SourceFile, String)>, SkootError>>()?;
246 let source_files_content_map = source_files_content_results
247 .into_iter()
248 .collect::<HashMap<SourceFile, String>>();
249 Ok(InitializedFacet::SourceBundle(
250 skootrs_model::skootrs::facet::SourceBundleFacet {
251 facet_type: s.facet_type.clone(),
252 source_files: None,
253 source_files_content: Some(source_files_content_map),
254 labels: s.labels.clone(),
255 },
256 ))
257 } else {
258 Err(SkootError::from("No source files found"))
259 }
260 }
261 InitializedFacet::APIBundle(a) => Ok(InitializedFacet::APIBundle(a.clone())),
262 }
263 }
264
265 async fn update(&self, params: ProjectUpdateParams) -> Result<InitializedProject, SkootError> {
267 let initialized_project = params.initialized_project.clone();
268 let initialized_repo = initialized_project.repo;
269 let initialized_source = self.repo_service.clone_local_or_pull(
270 initialized_repo.clone(),
271 initialized_project.source.path.clone(),
272 )?;
273 let initialized_ecosystem = initialized_project.ecosystem;
274
275 let facet_set_params_generator = FacetSetParamsGenerator {};
276 let common_params = CommonFacetCreateParams {
277 project_name: initialized_project.name.clone(),
278 source: initialized_source.clone(),
279 repo: initialized_repo.clone(),
280 ecosystem: initialized_ecosystem.clone(),
281 };
282 let source_facet_set_params = facet_set_params_generator
283 .generate_default_source_bundle_facet_params(&common_params)?;
284 let api_facet_set_params =
285 facet_set_params_generator.generate_default_api_bundle(&common_params)?;
286 let initialized_source_facets = self
287 .facet_service
288 .initialize_all(source_facet_set_params)
289 .await?;
290 self.source_service.commit_and_push_changes(
292 initialized_source.clone(),
293 "Updated facets for project".to_string(),
294 )?;
295 let initialized_api_facets = self
296 .facet_service
297 .initialize_all(api_facet_set_params)
298 .await?;
299 let initialized_facets = [initialized_source_facets, initialized_api_facets]
301 .concat()
302 .into_iter()
303 .map(|f| (FacetMapKey::Type(f.facet_type()), f))
304 .collect::<HashMap<FacetMapKey, InitializedFacet>>();
305
306 Ok(InitializedProject {
307 repo: initialized_repo,
308 ecosystem: initialized_ecosystem,
309 source: initialized_source,
310 facets: initialized_facets,
311 name: initialized_project.name.clone(),
312 })
313 }
314
315 async fn outputs_list(
316 &self,
317 params: ProjectOutputsListParams,
318 ) -> Result<Vec<ProjectOutputReference>, SkootError> {
319 self.output_service.list(params).await
320 }
321
322 async fn list_facets(&self, params: ProjectGetParams) -> Result<Vec<FacetMapKey>, SkootError> {
323 Ok(self.get(params).await?.facets.keys().cloned().collect())
324 }
325
326 async fn output_get(
327 &self,
328 params: ProjectOutputGetParams,
329 ) -> Result<ProjectOutput, SkootError> {
330 self.output_service.get(params).await
331 }
332
333 async fn archive(&self, params: ProjectArchiveParams) -> Result<String, SkootError> {
334 self.repo_service
335 .archive(params.initialized_project.repo)
336 .await
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use std::path::Path;
343
344 use skootrs_model::skootrs::{
345 facet::{
346 APIBundleFacet, APIContent, FacetCreateParams, FacetSetCreateParams, SourceBundleFacet,
347 SupportedFacetType,
348 },
349 label::Label,
350 EcosystemInitializeParams, GithubRepoParams, GithubUser, GoParams, InitializedEcosystem,
351 InitializedGithubRepo, InitializedGo, InitializedMaven, InitializedRepo, ProjectOutputType,
352 RepoCreateParams, SourceInitializeParams,
353 };
354
355 use super::*;
356 struct MockRepoService;
357 struct MockEcosystemService;
358 struct MockSourceService;
359 struct MockFacetService;
360 struct MockOutputService;
361
362 impl RepoService for MockRepoService {
363 async fn initialize(
364 &self,
365 params: RepoCreateParams,
366 ) -> Result<InitializedRepo, SkootError> {
367 let RepoCreateParams::Github(inner_params) = params;
368
369 if inner_params.name == "error" {
371 return Err("Error".into());
372 }
373
374 let initialized_repo = InitializedRepo::Github(InitializedGithubRepo {
375 name: inner_params.name,
376 organization: inner_params.organization,
377 });
378
379 Ok(initialized_repo)
380 }
381
382 fn clone_local(
383 &self,
384 initialized_repo: InitializedRepo,
385 path: String,
386 ) -> Result<InitializedSource, SkootError> {
387 let InitializedRepo::Github(inner_repo) = initialized_repo;
388
389 if inner_repo.name == "error" {
390 return Err("Error".into());
391 }
392
393 let initialized_source = InitializedSource {
394 path: format!("{}/{}", path, inner_repo.name),
395 };
396
397 Ok(initialized_source)
398 }
399
400 fn clone_local_or_pull(
401 &self,
402 initialized_repo: InitializedRepo,
403 path: String,
404 ) -> Result<InitializedSource, SkootError> {
405 self.clone_local(initialized_repo, path)
406 }
407
408 async fn get(
409 &self,
410 params: skootrs_model::skootrs::InitializedRepoGetParams,
411 ) -> Result<InitializedRepo, SkootError> {
412 let repo_url = params.repo_url.clone();
413 if repo_url == "error" {
414 return Err("Error".into());
415 }
416
417 let initialized_repo = InitializedRepo::Github(InitializedGithubRepo {
418 name: "test".to_string(),
419 organization: GithubUser::User("testuser".to_string()),
420 });
421
422 Ok(initialized_repo)
423 }
424
425 async fn fetch_file_content<P: AsRef<std::path::Path> + Send>(
426 &self,
427 _initialized_repo: &InitializedRepo,
428 path: P,
429 ) -> Result<String, SkootError> {
430 if path.as_ref().to_str().unwrap() == "error" {
431 return Err("Error".into());
432 }
433
434 Ok("Worked".to_string())
435 }
436
437 async fn archive(&self, initialized_repo: InitializedRepo) -> Result<String, SkootError> {
438 Ok(initialized_repo.full_url())
439 }
440 }
441
442 impl EcosystemService for MockEcosystemService {
443 fn initialize(
444 &self,
445 params: EcosystemInitializeParams,
446 _source: InitializedSource,
447 ) -> Result<InitializedEcosystem, SkootError> {
448 let initialized_ecosystem = match params {
449 EcosystemInitializeParams::Go(g) => {
450 if g.host == "error" {
451 return Err("Error".into());
452 }
453 InitializedEcosystem::Go(InitializedGo {
454 name: g.name,
455 host: g.host,
456 })
457 }
458 EcosystemInitializeParams::Maven(m) => {
459 if m.group_id == "error" {
460 return Err("Error".into());
461 }
462 InitializedEcosystem::Maven(InitializedMaven {
463 group_id: m.group_id,
464 artifact_id: m.artifact_id,
465 })
466 }
467 };
468
469 Ok(initialized_ecosystem)
470 }
471 }
472
473 impl SourceService for MockSourceService {
474 fn initialize(
475 &self,
476 params: skootrs_model::skootrs::SourceInitializeParams,
477 initialized_repo: InitializedRepo,
478 ) -> Result<InitializedSource, SkootError> {
479 if params.parent_path == "error" {
480 return Err("Error".into());
481 }
482
483 let repo_name = match initialized_repo {
484 InitializedRepo::Github(g) => g.name,
485 };
486
487 let initialized_source = InitializedSource {
488 path: format!("{}/{}", params.parent_path, repo_name),
489 };
490
491 Ok(initialized_source)
492 }
493
494 fn commit_and_push_changes(
495 &self,
496 _source: InitializedSource,
497 message: String,
498 ) -> Result<(), SkootError> {
499 if message == "error" {
500 return Err("Error".into());
501 }
502
503 Ok(())
504 }
505
506 fn write_file<P: AsRef<std::path::Path>, C: AsRef<[u8]>>(
507 &self,
508 _source: InitializedSource,
509 _path: P,
510 name: String,
511 _contents: C,
512 ) -> Result<(), SkootError> {
513 if name == "error" {
514 return Err("Error".into());
515 }
516
517 Ok(())
518 }
519
520 fn read_file<P: AsRef<std::path::Path>>(
521 &self,
522 _source: &InitializedSource,
523 _path: P,
524 name: String,
525 ) -> Result<String, SkootError> {
526 if name == "error" {
527 return Err("Error".into());
528 }
529
530 Ok("Worked".to_string())
531 }
532
533 fn hash_file<P: AsRef<Path>>(
534 &self,
535 _source: &InitializedSource,
536 path: P,
537 _name: String,
538 ) -> Result<String, SkootError> {
539 if path.as_ref().to_str().unwrap() == "error" {
540 return Err("Error".into());
541 }
542
543 Ok("fakehash".to_string())
544 }
545
546 fn pull_updates(&self, source: InitializedSource) -> Result<(), SkootError> {
547 if source.path == "error" {
548 return Err("Error".into());
549 }
550
551 Ok(())
552 }
553 }
554
555 impl RootFacetService for MockFacetService {
556 async fn initialize(
557 &self,
558 params: FacetCreateParams,
559 ) -> Result<InitializedFacet, SkootError> {
560 match params {
561 FacetCreateParams::SourceBundle(s) => {
562 if s.common.project_name == "error" {
563 return Err("Error".into());
564 }
565 let source_bundle_facet = SourceBundleFacet {
566 source_files: Some(vec![SourceFile {
567 name: "README.md".to_string(),
568 path: "./".to_string(),
569 hash: "fakehash".to_string(),
570 }]),
571 facet_type: SupportedFacetType::Readme,
572 source_files_content: None,
573 labels: vec![Label::Custom("test".to_string())],
574 };
575
576 Ok(InitializedFacet::SourceBundle(source_bundle_facet))
577 }
578 FacetCreateParams::APIBundle(a) => {
579 if a.common.project_name == "error" {
580 return Err("Error".into());
581 }
582 let api_bundle_facet = APIBundleFacet {
583 apis: vec![APIContent {
584 name: "test".to_string(),
585 url: "https://foo.bar/test".to_string(),
586 response: "worked".to_string(),
587 }],
588 facet_type: SupportedFacetType::BranchProtection,
589 labels: vec![Label::Custom("test".to_string())],
590 };
591
592 Ok(InitializedFacet::APIBundle(api_bundle_facet))
593 }
594 }
595 }
596
597 async fn initialize_all(
598 &self,
599 params: FacetSetCreateParams,
600 ) -> Result<Vec<InitializedFacet>, SkootError> {
601 let mut initialized_facets = Vec::new();
602 for facet_params in params.facets_params {
603 let initialized_facet = self.initialize(facet_params).await?;
604 initialized_facets.push(initialized_facet);
605 }
606
607 Ok(initialized_facets)
608 }
609 }
610
611 impl OutputService for MockOutputService {
612 async fn list(
613 &self,
614 _params: ProjectOutputsListParams,
615 ) -> Result<Vec<ProjectOutputReference>, SkootError> {
616 Ok(vec![ProjectOutputReference {
617 name: "test".into(),
618 output_type: ProjectOutputType::SBOM,
619 labels: vec![Label::Custom("test".to_string())],
620 }])
621 }
622
623 async fn get(
624 &self,
625 _params: skootrs_model::skootrs::ProjectOutputGetParams,
626 ) -> Result<skootrs_model::skootrs::ProjectOutput, SkootError> {
627 Ok(skootrs_model::skootrs::ProjectOutput {
628 reference: ProjectOutputReference {
629 name: "test".into(),
630 output_type: ProjectOutputType::SBOM,
631 labels: vec![Label::Custom("test".to_string())],
632 },
633 output: "test".into(),
634 })
635 }
636 }
637
638 #[tokio::test]
639 async fn test_initialize_project() {
640 let project_params = ProjectCreateParams {
641 name: "test".to_string(),
642 repo_params: RepoCreateParams::Github(GithubRepoParams {
643 name: "test".to_string(),
644 description: "foobar".to_string(),
645 organization: GithubUser::User("testuser".to_string()),
646 }),
647 ecosystem_params: EcosystemInitializeParams::Go(GoParams {
648 name: "test".to_string(),
649 host: "github.com".to_string(),
650 }),
651 source_params: SourceInitializeParams {
652 parent_path: "test".to_string(),
653 },
654 };
655
656 let local_project_service = LocalProjectService {
657 repo_service: MockRepoService,
658 ecosystem_service: MockEcosystemService,
659 source_service: MockSourceService,
660 facet_service: MockFacetService,
661 output_service: MockOutputService,
662 };
663
664 let result = local_project_service.initialize(project_params).await;
665
666 assert!(result.is_ok());
667 let initialized_project = result.unwrap();
668
669 assert!(initialized_project.repo.full_url() == "https://github.com/testuser/test");
670 let module = match initialized_project.ecosystem {
671 InitializedEcosystem::Go(g) => g,
672 _ => panic!("Wrong ecosystem type"),
673 };
674 assert!(module.name == "test");
675 assert!(initialized_project.source.path == "test/test");
676 println!("{:#?}", initialized_project.facets);
677
678 assert_eq!(initialized_project.facets.len(), 2);
682 }
683}