auditor/domain/
component.rs

1// Copyright 2021-2022 AUDITOR developers
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8use super::{Score, ScoreTest, ValidAmount, ValidName};
9use anyhow::{Context, Error};
10use fake::{Dummy, Fake, Faker, StringFaker};
11use rand::Rng;
12use serde::{Deserialize, Serialize};
13use sqlx::{
14    Postgres, Type,
15    postgres::{PgHasArrayType, PgTypeInfo},
16};
17
18/// A `Component` represents a single component that is to be accounted for.
19///
20/// A component has an associated `name` and `amount` (how many or how much of this component is to
21/// be accounted for).
22/// Optionally, multiple [`Score`]s can be attached to a single component.
23///
24/// # Example:
25///
26/// Create a component that represents 10 CPU cores with a HEPSPEC06 value of 9.2.
27///
28/// ```
29/// # use auditor::domain::{Component, Score};
30/// # fn main() -> Result<(), anyhow::Error> {
31/// let component = Component::new("CPU", 10)?
32///     .with_score(Score::new("HEPSPEC06", 9.2)?);
33/// # Ok(())
34/// # }
35/// ```
36#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, sqlx::Encode, Clone, PartialOrd, Ord)]
37#[sqlx(type_name = "component")]
38pub struct Component {
39    /// Name of the component.
40    pub name: ValidName,
41    /// Amount of the component (how many or how much of this component is to be accounted for).
42    pub amount: ValidAmount,
43    /// Scores that are attached to the component.
44    pub scores: Vec<Score>,
45}
46
47impl Component {
48    /// Create a new component.
49    ///
50    /// # Errors
51    ///
52    /// * [`anyhow::Error`] - If there was an invalid character (`/()"<>\{}`) in the `name`
53    ///   or if a negative `amount` was given.
54    pub fn new<T: AsRef<str>>(name: T, amount: i64) -> Result<Self, Error> {
55        Ok(Component {
56            name: ValidName::parse(name.as_ref().to_string())
57                .context("Failed to parse component name.")?,
58            amount: ValidAmount::parse(amount).context("Failed to parse component amount.")?,
59            scores: vec![],
60        })
61    }
62
63    /// Attach a [`Score`] to the component.
64    pub fn with_score(mut self, score: Score) -> Self {
65        self.scores.push(score);
66        self
67    }
68
69    /// Attach multiple [`Score`]s to the component.
70    pub fn with_scores(mut self, mut scores: Vec<Score>) -> Self {
71        self.scores.append(&mut scores);
72        self
73    }
74}
75
76// manual impl of decode because of a compiler bug. See:
77// https://github.com/launchbadge/sqlx/issues/1031
78// https://github.com/rust-lang/rust/issues/82219
79impl sqlx::decode::Decode<'_, sqlx::Postgres> for Component {
80    fn decode(
81        value: sqlx::postgres::PgValueRef<'_>,
82    ) -> Result<Self, std::boxed::Box<dyn std::error::Error + 'static + Send + Sync>> {
83        let mut decoder = sqlx::postgres::types::PgRecordDecoder::new(value)?;
84        let name = decoder.try_decode::<ValidName>()?;
85        let amount = decoder.try_decode::<ValidAmount>()?;
86        let scores = decoder.try_decode::<Vec<Score>>()?;
87        Ok(Component {
88            name,
89            amount,
90            scores,
91        })
92    }
93}
94
95impl Type<Postgres> for Component {
96    fn type_info() -> PgTypeInfo {
97        PgTypeInfo::with_name("component")
98    }
99}
100
101impl PgHasArrayType for Component {
102    fn array_type_info() -> PgTypeInfo {
103        PgTypeInfo::with_name("_component")
104    }
105}
106
107impl TryFrom<ComponentTest> for Component {
108    type Error = Error;
109
110    fn try_from(value: ComponentTest) -> Result<Self, Self::Error> {
111        Ok(Component {
112            name: ValidName::parse(value.name.ok_or_else(|| anyhow::anyhow!("name is None"))?)?,
113            amount: ValidAmount::parse(
114                value
115                    .amount
116                    .ok_or_else(|| anyhow::anyhow!("amount is None"))?,
117            )?,
118            scores: value
119                .scores
120                .into_iter()
121                .map(Score::try_from)
122                .collect::<Result<_, Self::Error>>()?,
123        })
124    }
125}
126
127#[derive(Debug, Serialize, Deserialize, Clone)]
128pub struct ComponentTest {
129    pub name: Option<String>,
130    pub amount: Option<i64>,
131    // Vecs can be empty, therefore no option needed
132    pub scores: Vec<ScoreTest>,
133}
134
135impl PartialEq<Component> for ComponentTest {
136    fn eq(&self, other: &Component) -> bool {
137        let ComponentTest {
138            name: s_name,
139            amount: s_amount,
140            scores: s_scores,
141        } = self;
142        let Component {
143            name: o_name,
144            amount: o_amount,
145            scores: o_scores,
146        } = other;
147
148        // Can't be equal if any field in ComponentTest is None
149        if s_name.is_none() || s_amount.is_none() {
150            return false;
151        }
152
153        let mut s_scores = s_scores.clone();
154        let mut o_scores = o_scores.clone();
155
156        s_scores.sort();
157        o_scores.sort();
158
159        s_name.as_ref().unwrap() == o_name.as_ref()
160            && s_amount.as_ref().unwrap() == o_amount.as_ref()
161            && s_scores
162                .into_iter()
163                .zip(o_scores)
164                .fold(true, |acc, (a, b)| acc && a == b)
165    }
166}
167
168impl PartialEq<ComponentTest> for Component {
169    fn eq(&self, other: &ComponentTest) -> bool {
170        other.eq(self)
171    }
172}
173
174impl Dummy<Faker> for ComponentTest {
175    fn dummy_with_rng<R: Rng + ?Sized>(_: &Faker, rng: &mut R) -> ComponentTest {
176        let name = StringFaker::with(
177            String::from("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*&^%$#@!~")
178                .into_bytes(),
179            1..256,
180        )
181        .fake_with_rng(rng);
182        ComponentTest {
183            name: Some(name),
184            amount: Some((0..i64::MAX).fake_with_rng(rng)),
185            scores: (0..(0..10u64).fake_with_rng(rng))
186                .map(|_| Faker.fake_with_rng::<ScoreTest, _>(rng))
187                .collect(),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use claim::assert_ok;
196
197    impl quickcheck::Arbitrary for ComponentTest {
198        fn arbitrary(_g: &mut quickcheck::Gen) -> Self {
199            Faker.fake()
200        }
201    }
202
203    #[quickcheck]
204    fn a_valid_name_is_parsed_successfully(component: ComponentTest) {
205        assert_ok!(Component::try_from(component));
206    }
207}