diff --git a/Cargo.toml b/Cargo.toml index 34c26c0..fe85318 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,25 @@ [package] name = "julid-rs" -version = "0.0.1" +version = "0.1.6" authors = ["Joe Ardent "] edition = "2021" -keywords = ["ulid", "library", "sqlite", "extension"] +keywords = ["ulid", "library", "sqlite", "extension", "julid"] description = "A library and loadable extension for SQLite that uses it, that provides Joe's ULIDs." readme = "README.md" license-file = "LICENSE.md" repository = "https://gitlab.com/nebkor/julid" +[features] +default = ["serde", "sqlx"] +clib = [] + [lib] name = "julid" crate-type = ["cdylib", "rlib"] [dependencies] rand = "0.8" +serde = { version = "1.0", features = ["derive"], optional = true } +sqlx = { version = "0.7", features = ["sqlite"], default-features = false, optional = true } sqlite-loadable = "0.0.5" diff --git a/src/lib.rs b/src/lib.rs index ebe4c18..939ad18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "clib")] use sqlite_loadable::{ api, define_scalar_function, prelude::{ @@ -9,12 +10,19 @@ use sqlite_loadable::{ mod base32; pub mod julid; +#[cfg(feature = "serde")] +pub mod serde; +#[cfg(feature = "sqlx")] +pub mod sqlx; + +pub use base32::JULID_LEN; pub use julid::Julid; //-************************************************************************ // Entrypoint into the loadable extension //-************************************************************************ +#[cfg(feature = "clib")] #[no_mangle] pub unsafe extern "C" fn sqlite3_julid_init( db: *mut sqlite3, @@ -28,6 +36,7 @@ pub unsafe extern "C" fn sqlite3_julid_init( } } +#[cfg(feature = "clib")] fn init_rs(db: *mut sqlite3) -> Result<()> { let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC; define_scalar_function(db, "julid_new", 0, julid_new, FunctionFlags::INNOCUOUS)?; @@ -42,11 +51,13 @@ fn init_rs(db: *mut sqlite3) -> Result<()> { //-************************************************************************ // impls //-************************************************************************ +#[cfg(feature = "clib")] fn julid_new(context: *mut sqlite3_context, _vals: &[*mut sqlite3_value]) -> Result<()> { api::result_blob(context, Julid::new().to_bytes().as_slice()); Ok(()) } +#[cfg(feature = "clib")] fn julid_timestamp(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { if let Some(value) = id.get(0) { let id = api::value_blob(value); @@ -64,6 +75,7 @@ fn julid_timestamp(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Ok(()) } +#[cfg(feature = "clib")] fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { if let Some(value) = id.get(0) { let id = api::value_blob(value); @@ -81,6 +93,7 @@ fn julid_counter(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Re Ok(()) } +#[cfg(feature = "clib")] fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { if let Some(value) = id.get(0) { let id = api::value_blob(value); @@ -98,6 +111,7 @@ fn julid_sortable(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> R Ok(()) } +#[cfg(feature = "clib")] fn julid_string(context: *mut sqlite3_context, id: &[*mut sqlite3_value]) -> Result<()> { if let Some(value) = id.get(0) { let id = api::value_blob(value); diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 0000000..dd0c5ac --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,120 @@ +//! Serialization and deserialization. +//! +//! By default, serialization and deserialization go through Julid's big-endian +//! bytes representation. + +use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::Julid; + +impl Serialize for Julid { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_bytes(&self.to_bytes()) + } +} + +struct JulidVisitor; + +impl<'de> Visitor<'de> for JulidVisitor { + type Value = Julid; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("16 bytes") + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + match std::convert::TryInto::<[u8; 16]>::try_into(v) { + Ok(v) => Ok(v.into()), + Err(_) => Err(serde::de::Error::invalid_length(v.len(), &self)), + } + } + + fn visit_byte_buf(self, v: Vec) -> Result + where + E: serde::de::Error, + { + let len = v.len(); + match std::convert::TryInto::<[u8; 16]>::try_into(v) { + Ok(v) => Ok(v.into()), + Err(_) => Err(serde::de::Error::invalid_length(len, &self)), + } + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut bytes = [0u8; 16]; + let size = seq.size_hint().unwrap_or(0); + let mut count = 0; + while let Some(val) = seq.next_element()? { + if count > 15 { + break; + } + bytes[count] = val; + count += 1; + } + if count != 16 || size > 16 { + let sz = if count < 16 { count } else { size }; + Err(serde::de::Error::invalid_length(sz, &self)) + } else { + Ok(bytes.into()) + } + } +} + +impl<'de> Deserialize<'de> for Julid { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_bytes(JulidVisitor) + } +} + +/// Serialization and deserialization of Julids through their string +/// representation. +/// +/// To use it, annotate a field with +/// `#[serde(with = "julid_as_str")]`, +/// `#[serde(serialize_with = "julid_as_str")]`, or +/// `#[serde(deserialize_with = "julid_as_str")]`. +/// +/// # Examples +/// ``` +/// # use julid::Julid; +/// # use julid::serde::ulid_as_str; +/// # use serde::{Serialize, Deserialize}; +/// #[derive(Serialize, Deserialize)] +/// struct StrExample { +/// #[serde(with = "julid_as_str")] +/// identifier: Julid +/// } +/// ``` +pub mod julid_as_str { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + use crate::Julid; + + pub fn serialize(value: &Julid, serializer: S) -> Result + where + S: Serializer, + { + let text = value.to_string(); + text.serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let deserialized_str = String::deserialize(deserializer)?; + Julid::from_string(&deserialized_str).map_err(serde::de::Error::custom) + } +} diff --git a/src/sqlx.rs b/src/sqlx.rs new file mode 100644 index 0000000..524ee89 --- /dev/null +++ b/src/sqlx.rs @@ -0,0 +1,32 @@ +use std::borrow::Cow; + +use sqlx::{ + encode::IsNull, + sqlite::{SqliteArgumentValue, SqliteValueRef}, + Decode, Encode, Sqlite, +}; + +use crate::Julid; + +impl sqlx::Type for Julid { + fn type_info() -> ::TypeInfo { + <&[u8] as sqlx::Type>::type_info() + } +} + +impl<'q> Encode<'q, Sqlite> for Julid { + fn encode_by_ref(&self, args: &mut Vec>) -> IsNull { + args.push(SqliteArgumentValue::Blob(Cow::Owned( + self.to_bytes().to_vec(), + ))); + IsNull::No + } +} + +impl Decode<'_, Sqlite> for Julid { + fn decode(value: SqliteValueRef<'_>) -> Result { + let bytes = <&[u8] as Decode>::decode(value)?; + let bytes: [u8; 16] = bytes.try_into().unwrap_or_default(); + Ok(bytes.into()) + } +}