1pub mod facet;
17pub mod label;
18
19use std::{collections::HashMap, error::Error, fmt, str::FromStr};
20
21use serde::{Deserialize, Serialize};
22use strum::{Display, EnumString, VariantNames};
23use url::Host;
24use utoipa::ToSchema;
25
26use self::{
27 facet::{InitializedFacet, SupportedFacetType},
28 label::Label,
29};
30
31pub type SkootError = Box<dyn Error + Send + Sync>;
33
34pub const SUPPORTED_ECOSYSTEMS: [&str; 2] = ["Go", "Maven"];
45
46#[derive(Serialize, Deserialize, Clone, Debug, EnumString, VariantNames, Default)]
48#[cfg_attr(feature = "openapi", derive(ToSchema))]
49pub enum SupportedEcosystems {
50 #[default]
52 Go,
53 }
58
59#[derive(Serialize, Deserialize, Clone, Debug)]
64#[cfg_attr(feature = "openapi", derive(ToSchema))]
65pub struct InitializedProject {
66 pub repo: InitializedRepo,
68 pub ecosystem: InitializedEcosystem,
70 pub source: InitializedSource,
72 pub facets: HashMap<FacetMapKey, InitializedFacet>,
74 pub name: String,
77}
78
79#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
81#[cfg_attr(feature = "openapi", derive(ToSchema))]
82#[serde(try_from = "String", into = "String")]
83pub enum FacetMapKey {
84 Name(String),
86 Type(SupportedFacetType),
88}
89
90impl TryFrom<String> for FacetMapKey {
91 type Error = SkootError;
92
93 fn try_from(value: String) -> Result<Self, Self::Error> {
94 let parts: Vec<&str> = value.split(": ").collect();
95 if parts.len() != 2 {
96 return Err("Invalid facet map key".into());
97 }
98 match parts.first() {
99 Some(&"Name") => Ok(Self::Name(parts[1].to_string())),
100 Some(&"Type") => Ok(Self::Type(parts[1].parse()?)),
101 _ => Err("Invalid facet map key".into()),
102 }
103 }
104}
105
106impl From<FacetMapKey> for String {
107 fn from(val: FacetMapKey) -> Self {
108 match val {
109 FacetMapKey::Name(x) => format!("Name: {x}"),
110 FacetMapKey::Type(x) => format!("Type: {x}"),
111 }
112 }
113}
114
115impl FromStr for FacetMapKey {
116 type Err = SkootError;
117
118 fn from_str(s: &str) -> Result<Self, Self::Err> {
119 let parts: Vec<&str> = s.split(": ").collect();
120 if parts.len() != 2 {
121 return Err("Invalid facet map key".into());
122 }
123 match parts.first() {
124 Some(&"Name") => Ok(Self::Name(parts[1].to_string())),
125 Some(&"Type") => Ok(Self::Type(parts[1].parse()?)),
126 _ => Err("Invalid facet map key".into()),
127 }
128 }
129}
130
131impl fmt::Display for FacetMapKey {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 String::from(self.clone()).fmt(f)?;
136 Ok(())
137 }
138}
139
140#[derive(Serialize, Deserialize, Clone, Debug)]
142#[cfg_attr(feature = "openapi", derive(ToSchema))]
143pub struct ProjectCreateParams {
144 pub name: String,
146 pub repo_params: RepoCreateParams,
148 pub ecosystem_params: EcosystemInitializeParams,
150 pub source_params: SourceInitializeParams,
152}
153
154#[derive(Serialize, Deserialize, Clone, Debug)]
156#[cfg_attr(feature = "openapi", derive(ToSchema))]
157pub struct ProjectUpdateParams {
158 pub initialized_project: InitializedProject,
160}
161
162#[derive(Serialize, Deserialize, Clone, Debug)]
164#[cfg_attr(feature = "openapi", derive(ToSchema))]
165pub struct ProjectGetParams {
166 pub project_url: String,
168}
169
170#[derive(Serialize, Deserialize, Clone, Debug)]
172#[cfg_attr(feature = "openapi", derive(ToSchema))]
173pub struct ProjectOutputsListParams {
174 pub initialized_project: InitializedProject,
176 pub release: ProjectReleaseParam,
178}
179
180#[derive(Serialize, Deserialize, Clone, Debug)]
182#[cfg_attr(feature = "openapi", derive(ToSchema))]
183pub enum ProjectReleaseParam {
184 Tag(String),
186 Latest,
188}
189
190impl ProjectReleaseParam {
191 #[must_use]
193 pub fn tag(&self) -> Option<String> {
194 match self {
195 Self::Tag(x) => Some(x.to_string()),
196 Self::Latest => None,
197 }
198 }
199}
200
201#[derive(Serialize, Deserialize, Clone, Debug)]
203#[cfg_attr(feature = "openapi", derive(ToSchema))]
204pub struct ProjectOutputGetParams {
205 pub initialized_project: InitializedProject,
207 pub project_output_type: ProjectOutputType,
209 pub project_output: String,
212 pub release: ProjectReleaseParam,
214}
215
216#[derive(Serialize, Deserialize, Clone, Debug)]
218#[cfg_attr(feature = "openapi", derive(ToSchema))]
219pub struct ProjectArchiveParams {
220 pub initialized_project: InitializedProject,
222}
223
224#[derive(Serialize, Deserialize, Clone, Debug, EnumString, VariantNames, Default, Display)]
226#[cfg_attr(feature = "openapi", derive(ToSchema))]
227pub enum ProjectOutputType {
228 #[default]
229 SBOM,
231 InToto,
233 Unknown(String),
235 Custom(String),
237}
238
239#[derive(Serialize, Deserialize, Clone, Debug)]
241#[cfg_attr(feature = "openapi", derive(ToSchema))]
242pub struct ProjectOutput {
243 pub reference: ProjectOutputReference,
245 pub output: String,
247}
248
249#[derive(Serialize, Deserialize, Clone, Debug)]
251#[cfg_attr(feature = "openapi", derive(ToSchema))]
252pub struct ProjectOutputReference {
253 pub output_type: ProjectOutputType,
255 pub name: String,
257 pub labels: Vec<Label>,
259}
260
261#[derive(Serialize, Deserialize, Clone, Debug)]
263#[cfg_attr(feature = "openapi", derive(ToSchema))]
264pub struct FacetGetParams {
265 pub project_get_params: ProjectGetParams,
267 pub facet_map_key: FacetMapKey,
269}
270
271#[derive(Serialize, Deserialize, Clone, Debug)]
273#[cfg_attr(feature = "openapi", derive(ToSchema))]
274pub enum InitializedRepo {
275 Github(InitializedGithubRepo),
277}
278
279impl InitializedRepo {
280 #[must_use]
282 pub fn host_url(&self) -> String {
283 match self {
284 Self::Github(x) => x.host_url(),
285 }
286 }
287
288 #[must_use]
290 pub fn full_url(&self) -> String {
291 match self {
292 Self::Github(x) => x.full_url(),
293 }
294 }
295}
296
297impl TryFrom<String> for InitializedRepo {
298 type Error = SkootError;
299
300 fn try_from(value: String) -> Result<Self, Self::Error> {
301 let parts = url::Url::parse(&value)?;
302 let path_segments = parts
303 .path_segments()
304 .map_or(Vec::new(), Iterator::collect::<Vec<_>>);
305 if path_segments.len() != 2 {
306 return Err(format!("Invalid repo URL: {value}").into());
307 }
308
309 let organization = *path_segments
310 .first()
311 .ok_or(format!("Invalid repo URL: {value}"))?;
312 let name = *path_segments
313 .get(1)
314 .ok_or(format!("Invalid repo URL: {value}"))?;
315 match parts.host() {
316 Some(Host::Domain("github.com")) => {
317 Ok(Self::Github(InitializedGithubRepo {
318 name: name.to_string(),
319 organization: GithubUser::User(organization.into()),
321 }))
322 }
323 _ => Err("Unsupported repo host".into()),
324 }
325 }
326}
327
328#[derive(Serialize, Deserialize, Clone, Debug)]
330#[cfg_attr(feature = "openapi", derive(ToSchema))]
331pub struct InitializedGithubRepo {
332 pub name: String,
334 pub organization: GithubUser,
336}
337
338impl InitializedGithubRepo {
339 #[must_use]
341 pub fn host_url(&self) -> String {
342 "https://github.com".into()
343 }
344
345 #[must_use]
347 pub fn full_url(&self) -> String {
348 format!(
349 "{}/{}/{}",
350 self.host_url(),
351 self.organization.get_name(),
352 self.name
353 )
354 }
355}
356
357#[derive(Serialize, Deserialize, Clone, Debug)]
360#[cfg_attr(feature = "openapi", derive(ToSchema))]
361pub enum InitializedEcosystem {
362 Go(InitializedGo),
364 Maven(InitializedMaven),
366}
367
368#[derive(Serialize, Deserialize, Clone, Debug)]
370#[cfg_attr(feature = "openapi", derive(ToSchema))]
371pub enum RepoCreateParams {
372 Github(GithubRepoParams),
374}
375
376#[derive(Serialize, Deserialize, Clone, Debug)]
378#[cfg_attr(feature = "openapi", derive(ToSchema))]
379pub enum EcosystemInitializeParams {
380 Go(GoParams),
382 Maven(MavenParams),
384}
385
386#[derive(Serialize, Deserialize, Clone, Debug)]
388pub struct InitializedRepoGetParams {
389 pub repo_url: String,
391}
392
393#[derive(Serialize, Deserialize, Clone, Debug)]
397#[cfg_attr(feature = "openapi", derive(ToSchema))]
398pub enum GithubUser {
399 User(String),
401 Organization(String),
403}
404
405impl GithubUser {
406 #[must_use]
408 pub fn get_name(&self) -> String {
409 match self {
410 Self::User(x) | Self::Organization(x) => x.to_string(),
411 }
412 }
413}
414
415#[derive(Serialize, Deserialize, Clone, Debug)]
417#[cfg_attr(feature = "openapi", derive(ToSchema))]
418pub struct GithubRepoParams {
419 pub name: String,
421 pub description: String,
423 pub organization: GithubUser,
425}
426
427impl GithubRepoParams {
428 #[must_use]
430 pub fn host_url(&self) -> String {
431 "https://github.com".into()
432 }
433
434 #[must_use]
436 pub fn full_url(&self) -> String {
437 format!(
438 "{}/{}/{}",
439 self.host_url(),
440 self.organization.get_name(),
441 self.name
442 )
443 }
444}
445
446#[derive(Serialize, Deserialize, Clone, Debug)]
448#[cfg_attr(feature = "openapi", derive(ToSchema))]
449pub struct SourceInitializeParams {
450 pub parent_path: String,
452}
453
454impl SourceInitializeParams {
455 #[must_use]
457 pub fn path(&self, name: &str) -> String {
458 format!("{}/{}", self.parent_path, name)
459 }
460}
461
462#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
464pub struct InitializedSource {
465 pub path: String,
467}
468
469#[derive(Serialize, Deserialize, Clone, Debug)]
471#[cfg_attr(feature = "openapi", derive(ToSchema))]
472pub struct MavenParams {
473 pub group_id: String,
475 pub artifact_id: String,
477}
478
479#[derive(Serialize, Deserialize, Clone, Debug)]
481#[cfg_attr(feature = "openapi", derive(ToSchema))]
482pub struct GoParams {
483 pub name: String,
485 pub host: String,
487}
488
489#[derive(Serialize, Deserialize, Clone, Debug)]
491#[cfg_attr(feature = "openapi", derive(ToSchema))]
492pub struct InitializedGo {
493 pub name: String,
495 pub host: String,
497}
498
499impl InitializedGo {
500 #[must_use]
502 pub fn module(&self) -> String {
503 format!("{}/{}", self.host, self.name)
504 }
505}
506
507#[derive(Serialize, Deserialize, Clone, Debug)]
509#[cfg_attr(feature = "openapi", derive(ToSchema))]
510pub struct InitializedMaven {
511 pub group_id: String,
513 pub artifact_id: String,
515}
516
517impl GoParams {
518 #[must_use]
520 pub fn module(&self) -> String {
521 format!("{}/{}", self.host, self.name)
522 }
523}
524
525#[derive(Serialize, Deserialize, Clone, Debug)]
527#[cfg_attr(feature = "openapi", derive(ToSchema))]
528pub struct Config {
529 pub local_project_path: String,
531}
532
533impl Default for Config {
534 fn default() -> Self {
535 Self {
536 local_project_path: "/tmp".into(),
537 }
538 }
539}
540
541#[cfg(test)]
542mod tests {
543 #![allow(clippy::unwrap_used)]
544 use super::*;
545 #[test]
546 fn test_initialized_repo_try_from() {
547 let repo: InitializedRepo =
548 InitializedRepo::try_from("https://github.com/kusaridev/skootrs".to_string()).unwrap();
549 assert_eq!(repo.host_url(), "https://github.com");
550 assert_eq!(repo.full_url(), "https://github.com/kusaridev/skootrs");
551 }
552}