3 releases

0.1.2 Sep 8, 2024
0.1.1 Sep 8, 2024
0.1.0 Sep 8, 2024

#653 in Rust patterns

MIT/Apache

12KB
201 lines

Simple newtype macro to create strong ID types over other types.

Make a strong ID out of a string:

tacit!(id1::Id1 -> String);
tacit!(id2::Id2 -> Arc<str>);

Try it with ulid

use ulid::Ulid;
tacit!(id::Id -> Ulid);

You can also write a doc comment for your tacits:

tacit!(
    /// My unique type!
    id::Id -> Ulid
);

Tacit works by generating two structs with the same name. The first part of the macro, id::Id generates a struct in a module named id, like so:

mod id {
    #[derive(Clone, Copy, Debug...)]
    struct Id;
}

which then uses that type to make a type alias over the Tacit struct:

pub type Id = Tacit<Ulid, id::Id>;

The first generic argument is the representation, the actual data in the type. The second argument is the identifier, which prevents Tacits of the same representation to not compare with each other or allow them to be passed as arguments to functions expecting different types.

While Tacits of different types do not compare, they can convert between other tacits that implement the same representation:

tacit!(a::A -> Ulid);
tacit!(b::B -> Ulid);
let a: A = Ulid::new().into();
let b: B = a.cast();

the Tacit struct implements a variety of traits when the representation implements them. Below is a list of the following:

  • Clone
  • Copy
  • Debug[1]
  • Display[1]
  • Eq
  • PartialEq
  • Ord
  • PartialOrd
  • Hash
  • From<R> where R is the representation type.
  • FromStr where FromStr::Err is R::FromStr::Err
  • serde::Serialize[2]
  • serde::Deserialize[2]

Note that, if the representation doesn't implement any of these, the Tacit won't have it implemented either. You can still use types that don't implement these, though, as they're implemented conditionally. This enables non-Copy types to be Tacits, while also allowing Copy types to be Copyable.

Additionally, there are special From<str>/From<String> impls for Arc<str> and Rc<str> as I couldn't find a good way to implements a generic From<R> for types that encase other types like Arc does. I figured this was a reasonable thing to hard-code since I'm essentially hardcoding over Rust's string literal syntax.

[1] Tacit's display outputs are as follows:
Given tacit!(a::A -> Arc<str>):

  • Debug: Tacit(A, "abc")
  • Display: A(abc)

If you want the raw debug/display output without the tacit's name, call:

  • tacit.repr_debug()
  • tacit.repr_str()

Note that the automatically generated identifier implements the tacit::Identity trait in order to display it's name. If you are not using an automatically generated ID, ensure your identifier type implements Identity as well, or else the entire Tacit cannot use Debug or Display. This seems to be a limitation in Rust's trait system and one I can't find a way to overcome, as ideally I'd like to simply only print the representation if the identifier has no display.

[2] A note on serde: The Tacit will not serialize or deserialize itself, it is a direct passthrough to the representation type. This means instead of getting (from tacit!(a::A -> u32)):

Tacit {
    repr: 123
}

You will instead simply get 123. Keep this in mind when handling serialization, as this essentially means Tacit will not help you out with deserializing types.

Dependencies

~0.3–1MB
~21K SLoC