cyclonedx_bom/external_models/
date_time.rs

1/*
2 * This file is part of CycloneDX Rust Cargo.
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 *
16 * SPDX-License-Identifier: Apache-2.0
17 */
18
19use std::convert::TryFrom;
20
21use thiserror::Error;
22use time::{format_description::well_known::Iso8601, OffsetDateTime};
23
24use crate::validation::ValidationError;
25
26/// For the purposes of CycloneDX SBOM documents, `DateTime` is a ISO8601 formatted timestamp
27///
28/// The corresponding CycloneDX XML schema definition is the [`xs` namespace](https://cyclonedx.org/docs/1.3/xml/#ns_xs), which defines the [`dateTime`](https://www.w3.org/TR/xmlschema11-2/#dateTime)) format.
29///
30/// A valid timestamp can be created from a [`String`](std::string::String) using the [`TryFrom`](std::convert::TryFrom) / [`TryInto`](std::convert::TryInto) traits.
31///
32/// ```
33/// use cyclonedx_bom::external_models::date_time::DateTime;
34/// use std::convert::TryInto;
35///
36/// let timestamp = String::from("1970-01-01T00:00:00Z");
37/// let date_time: DateTime = timestamp.clone().try_into().expect("Failed to parse as DateTime");
38///
39/// assert_eq!(date_time.to_string(), timestamp);
40/// ```
41#[derive(Clone, Debug, PartialEq, Eq, Hash)]
42pub struct DateTime(pub(crate) String);
43
44pub fn validate_date_time(date_time: &DateTime) -> Result<(), ValidationError> {
45    if OffsetDateTime::parse(&date_time.0, &Iso8601::DEFAULT).is_err() {
46        return Err("DateTime does not conform to ISO 8601".into());
47    }
48    Ok(())
49}
50
51impl DateTime {
52    pub fn now() -> Result<Self, DateTimeError> {
53        let now = OffsetDateTime::now_utc()
54            .format(&Iso8601::DEFAULT)
55            .map_err(|_| DateTimeError::FailedCurrentTime)?;
56        Ok(Self(now))
57    }
58}
59
60impl TryFrom<String> for DateTime {
61    type Error = DateTimeError;
62
63    fn try_from(value: String) -> Result<Self, Self::Error> {
64        match OffsetDateTime::parse(&value, &Iso8601::DEFAULT) {
65            Ok(_) => Ok(Self(value)),
66            Err(e) => Err(DateTimeError::InvalidDateTime(format!(
67                "DateTime does not conform to ISO 8601: {}",
68                e
69            ))),
70        }
71    }
72}
73
74impl std::fmt::Display for DateTime {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        f.write_str(&self.0)
77    }
78}
79
80impl AsRef<str> for DateTime {
81    fn as_ref(&self) -> &str {
82        &self.0
83    }
84}
85
86#[derive(Debug, Error, PartialEq, Eq)]
87pub enum DateTimeError {
88    #[error("Invalid DateTime: {}", .0)]
89    InvalidDateTime(String),
90
91    #[error("Failed to get current time")]
92    FailedCurrentTime,
93}
94
95#[cfg(test)]
96mod test {
97    use pretty_assertions::assert_eq;
98
99    use crate::{external_models::validate_date_time, prelude::DateTime};
100
101    #[test]
102    fn valid_datetimes_should_pass_validation() {
103        let validation_result =
104            validate_date_time(&DateTime("1969-06-28T01:20:00.00-04:00".to_string()));
105
106        assert!(validation_result.is_ok());
107    }
108
109    #[test]
110    fn invalid_datetimes_should_fail_validation() {
111        let validation_result = validate_date_time(&DateTime("invalid date".to_string()));
112
113        assert_eq!(
114            validation_result,
115            Err("DateTime does not conform to ISO 8601".into()),
116        );
117    }
118}