Skip to content

Commit c15be47

Browse files
committed
feat: add engcon procedural macro with distillation column example
1 parent 66b17a2 commit c15be47

File tree

16 files changed

+847
-1
lines changed

16 files changed

+847
-1
lines changed

.github/workflows/ci.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths-ignore:
8+
- "**/**.md"
9+
- "**/LICENSE"
10+
- "**/.gitignore"
11+
- "**/.github/ISSUE_TEMPLATE/**"
12+
- "**/.config/**"
13+
pull_request:
14+
branches:
15+
- master
16+
paths-ignore:
17+
- "**/**.md"
18+
- "**/LICENSE"
19+
- "**/.gitignore"
20+
- "**/.github/ISSUE_TEMPLATE/**"
21+
- "**/.config/**"
22+
23+
jobs:
24+
core:
25+
uses: blackportal-ai/infra/.github/workflows/core.yml@master

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ target/
55

66
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
77
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
8-
Cargo.lock
8+
# Cargo.lock
99

1010
# These are backup files generated by rustfmt
1111
**/*.rs.bk

Cargo.lock

Lines changed: 62 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[workspace]
2+
members = ["engcon", "engcon_macros", "examples/distillation"]
3+
resolver = "2"

engcon/Cargo.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "engcon"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = ["Tim Janus <tim@janus.rs>"]
6+
license = "MIT OR Apache-2.0"
7+
8+
description = "Helpful macros to define contracts for constraints at rust struct level."
9+
#keywords = ["engineering", "contracts"]
10+
#categories = []
11+
12+
#documentation = "https://docs.rs/strum"
13+
homepage = "https://github.com/DarthB/engcon"
14+
repository = "https://github.com/DarthB/engcon"
15+
readme = "README.md"
16+
17+
[features]
18+
derive = ["engcon_macros"]
19+
20+
[dependencies]
21+
engcon_macros = { path = "../engcon_macros", optional = true, version = "0.1" }
22+
23+
[dev-dependencies]
24+
engcon_macros = { path = "../engcon_macros", version = "0.1" }
25+
26+
[package.metadata.docs.rs]
27+
features = ["derive"]
28+
rustdoc-args = ["--cfg", "docsrs"]

engcon/src/lib.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//! # EngCon
2+
//!
3+
//! EngCon 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+
24+
use std::{
25+
error::Error,
26+
fmt::Display,
27+
ops::{Deref, DerefMut},
28+
};
29+
30+
#[cfg(feature = "derive")]
31+
pub 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.
39+
pub 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)]
45+
pub 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.
53+
pub 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+
73+
impl Error for ValidationError {}
74+
75+
impl ValidationError {
76+
pub fn new(msg: String, src: String) -> Self {
77+
ValidationError { msg, src }
78+
}
79+
}
80+
81+
impl 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+
87+
impl<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+
|
107+
69 | 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+
/*
117+
impl<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+
129+
impl<T: Validator + Sized> Deref for Validated<T> {
130+
type Target = T;
131+
132+
fn deref(&self) -> &Self::Target {
133+
&self.inner
134+
}
135+
}
136+
137+
impl<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)]
144+
mod 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+
}

engcon_macros/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "engcon_macros"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = ["Tim Janus <tim@janus.rs>"]
6+
license = "MIT OR Apache-2.0"
7+
8+
description = "Helpful macros to define contracts for constraints at rust struct level."
9+
#keywords = ["engineering", "contracts"]
10+
#categories = []
11+
12+
#documentation = "https://docs.rs/strum"
13+
homepage = "https://github.com/DarthB/engcon"
14+
repository = "https://github.com/DarthB/engcon"
15+
readme = "README.md"
16+
17+
[lib]
18+
path = "src/lib.rs"
19+
proc-macro = true
20+
21+
[dependencies]
22+
syn = { version = "2.0", features = ["extra-traits"] }
23+
quote = { version = "1.0" }
24+
proc-macro2 = { version = "1.0" }
25+
26+
[dev-dependencies]
27+
engcon = { path = "../engcon" }

engcon_macros/src/helper.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
use proc_macro2::Span;
2+
use syn::punctuated::Punctuated;
3+
use syn::Expr;
4+
use syn::Path;
5+
use syn::PathSegment;
6+
7+
/// This Result returns in both cases a TokenStream whereby the TokenStream of the Error case
8+
/// contains at least one `compile_error!(...)` expression
9+
pub(crate) type MacroResult = std::result::Result<proc_macro::TokenStream, proc_macro::TokenStream>;
10+
11+
/// converts a syn::Error to a tokenstream, useful in `map_err`
12+
pub(crate) fn synerr_to_tokens(err: syn::Error) -> proc_macro::TokenStream {
13+
err.to_compile_error().into()
14+
}
15+
16+
/// Adapts an expression that contains an ident `field` with a preceeding self generating: `self.field`
17+
pub(crate) fn adapt_ident_with_self(expr: &mut Expr) -> bool {
18+
let adaption: Option<syn::ExprField> = match &expr {
19+
Expr::Path(orig_path) => {
20+
// check if we're using just an ident:
21+
if orig_path.path.segments.len() == 1 {
22+
let ident = orig_path.path.segments.first().unwrap().ident.clone();
23+
let self_segment = PathSegment {
24+
ident: syn::Ident::new("self", Span::call_site()),
25+
arguments: syn::PathArguments::None,
26+
};
27+
let mut segments = Punctuated::new();
28+
segments.push(self_segment);
29+
Some(syn::ExprField {
30+
attrs: vec![],
31+
base: Box::new(Expr::Path(syn::ExprPath {
32+
attrs: vec![],
33+
qself: None,
34+
path: Path {
35+
leading_colon: None,
36+
segments,
37+
},
38+
})),
39+
dot_token: Default::default(),
40+
member: syn::Member::Named(ident),
41+
})
42+
} else {
43+
None
44+
}
45+
}
46+
_ => None,
47+
};
48+
49+
// if we use an identifier without self. we adapt the statement here
50+
if let Some(a) = adaption {
51+
*expr = Expr::Field(a);
52+
true
53+
} else {
54+
false
55+
}
56+
}

0 commit comments

Comments
 (0)