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}