diff --git a/Cargo.lock b/Cargo.lock index 56ef8a672..8259d1ccd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dataplane-id" +version = "0.1.0" +dependencies = [ + "bolero", + "serde", + "uuid", +] + [[package]] name = "derive_arbitrary" version = "1.4.1" @@ -1082,6 +1091,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1326,6 +1341,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" +dependencies = [ + "getrandom", + "serde", + "sha1_smol", +] + [[package]] name = "valuable" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 0e04bce1a..2881fef7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "dpdk-sys", "dpdk-sysroot-helper", "errno", + "id", "net", "routing", ] @@ -14,6 +15,7 @@ default-members = [ "dataplane", "dpdk-sysroot-helper", "errno", + "id", "net", "routing", ] @@ -28,6 +30,7 @@ dpdk-sys = { path = "./dpdk-sys" } dpdk-sysroot-helper = { path = "./dpdk-sysroot-helper" } dplane-rpc = { git = "https://github.com/githedgehog/dplane-rpc.git", version = "1.0.0" } errno = { path = "./errno", package = "dataplane-errno" } +id = { path = "./id", package = "dataplane-id" } net = { path = "./net" } routing = { path = "./routing" } @@ -53,6 +56,7 @@ thiserror = { version = "2.0.12", default-features = false, features = [] } tracing = { version = "0.1.41", default-features = false, features = ["attributes"] } tracing-subscriber = { version = "0.3.19", default-features = false, features = [] } tracing-test = { version = "0.2.5", default-features = false, features = [] } +uuid = { version = "1.15.1", default-features = false, features = [] } [profile.dev] panic = "unwind" diff --git a/id/Cargo.toml b/id/Cargo.toml new file mode 100644 index 000000000..3ae65c6d9 --- /dev/null +++ b/id/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dataplane-id" +version = "0.1.0" +edition = "2024" +license = "Apache-2.0" + +# TODO: fix doctests in sterile env +[lib] +doctest = false + +[features] +default = ["serde"] +bolero = ["dep:bolero"] +serde = ["dep:serde", "uuid/serde"] + +[dependencies] +bolero = { workspace = true, optional = true } +serde = { workspace = true, optional = true, features = ["derive"] } +uuid = { workspace = true, features = ["v4", "v5"] } + +[dev-dependencies] +bolero = { workspace = true, features = ["std"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(kani)'] } diff --git a/id/src/lib.rs b/id/src/lib.rs new file mode 100644 index 000000000..e8bed1a72 --- /dev/null +++ b/id/src/lib.rs @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +//! A "typed" [UUID] crate. +//! +//! [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier + +use core::fmt::{Debug, Formatter}; +use std::borrow::Borrow; +use std::fmt::Display; +use std::marker::PhantomData; +use uuid::Uuid; + +#[allow(unused_imports)] // re-export +#[cfg(any(test, feature = "bolero"))] +pub use contract::*; + +/// A typed [UUID]. +/// +/// The goal of this crate is to create compile-time associations between UUIDs and types. +/// +/// This association helps prevent us from conflating id types while avoiding the need to write a +/// different `FooId` type for each type which needs an id. +/// +/// # Example +/// +/// ``` +/// # use std::collections::HashSet; +/// # use dataplane_id::Id; +/// +/// pub struct User { +/// id: Id, +/// name: String, +/// orders: HashSet>, +/// } +/// +/// pub struct Order { +/// id: Id, +/// user: Id, +/// items: Vec>, +/// } +/// +/// pub struct Item { +/// id: Id, +/// name: String, +/// price: f64, +/// } +/// +/// ``` +/// +/// The [Id] type can be of service in disambiguating the return types of functions and resisting +/// programming errors. +/// +/// As a somewhat trite example, consider +/// +/// ``` +/// # use uuid::Uuid; +/// # type DbConnection = (); // stub for example +/// # type User = (); // stub for example +/// /// List the users +/// fn list(connection: &mut DbConnection) -> Vec { +/// // ... +/// # todo!() +/// } +/// ``` +/// +/// In this case the `list` function returns a list of user ids from a database of some kind. +/// This is both more explicit and less error-prone when written as +/// +/// ``` +/// # use dataplane_id::Id; +/// # type DbConnection = (); // stub for example +/// # type User = (); // stub for example +/// fn list(connection: &mut DbConnection) -> Vec> { +/// // ... +/// # todo!() +/// } +/// ``` +/// +/// Further, consider this method. +/// +/// ```compile_fail +/// fn simple_example(mut user_id: Id, mut order_id: Id) { +/// user_id = order_id; // <- this won't compile, and that's a good thing +/// } +/// ``` +/// +/// The fact that this does not compile is very useful; it has prevented us from conflating our ids. +/// +/// [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier +pub type Id = AbstractIdType<*const T, Uuid>; + +/// An abstract, typed ID. +/// +///
+/// +/// Unless you need something besides UUID, use the [Id] type alias instead. +/// +/// If you use this type directly, you will need to write `AbstractIdType<*const X>` instead of +/// `Id` or you will expose yourself to derive, lifetime, and co/contravariance concerns which +/// have nothing to do with this type. +/// +/// If you need something besides UUID as your ID type, I recommend making a `type` alias such as +/// +/// ``` +/// # use dataplane_id::AbstractIdType; +/// # type MySpecialType = (); // stub for example +/// type MySpecialId = AbstractIdType<*const T, MySpecialType>; +/// ``` +/// +/// if you need to use `MySpecialType` instead of [`Uuid`] for your special type of tagged type. +/// +///
+/// +/// [UUID]: https://en.wikipedia.org/wiki/Universally_unique_identifier +#[cfg_attr(feature = "serde", allow(clippy::unsafe_derive_deserialize))] // not used in deserialize method +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[repr(transparent)] +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct AbstractIdType(U, PhantomData); + +impl AsRef for Id { + fn as_ref(&self) -> &Uuid { + &self.0 + } +} + +impl Display for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + <_ as Display>::fmt(self.0.as_hyphenated(), f) + } +} + +impl Debug for Id { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + <_ as Debug>::fmt(self.0.as_hyphenated(), f) + } +} + +impl Default for Id { + fn default() -> Self { + Self::new() + } +} + +impl Id { + /// Namespace UUID used for generating namespaced [UUIDv5] identifiers + /// + /// [UUIDv5]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.5 + pub const NAMESPACE_UUID: Uuid = Uuid::from_u128(0x8178d539_96b8_40fd_8fbf_402503aa204a); + + /// Generate a new `Id`. + /// This method returns a transparently wrapped [Uuid] which is compile-time tagged with the + /// type parameter `U`. + /// The annotation consumes no space and has no runtime overhead whatsoever. + /// The only function of `U` is to distinguish this type from other [Id] types. + #[must_use] + pub fn new() -> Id { + AbstractIdType(Uuid::new_v4(), PhantomData) + } + + /// Strip type safety and return the wrapped (untyped) [Uuid] + #[must_use] + pub const fn into_raw(self) -> Uuid { + self.0 + } + + /// Return a reference to the underlying (untyped) [Uuid]. + #[must_use] + pub const fn as_raw(&self) -> &Uuid { + &self.0 + } + + /// Create a typed version of `uuid`. + /// + /// # Note + /// + /// You generally should not need this method. + /// In particular, you should not attempt to convert `Id` into `Id` by removing and + /// re-adding the types as doing so defeats the core function of this type. + /// + /// The appropriate use for this method is to add a compile-time type annotation to a [Uuid] + /// in situations where you received the [Uuid] in a context where you may conclusively infer + /// the type of data associated with that [Uuid]. + /// + /// You _should not_ use this method in situations where you are generating a [Uuid] and wish + /// to associate it with a type. + /// In such cases use [`Id::new::`] instead. + #[must_use] + pub const fn from_raw(uuid: Uuid) -> Self { + Self(uuid, PhantomData) + } + + /// Generate a [UUID version 5] based on the supplied namespace and byte string. + /// + /// [UUID version 5]: https://datatracker.ietf.org/doc/html/rfc9562#section-5.5 + #[must_use] + pub fn new_v5>(namespace: Uuid, name: N) -> Self { + Self(Uuid::new_v5(&namespace, name.borrow()), PhantomData) + } + + /// Generate a compile time "typed" UUID version 5. + /// + /// This value will not change between compiler runs if `tag` does not. + /// This value will be unique per tag (neglecting SHA1 hash collisions). + pub fn new_static(tag: &str) -> Self { + Self::new_v5(Self::NAMESPACE_UUID, tag.as_bytes()) + } +} + +impl From> for Uuid { + fn from(value: Id) -> Self { + value.0 + } +} + +impl From for Id { + /// You generally should not use this method. + /// See the docs for [`Id::::from_raw`] + fn from(value: Uuid) -> Self { + Self::from_raw(value) + } +} + +#[cfg(any(test, feature = "bolero"))] +mod contract { + use crate::{AbstractIdType, Id}; + use bolero::{Driver, TypeGenerator}; + use std::marker::PhantomData; + + impl TypeGenerator for Id { + fn generate(driver: &mut D) -> Option { + Some(AbstractIdType( + uuid::Builder::from_random_bytes(driver.produce::<[u8; 16]>()?).into_uuid(), + PhantomData, + )) + } + } +} + +#[cfg(test)] +mod test { + use crate::Id; + use uuid::Uuid; + + fn parse_back_test() { + bolero::check!() + .with_type() + .for_each(|x: &Id| assert_eq!(*x, Id::from_raw(x.into_raw()))); + } + + #[test] + fn parse_back_unit() { + parse_back_test::<()>() + } + + #[test] + fn parse_back_u32() { + parse_back_test::() + } + + #[test] + fn parse_back_string() { + parse_back_test::() + } + + #[test] + fn parse_back_recursive() { + parse_back_test::>() + } + + #[test] + fn new_generates_unique() { + bolero::check!().with_type().for_each(|x: &Id<()>| { + let y = Id::<()>::new(); + assert_ne!(*x, y); + }); + } + + #[test] + fn test_v5() { + bolero::check!() + .with_type() + .for_each(|(namespace, val): &([u8; 16], [u8; 16])| { + let namespace = Uuid::from_slice(namespace).unwrap(); + let raw = Id::<()>::new_v5(namespace, val.as_slice()).into_raw(); + let reference = Uuid::new_v5(&namespace, val); + assert_eq!(raw, reference); + }); + } + + #[test] + fn test_static() { + bolero::check!().with_type().for_each(|x: &String| { + let raw = Id::<()>::new_static(x.as_str()).into_raw(); + let reference = Uuid::new_v5(&Id::<()>::NAMESPACE_UUID, x.as_bytes()); + assert_eq!(raw, reference); + }); + } +}