skootrs_model/skootrs/
mod.rs

1//
2// Copyright 2024 The Skootrs Authors.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16pub 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
31/// A helper type for the error type used throughout Skootrs. This is a `Box<dyn Error + Send + Sync>`.
32pub type SkootError = Box<dyn Error + Send + Sync>;
33
34/// The general structure of the models here is the struct names take the form:
35/// `<Thing>Params` reflecting the parameters for something to be created or initilized, like the parameters
36/// to create a repo or project.
37///
38/// `Initialized<Thing>` models the data and state for a created or initialized thing, like a repo created inside of Github.
39/// This module is purely focused on the data for skootrs, and not for performing any of the operations. In order to make
40/// it easy for (de)serialization, the structs and impls only contain the logic for the data, and not for the operations,
41/// which falls under service.
42// TODO: These categories of structs should be moved to their own modules.
43/// Consts for the supported ecosystems, repos, etc. for convenient use by things like the CLI.
44pub const SUPPORTED_ECOSYSTEMS: [&str; 2] = ["Go", "Maven"];
45
46/// The set of supported ecosystems.
47#[derive(Serialize, Deserialize, Clone, Debug, EnumString, VariantNames, Default)]
48#[cfg_attr(feature = "openapi", derive(ToSchema))]
49pub enum SupportedEcosystems {
50    /// The Go ecosystem
51    #[default]
52    Go,
53    // TODO: Add Maven support back.
54    /*
55    /// The Maven ecosystem
56    Maven,*/
57}
58
59// TODO: These should be their own structs, but they're currently not any different from the params structs.
60
61/// Represents a project that has been initialized. This is the data and state of a project that has been
62/// created.
63#[derive(Serialize, Deserialize, Clone, Debug)]
64#[cfg_attr(feature = "openapi", derive(ToSchema))]
65pub struct InitializedProject {
66    /// The metadata associated with an Skootrs initilialized source repository.
67    pub repo: InitializedRepo,
68    /// The metadata associated with an Skootrs initilialized ecosystem.
69    pub ecosystem: InitializedEcosystem,
70    /// The metadata associated with an Skootrs initilialized source location.
71    pub source: InitializedSource,
72    /// The facets associated with the project.
73    pub facets: HashMap<FacetMapKey, InitializedFacet>,
74    // TODO: What to do if there are name collisions?
75    /// The name of the project.
76    pub name: String,
77}
78
79/// A helper enum for how a facet can be pulled from a `HashMap`
80#[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    /// A map key based on the name of a facet. Useful for filtering based on the name of a facet.
85    Name(String),
86    /// A map key based on the type of a facet. Useful for filtering based on the type of facet.
87    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
131// TODO: This seems redundant with From<FacetMapKey> for String.
132// I am not sure why this can't be automatically derived
133impl 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/// The parameters for creating a project.
141#[derive(Serialize, Deserialize, Clone, Debug)]
142#[cfg_attr(feature = "openapi", derive(ToSchema))]
143pub struct ProjectCreateParams {
144    /// The name of the project to be created.
145    pub name: String,
146    /// The parameters for creating the repository for the project.
147    pub repo_params: RepoCreateParams,
148    /// The parameters for initializing the ecosystem for the project.
149    pub ecosystem_params: EcosystemInitializeParams,
150    /// The parameters for initializing the source code for the project.
151    pub source_params: SourceInitializeParams,
152}
153
154/// The parameters for updating a project.
155#[derive(Serialize, Deserialize, Clone, Debug)]
156#[cfg_attr(feature = "openapi", derive(ToSchema))]
157pub struct ProjectUpdateParams {
158    /// The initialized project to update.
159    pub initialized_project: InitializedProject,
160}
161
162/// The parameters for getting an existing Skootrs project.
163#[derive(Serialize, Deserialize, Clone, Debug)]
164#[cfg_attr(feature = "openapi", derive(ToSchema))]
165pub struct ProjectGetParams {
166    /// The URL of the Skootrs project to get.
167    pub project_url: String,
168}
169
170/// The parameters for listing all the outputs for a Skootrs project.
171#[derive(Serialize, Deserialize, Clone, Debug)]
172#[cfg_attr(feature = "openapi", derive(ToSchema))]
173pub struct ProjectOutputsListParams {
174    /// The initialized project to list the outputs for.
175    pub initialized_project: InitializedProject,
176    /// The release to get the outputs for.
177    pub release: ProjectReleaseParam,
178}
179
180/// The parameters for getting a release from a project.
181#[derive(Serialize, Deserialize, Clone, Debug)]
182#[cfg_attr(feature = "openapi", derive(ToSchema))]
183pub enum ProjectReleaseParam {
184    /// A release based on a tag.
185    Tag(String),
186    /// The latest release.
187    Latest,
188}
189
190impl ProjectReleaseParam {
191    /// Returns the tag of the release.
192    #[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/// The paramaters for getting the output of a project, e.g. an SBOM from a release
202#[derive(Serialize, Deserialize, Clone, Debug)]
203#[cfg_attr(feature = "openapi", derive(ToSchema))]
204pub struct ProjectOutputGetParams {
205    /// The initialized project to get the output from.
206    pub initialized_project: InitializedProject,
207    /// The type of output, e.g. SBOM, to get from the project.
208    pub project_output_type: ProjectOutputType,
209    // TODO: Should project_output be a part of the ProjectOutputType enum?
210    /// The output to get from the project.
211    pub project_output: String,
212    /// The release to get the output from.
213    pub release: ProjectReleaseParam,
214}
215
216/// The parameters for archiving a project.
217#[derive(Serialize, Deserialize, Clone, Debug)]
218#[cfg_attr(feature = "openapi", derive(ToSchema))]
219pub struct ProjectArchiveParams {
220    /// The initialized project to archive.
221    pub initialized_project: InitializedProject,
222}
223
224/// The set of supported output types
225#[derive(Serialize, Deserialize, Clone, Debug, EnumString, VariantNames, Default, Display)]
226#[cfg_attr(feature = "openapi", derive(ToSchema))]
227pub enum ProjectOutputType {
228    #[default]
229    /// An output type for an SBOM from a project.
230    SBOM,
231    /// An output type for an in-toto attestation from a project.
232    InToto,
233    /// An output type for an unknown output from a project.
234    Unknown(String),
235    /// An output type for a custom output from a project.
236    Custom(String),
237}
238
239/// The output of a project.
240#[derive(Serialize, Deserialize, Clone, Debug)]
241#[cfg_attr(feature = "openapi", derive(ToSchema))]
242pub struct ProjectOutput {
243    /// The reference to the project output.
244    pub reference: ProjectOutputReference,
245    /// The output to get from the project.
246    pub output: String,
247}
248
249/// A reference to the output of a project.
250#[derive(Serialize, Deserialize, Clone, Debug)]
251#[cfg_attr(feature = "openapi", derive(ToSchema))]
252pub struct ProjectOutputReference {
253    /// The type of output to get from the project.
254    pub output_type: ProjectOutputType,
255    /// The name of the output to get from the project.
256    pub name: String,
257    /// Labels associated with the output
258    pub labels: Vec<Label>,
259}
260
261/// The parameters for getting a facet from a project.
262#[derive(Serialize, Deserialize, Clone, Debug)]
263#[cfg_attr(feature = "openapi", derive(ToSchema))]
264pub struct FacetGetParams {
265    /// Parameters for first getting the project.
266    pub project_get_params: ProjectGetParams,
267    /// The key of the facet to get from the project.
268    pub facet_map_key: FacetMapKey,
269}
270
271/// Represents an initialized repository along with its host.
272#[derive(Serialize, Deserialize, Clone, Debug)]
273#[cfg_attr(feature = "openapi", derive(ToSchema))]
274pub enum InitializedRepo {
275    /// An initialized Github repository.
276    Github(InitializedGithubRepo),
277}
278
279impl InitializedRepo {
280    /// Returns the host URL of the repo.
281    #[must_use]
282    pub fn host_url(&self) -> String {
283        match self {
284            Self::Github(x) => x.host_url(),
285        }
286    }
287
288    /// Returns the full URL to the repo.
289    #[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                    // FIXME: This will have issues if this isn't a user repo and in fact an organization user.
320                    organization: GithubUser::User(organization.into()),
321                }))
322            }
323            _ => Err("Unsupported repo host".into()),
324        }
325    }
326}
327
328/// Represents an initialized Github repository.
329#[derive(Serialize, Deserialize, Clone, Debug)]
330#[cfg_attr(feature = "openapi", derive(ToSchema))]
331pub struct InitializedGithubRepo {
332    /// The name of the Github repository.
333    pub name: String,
334    /// The organization the Github repository belongs to.
335    pub organization: GithubUser,
336}
337
338impl InitializedGithubRepo {
339    /// Returns the host URL of github.
340    #[must_use]
341    pub fn host_url(&self) -> String {
342        "https://github.com".into()
343    }
344
345    /// Returns the full URL to the github repo.
346    #[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/// Represents an initialized ecosystem. The enum is used to represent the different types of ecosystems
358/// that are supported by Skootrs currently.
359#[derive(Serialize, Deserialize, Clone, Debug)]
360#[cfg_attr(feature = "openapi", derive(ToSchema))]
361pub enum InitializedEcosystem {
362    /// An initialized Go ecosystem for `InitializedSource`.
363    Go(InitializedGo),
364    /// An initialized Maven ecosystem `InitializedSource`.
365    Maven(InitializedMaven),
366}
367
368/// The parameters for creating a repository.
369#[derive(Serialize, Deserialize, Clone, Debug)]
370#[cfg_attr(feature = "openapi", derive(ToSchema))]
371pub enum RepoCreateParams {
372    /// The parameters for creating a Github repository.
373    Github(GithubRepoParams),
374}
375
376/// The parameters for initializing an ecosystem.
377#[derive(Serialize, Deserialize, Clone, Debug)]
378#[cfg_attr(feature = "openapi", derive(ToSchema))]
379pub enum EcosystemInitializeParams {
380    /// The parameters for initializing a Go ecosystem for `InitializedSource`.
381    Go(GoParams),
382    /// The parameters for initializing a Maven ecosystem for `InitializedSource`.
383    Maven(MavenParams),
384}
385
386/// The parameter for getting an initialized repository
387#[derive(Serialize, Deserialize, Clone, Debug)]
388pub struct InitializedRepoGetParams {
389    /// The URL of the repository that Skootrs has previously initialized and you want to get.
390    pub repo_url: String,
391}
392
393/// Represents a Github user which is really just whether or not a repo belongs to  a user or organization.
394/// This is used to create a repo in the Github API. The Github API has different calls for creating a repo
395/// that belongs to the current authorized user or an organization the user has access to.
396#[derive(Serialize, Deserialize, Clone, Debug)]
397#[cfg_attr(feature = "openapi", derive(ToSchema))]
398pub enum GithubUser {
399    /// A Github user, i.e. not an organization.
400    User(String),
401    /// A Github organization, i.e. not a user.
402    Organization(String),
403}
404
405impl GithubUser {
406    /// Returns the name of the user or organization.
407    #[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/// Represents the parameters for creating a Github repository.
416#[derive(Serialize, Deserialize, Clone, Debug)]
417#[cfg_attr(feature = "openapi", derive(ToSchema))]
418pub struct GithubRepoParams {
419    /// The name of the Github repository.
420    pub name: String,
421    /// The description of the Github repository.
422    pub description: String,
423    /// The organization the Github repository belongs to.
424    pub organization: GithubUser,
425}
426
427impl GithubRepoParams {
428    /// Helper for returning the github host.
429    #[must_use]
430    pub fn host_url(&self) -> String {
431        "https://github.com".into()
432    }
433
434    /// Helper for returning the full URL to the github repo.
435    #[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/// Represents the parameters for initializing a source code repository.
447#[derive(Serialize, Deserialize, Clone, Debug)]
448#[cfg_attr(feature = "openapi", derive(ToSchema))]
449pub struct SourceInitializeParams {
450    /// The parent path of the source code repository.
451    pub parent_path: String,
452}
453
454impl SourceInitializeParams {
455    /// Returns the full path to the source code repository with the given name.
456    #[must_use]
457    pub fn path(&self, name: &str) -> String {
458        format!("{}/{}", self.parent_path, name)
459    }
460}
461
462/// Struct representing a working copy of source code.
463#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
464pub struct InitializedSource {
465    /// The path to the source code repository.
466    pub path: String,
467}
468
469/// Represents the Maven ecosystem.
470#[derive(Serialize, Deserialize, Clone, Debug)]
471#[cfg_attr(feature = "openapi", derive(ToSchema))]
472pub struct MavenParams {
473    /// The group ID of the Maven project.
474    pub group_id: String,
475    /// The artifact ID of the Maven project.
476    pub artifact_id: String,
477}
478
479/// Represents the Go ecosystem.
480#[derive(Serialize, Deserialize, Clone, Debug)]
481#[cfg_attr(feature = "openapi", derive(ToSchema))]
482pub struct GoParams {
483    /// The name of the Go module.
484    pub name: String,
485    /// The host of the Go module.
486    pub host: String,
487}
488
489/// Represents an initialized go module.
490#[derive(Serialize, Deserialize, Clone, Debug)]
491#[cfg_attr(feature = "openapi", derive(ToSchema))]
492pub struct InitializedGo {
493    /// The name of the Go module.
494    pub name: String,
495    /// The host of the Go module.
496    pub host: String,
497}
498
499impl InitializedGo {
500    /// Returns the module name in the format "{host}/{name}".
501    #[must_use]
502    pub fn module(&self) -> String {
503        format!("{}/{}", self.host, self.name)
504    }
505}
506
507/// Represents an initialized Maven project.
508#[derive(Serialize, Deserialize, Clone, Debug)]
509#[cfg_attr(feature = "openapi", derive(ToSchema))]
510pub struct InitializedMaven {
511    /// The group ID of the Maven project.
512    pub group_id: String,
513    /// The artifact ID of the Maven project.
514    pub artifact_id: String,
515}
516
517impl GoParams {
518    /// Returns the module name in the format "{host}/{name}".
519    #[must_use]
520    pub fn module(&self) -> String {
521        format!("{}/{}", self.host, self.name)
522    }
523}
524
525/// A set of configuration options for Skootrs.
526#[derive(Serialize, Deserialize, Clone, Debug)]
527#[cfg_attr(feature = "openapi", derive(ToSchema))]
528pub struct Config {
529    /// The local path to cached projects. This is used by `LocalProjectService` for performing operations locally.
530    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}