engcon/
lib.rs

1//! # EngCon
2//!
3//! EngCon (Engineering Contracts) is a set of macros and traits defining contracts often found in
4//! engineering problems, e.g. the design of a distilation column.
5//!
6//! # Including EngCon in your Project
7//!
8//! Import engcon and engcon_macros into your project by adding the following lines to your Cargo.toml.
9//! engcon_macros contains the macros needed to derive the traits in EngCon.
10//!
11//! ```toml
12//! [dependencies]
13//! engcon = "0.1"
14//! engcon_macros = "0.1"
15//!
16//! # You can also access engcon_macros exports directly through strum using the "derive" feature
17//! engcon = { version = "0.1", features = ["derive"] }
18//! ```
19//!
20//! This pattern is also used by by the well known [strum crate](https://docs.rs/strum/latest/strum/) that has helpful procedural macros
21//! for enumerations.
22//!
23
24use std::{
25    error::Error,
26    fmt::Display,
27    ops::{Deref, DerefMut},
28};
29
30#[cfg(feature = "derive")]
31pub use engcon_macros::*;
32
33/// A new-type  that ensures validated data for a generic T.
34///
35/// Use the [Validatable] dervice macro and it's rules to
36/// implement define validation rules on a type T.
37///
38/// The type parameter `T` must be [Sized] and implement the [Validator] trait.
39pub struct Validated<T: Validator + Sized> {
40    inner: T,
41}
42
43/// An error type that is used when a validation error occurs
44#[derive(Debug, Clone, PartialEq)]
45pub struct ValidationError {
46    msg: String,
47    src: String,
48}
49
50/// Provides methods to validate and to transform into a [Validated] new-type.
51///
52/// Using the derive macro [Validatable] is recommended instead of a manual implemenation.
53pub trait Validator: Sized {
54    /// Checks if the underlying data is valid and returns an [ValidationError] if not.
55    ///
56    /// Using the derive macro [Validatable] is recommended instead of a
57    /// manual implemenation.
58    fn validate(&self) -> Result<(), ValidationError>;
59
60    /// tries to transform Self into a [Validated] may give an [ValidationError]
61    fn try_into_validated(self) -> Result<Validated<Self>, ValidationError> {
62        match self.validate() {
63            Ok(_) => {
64                // just checked if that is validated...
65                let reval = unsafe { Validated::new_unchecked(self) };
66                Ok(reval)
67            }
68            Err(err) => Err(err),
69        }
70    }
71}
72
73impl Error for ValidationError {}
74
75impl ValidationError {
76    pub fn new(msg: String, src: String) -> Self {
77        ValidationError { msg, src }
78    }
79}
80
81impl Display for ValidationError {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        write!(f, "Error validating '{}': {}", self.src, self.msg)
84    }
85}
86
87impl<T: Validator + Sized> Validated<T> {
88    /// Generates a validated instance of T, usable for compile-time API safety.
89    ///
90    /// # Safety
91    /// The caller has to ensure Validator::validate returns true for that function
92    pub unsafe fn new_unchecked(inner: T) -> Self {
93        Validated::<T> { inner }
94    }
95
96    /// gets the inner unchecked type
97    pub fn into_inner(self) -> T {
98        self.inner
99    }
100}
101
102/* TODO: I don't get where a Into<Validated<T>> has been implemented...
103 *       All the codegen of TryFrom Into etc. was commented out once
104 * error[E0119]: conflicting implementations of trait `std::convert::TryFrom<_>` for type `Validated<_>`
105  --> engcon\src\lib.rs:69:1
106   |
10769 | impl<T: Validator + Sized> TryFrom<T> for Validated<T> {
108   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
109   |
110   = note: conflicting implementation in crate `core`:
111           - impl<T, U> std::convert::TryFrom<U> for T
112             where U: std::convert::Into<T>;
113
114
115 */
116/*
117impl<T: Validator + Sized> TryFrom<T> for Validated<T> {
118    type Error = ValidationError;
119
120    fn try_from(value: T) -> Result<Self, Self::Error> {
121        match value.validate() {
122            Ok(_) => Ok(Validated::<T> { inner: value }),
123            Err(err) => Err(err),
124        }
125    }
126}
127 */
128
129impl<T: Validator + Sized> Deref for Validated<T> {
130    type Target = T;
131
132    fn deref(&self) -> &Self::Target {
133        &self.inner
134    }
135}
136
137impl<T: Validator + Sized> DerefMut for Validated<T> {
138    fn deref_mut(&mut self) -> &mut Self::Target {
139        &mut self.inner
140    }
141}
142
143#[cfg(test)]
144mod tests {
145
146    #[derive(Debug, Clone, PartialEq)]
147    struct PlainOldData {
148        only_lower: String,
149    }
150
151    impl Validator for PlainOldData {
152        fn validate(&self) -> Result<(), ValidationError> {
153            if self.only_lower.chars().any(|ch| !ch.is_lowercase()) {
154                Err(ValidationError::new(
155                    "String is not entirely lower-case".to_owned(),
156                    "Src".to_owned(),
157                ))
158            } else {
159                Ok(())
160            }
161        }
162    }
163
164    use super::*;
165
166    #[test]
167    fn all_lowercase_works() {
168        let tmp = PlainOldData {
169            only_lower: "thisisonlylowercase".to_owned(),
170        };
171        let result = tmp.clone().validate();
172        assert!(result.is_ok());
173    }
174
175    #[test]
176    fn whitespaces_are_errors() {
177        let tmp = PlainOldData {
178            only_lower: "this is not only lowercase".to_owned(),
179        };
180        let result = tmp.clone().validate();
181        assert!(result.is_err());
182    }
183}