Skip to content

Commit af1605f

Browse files
committed
functora-tagged readme
1 parent 52a74e7 commit af1605f

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed

rust/functora-tagged/README.md

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,315 @@
22

33
Lightweight, macro-free newtypes with refinement and derived traits.
44

5+
## Motivation
6+
7+
Newtypes are a fundamental pattern in Rust for enhancing type safety and expressing semantic meaning. By wrapping an existing type (the `Rep`resentation) in a new, distinct type, we prevent accidental misuse and clearly communicate intent. For example, distinguishing between a `UserId` and a `ProductId`, even if both are internally represented as `u64`, prevents bugs where one might be used in place of the other. This style makes code more self-documenting and less prone to logical errors.
8+
9+
While standard traits like `Eq`, `PartialEq`, `Ord`, `PartialOrd`, `Clone`, and `Debug` can often be derived automatically in Rust, many essential traits are not. These include parsing (`FromStr`), serialization (`serde::Serialize`/`Deserialize`), and database integration (`diesel::Queryable`, `ToSql`, `FromSql`, `AsExpression`). Implementing these traits for newtypes manually can lead to substantial boilerplate.
10+
11+
Rust has common macro-based solutions for the newtype traits derivation problem. Macros are often employed as a last resort when language expressiveness is insufficient. However, they introduce significant drawbacks:
12+
13+
- Syntax: Macro-based code is not just Rust code. It acts as a complex, "foreign" DSL that is hard to read, maintain, and extend.
14+
- Boilerplate: Despite their intent, macros frequently require significant boilerplate, undermining their primary benefit.
15+
- Complexity: Macros can obscure the underlying logic and make debugging difficult.
16+
17+
`functora-tagged` offers a superior, macro-free alternative. It provides a clean, idiomatic, and type-safe mechanism for creating newtypes. Through the `Refine` trait, you can define custom validation and transformation logic for your newtype. This logic is then automatically integrated into implementations for crucial, non-trivially derivable traits like `FromStr`, `serde`, and `diesel`, achieving true zero boilerplate for these complex scenarios without the downsides of macros.
18+
19+
## Tagged Struct
20+
21+
The primary newtype building block is the `Tagged<Rep, Tag>` struct.
22+
23+
- `Rep`: The underlying representation type (e.g., `String`, `i32`).
24+
- `Tag`: A phantom type used at compile time to distinguish between different newtypes that share the same `Rep`. The `Tag` type itself implements the `Refine<Rep>` trait to define refinement logic.
25+
26+
This structure allows you to create distinct types that behave identically to their `Rep` type for many traits, unless explicitly customized.
27+
28+
```rust
29+
use std::marker::PhantomData;
30+
31+
pub struct Tagged<Rep, Tag>(Rep, PhantomData<Tag>);
32+
```
33+
34+
## Refine Trait
35+
36+
To enforce specific refinement rules for your newtypes, you implement the `Refine<Rep>` trait for the `Tag` type. This trait allows you to define custom logic for refining the newtype representation.
37+
38+
```rust
39+
use functora_tagged::*;
40+
41+
pub enum NonEmptyTag {}
42+
pub struct NonEmptyError;
43+
44+
impl Refine<String> for NonEmptyTag {
45+
type RefineError = NonEmptyError;
46+
fn refine(
47+
rep: String,
48+
) -> Result<String, Self::RefineError> {
49+
if rep.is_empty() {
50+
Err(NonEmptyError)
51+
} else {
52+
Ok(rep)
53+
}
54+
}
55+
}
56+
```
57+
58+
## Derived Traits
59+
60+
`functora-tagged` provides blanket implementations for several important traits. These traits work seamlessly with your newtypes, respecting the underlying representation behavior and customizable refinement rules defined by the `Tag` type's implementation of `Refine<Rep>`.
61+
62+
### Direct Derive:
63+
64+
- Eq
65+
- PartialEq
66+
- Ord
67+
- PartialOrd
68+
- Clone
69+
- Debug
70+
- serde::Serialize (with `serde` feature)
71+
- diesel::serialize::ToSql (with `diesel` feature)
72+
- diesel::expression::AsExpression (with `diesel` feature)
73+
74+
### Refined Derive:
75+
76+
- FromStr
77+
- serde::Deserialize (with `serde` feature)
78+
- diesel::Queryable (with `diesel` feature)
79+
- diesel::deserialize::FromSql (with `diesel` feature)
80+
81+
## Examples
82+
83+
You can promote `Rep` values into newtype values using `Tagged::new(rep)` applied directly to a `Rep` value. To demote a newtype value back to a `Rep` value, you can use the `.rep()` method. You can also use any serializer or deserializer for the newtype that is available for `Rep`.
84+
85+
### Default Newtype
86+
87+
When a `Tag` type has a default `Refine` implementation that doesn't add new constraints or transformations, `Tagged` can be used for simple type distinction.
88+
89+
```rust
90+
use functora_tagged::*;
91+
92+
pub enum NonNegTag {}
93+
94+
impl Refine<usize> for NonNegTag {
95+
type RefineError = ();
96+
}
97+
98+
pub type NonNeg = Tagged<usize, NonNegTag>;
99+
100+
let rep = 123;
101+
let new = NonNeg::new(rep).unwrap();
102+
103+
assert_eq!(*new.rep(), rep);
104+
```
105+
106+
### Refined Newtype
107+
108+
This example demonstrates a simple refinement for numeric types to ensure they are positive, using `PositiveTag`.
109+
110+
```rust
111+
use functora_tagged::*;
112+
113+
#[derive(PartialEq, Debug)]
114+
pub enum PositiveTag {}
115+
pub type Positive = Tagged<usize, PositiveTag>;
116+
117+
#[derive(PartialEq, Debug)]
118+
pub struct PositiveError;
119+
120+
impl Refine<usize> for PositiveTag {
121+
type RefineError = PositiveError;
122+
fn refine(
123+
rep: usize,
124+
) -> Result<usize, Self::RefineError>
125+
{
126+
if rep > 0 {
127+
Ok(rep)
128+
} else {
129+
Err(PositiveError)
130+
}
131+
}
132+
}
133+
134+
let rep = 100;
135+
let new = Positive::new(rep).unwrap();
136+
assert_eq!(*new.rep(), rep);
137+
138+
let err = Positive::new(0).unwrap_err();
139+
assert_eq!(err, PositiveError);
140+
```
141+
142+
### Generic Newtype
143+
144+
This demonstrates a generic `Positive<Rep>` newtype that enforces positive values for any numeric type `Rep` that implements `Refine<PositiveTag>`.
145+
146+
```rust
147+
use functora_tagged::*;
148+
use num_traits::Zero;
149+
150+
#[derive(PartialEq, Debug)]
151+
pub enum PositiveTag {}
152+
pub type Positive<Rep> = Tagged<Rep, PositiveTag>;
153+
154+
#[derive(PartialEq, Debug)]
155+
pub struct PositiveError;
156+
157+
impl<Rep> Refine<Rep> for PositiveTag
158+
where
159+
Rep: Zero + PartialOrd,
160+
{
161+
type RefineError = PositiveError;
162+
fn refine(
163+
rep: Rep,
164+
) -> Result<Rep, Self::RefineError>
165+
{
166+
if rep > Rep::zero() {
167+
Ok(rep)
168+
} else {
169+
Err(PositiveError)
170+
}
171+
}
172+
}
173+
174+
let rep = 100;
175+
let new = Positive::<i32>::new(rep).unwrap();
176+
assert_eq!(*new.rep(), rep);
177+
178+
let rep = 10.5;
179+
let new = Positive::<f64>::new(rep).unwrap();
180+
assert_eq!(*new.rep(), rep);
181+
182+
let err = Positive::<i32>::new(-5).unwrap_err();
183+
assert_eq!(err, PositiveError);
184+
185+
let err = Positive::<f64>::new(0.0).unwrap_err();
186+
assert_eq!(err, PositiveError);
187+
```
188+
189+
### Nested Newtype
190+
191+
This example demonstrates nesting newtypes: `UserId<Rep>` generic newtype is built on top of the other `NonEmpty<Rep>` generic newtype and adds its own refinement logic.
192+
193+
```rust
194+
use functora_tagged::*;
195+
196+
#[derive(PartialEq, Debug)]
197+
pub enum NonEmptyTag {}
198+
pub type NonEmpty<Rep> = Tagged<Rep, NonEmptyTag>;
199+
200+
#[derive(PartialEq, Debug)]
201+
pub struct NonEmptyError;
202+
203+
impl Refine<String> for NonEmptyTag {
204+
type RefineError = NonEmptyError;
205+
fn refine(
206+
rep: String,
207+
) -> Result<String, Self::RefineError>
208+
{
209+
if rep.is_empty() {
210+
Err(NonEmptyError)
211+
} else {
212+
Ok(rep)
213+
}
214+
}
215+
}
216+
217+
impl Refine<isize> for NonEmptyTag {
218+
type RefineError = NonEmptyError;
219+
fn refine(
220+
rep: isize,
221+
) -> Result<isize, Self::RefineError>
222+
{
223+
if rep == 0 {
224+
Err(NonEmptyError)
225+
} else {
226+
Ok(rep)
227+
}
228+
}
229+
}
230+
231+
#[derive(PartialEq, Debug)]
232+
pub enum UserIdTag {}
233+
pub type UserId<Rep> =
234+
Tagged<NonEmpty<Rep>, UserIdTag>;
235+
236+
#[derive(PartialEq, Debug)]
237+
pub struct UserIdError;
238+
239+
impl Refine<NonEmpty<String>> for UserIdTag {
240+
type RefineError = UserIdError;
241+
fn refine(
242+
rep: NonEmpty<String>,
243+
) -> Result<NonEmpty<String>, Self::RefineError>
244+
{
245+
if rep.rep().starts_with("user_")
246+
&& rep.rep().len() > 5
247+
{
248+
Ok(rep)
249+
} else {
250+
Err(UserIdError)
251+
}
252+
}
253+
}
254+
255+
impl Refine<NonEmpty<isize>> for UserIdTag {
256+
type RefineError = UserIdError;
257+
fn refine(
258+
rep: NonEmpty<isize>,
259+
) -> Result<NonEmpty<isize>, Self::RefineError>
260+
{
261+
if *rep.rep() > 0 {
262+
Ok(rep)
263+
} else {
264+
Err(UserIdError)
265+
}
266+
}
267+
}
268+
269+
let rep = "user_123";
270+
let new = rep.parse::<UserId<String>>().unwrap();
271+
assert_eq!(new.rep().rep(), rep);
272+
273+
let err = "".parse::<UserId<String>>().unwrap_err();
274+
assert_eq!(
275+
err,
276+
ParseError::Decode(ParseError::Refine(
277+
NonEmptyError
278+
))
279+
);
280+
281+
let err = "post_123"
282+
.parse::<UserId<String>>()
283+
.unwrap_err();
284+
assert_eq!(err, ParseError::Refine(UserIdError));
285+
286+
let rep: isize = 123;
287+
let new = rep
288+
.to_string()
289+
.parse::<UserId<isize>>()
290+
.unwrap();
291+
assert_eq!(*new.rep().rep(), rep);
292+
293+
let err = "0".parse::<UserId<isize>>().unwrap_err();
294+
assert_eq!(
295+
err,
296+
ParseError::Decode(ParseError::Refine(
297+
NonEmptyError
298+
))
299+
);
300+
301+
let err = "-1".parse::<UserId<isize>>().unwrap_err();
302+
assert_eq!(err, ParseError::Refine(UserIdError));
303+
```
304+
305+
## Integrations
306+
307+
`functora-tagged` provides optional integrations for common Rust ecosystems:
308+
309+
- **`serde`**: For serialization and deserialization. Enable with the `serde` feature.
310+
- **`diesel`**: For database interactions. Enable with the `diesel` feature.
311+
312+
These integrations respect the `Refine` rules defined for your types.
313+
5314
<hr>
6315

7316
© 2025 [Functora](https://functora.github.io/). All rights reserved.

0 commit comments

Comments
 (0)