Skip to main content

keelson_exec/
bind.rs

1use keelson_core::{FromValue, ToValue};
2
3/// What a column's Rust type must be able to do: bind in
4/// ([`ToValue`]) and read back out ([`FromValue`]).
5///
6/// This is the contract a code-generation column override hangs on. Blanket
7/// over core's pair — deliberately not a third trait vocabulary: `ToValue` /
8/// `FromValue` are already the two halves, `docs/type-mappings.md` already
9/// defines their semantics per type, and the contract is defined against
10/// [`Value`](keelson_core::Value) rather than any driver — so an override
11/// type binds on every backend or on none.
12///
13/// Generated code names this bound explicitly per overridden column type —
14/// `const _: () = keelson_exec::assert_bind::<UserId>();` — so a non-binding
15/// override fails to compile in one line naming the type, not in an
16/// inference swamp.
17///
18/// The message that line produces is keelson's own, not the compiler's default
19/// walk through the blanket impl: `do_not_recommend` stops rustc from
20/// re-reporting the failure as two unsatisfied supertrait bounds, and
21/// `on_unimplemented` says the useful thing instead. That is worth an attribute
22/// on two counts — the default said `ToValue is not implemented` twice, with a
23/// list of unrelated types that happened to implement it, and the list's
24/// contents drift with the compiler version (which is a failing UI test on the
25/// next release, for no change in this crate).
26#[diagnostic::on_unimplemented(
27    message = "`{Self}` cannot be a keelson column type",
28    label = "not a column type",
29    note = "a column type binds in and reads back out: it must implement both `ToValue` and `FromValue`",
30    note = "for a newtype over one that already binds, `#[derive(Bind)]` (feature `macros`) or `bind_newtype!(Name(inner))` writes both impls"
31)]
32pub trait Bind: ToValue + FromValue + Send + 'static {}
33
34#[diagnostic::do_not_recommend]
35impl<T: ToValue + FromValue + Send + 'static> Bind for T {}
36
37/// Assert at compile time that `T` can bind as a column.
38pub const fn assert_bind<T: Bind>() {}
39
40/// Implement [`ToValue`] and [`FromValue`] for a single-field newtype by
41/// delegating to the inner type — the "derivable for newtypes" story, without
42/// a proc-macro crate.
43///
44/// ```
45/// use keelson_core::{FromValue, ToValue, Value};
46///
47/// #[derive(Debug, Clone, PartialEq)]
48/// pub struct UserId(pub i64);
49/// keelson_exec::bind_newtype!(UserId(i64));
50///
51/// const _: () = keelson_exec::assert_bind::<UserId>();
52/// assert_eq!(UserId(7).to_value(), Value::I64(7));
53/// assert_eq!(UserId::from_value(Value::I64(7)).unwrap(), UserId(7));
54/// ```
55///
56/// Single-field tuple structs only: a multi-field type has no obvious single
57/// column shape, and refusing is the honest move — write the two impls by
58/// hand (about six lines) and put the domain rule where it belongs.
59#[macro_export]
60macro_rules! bind_newtype {
61    ($name:ident($inner:ty)) => {
62        impl $crate::__core::ToValue for $name {
63            fn to_value(self) -> $crate::__core::Value {
64                <$inner as $crate::__core::ToValue>::to_value(self.0)
65            }
66        }
67
68        impl $crate::__core::FromValue for $name {
69            fn from_value(
70                v: $crate::__core::Value,
71            ) -> ::std::result::Result<Self, $crate::__core::Error> {
72                <$inner as $crate::__core::FromValue>::from_value(v).map($name)
73            }
74        }
75    };
76}
77
78#[cfg(test)]
79mod tests {
80    use keelson_core::{FromValue, ToValue, Value};
81
82    use super::assert_bind;
83
84    #[derive(Debug, Clone, PartialEq)]
85    struct UserId(i64);
86    crate::bind_newtype!(UserId(i64));
87
88    // The compile-error guarantee, exercised in the affirmative: this line is
89    // what generated code will emit per overridden column type.
90    const _: () = assert_bind::<UserId>();
91
92    #[test]
93    fn a_newtype_delegates_both_ways() {
94        assert_eq!(UserId(7).to_value(), Value::I64(7));
95        assert_eq!(UserId::from_value(Value::I64(7)).unwrap(), UserId(7));
96        assert_eq!(UserId::from_value(Value::I32(7)).unwrap(), UserId(7)); // widening survives
97        assert!(UserId::from_value(Value::Text("x".into())).is_err());
98    }
99
100    #[test]
101    fn options_of_newtypes_still_bind() {
102        // Option<T: Bind> composes through core's blanket impls.
103        assert_eq!(Some(UserId(1)).to_value(), Value::I64(1));
104        assert_eq!(Option::<UserId>::from_value(Value::Null).unwrap(), None);
105    }
106}