From d950286c4c54935f5549ad62d713987cc78dc5db Mon Sep 17 00:00:00 2001 From: Orual Date: Sat, 27 Sep 2025 16:15:41 +0000 Subject: [PATCH] moving types into module, tid, datetime, identifier, handle --- Cargo.lock | 302 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .tangled/workflows/build.yml | 14 ++++++++++++++ crates/jacquard-common/Cargo.toml | 3 +++ crates/jacquard/Cargo.toml | 1 + crates/jacquard-common/src/aturi.rs | 0 crates/jacquard-common/src/blob.rs | 132 ------------------------------------------------------------------------------------------------------------------------------------ crates/jacquard-common/src/cid.rs | 195 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- crates/jacquard-common/src/cowstr.rs | 53 +---------------------------------------------------- crates/jacquard-common/src/did.rs | 143 ----------------------------------------------------------------------------------------------------------------------------------------------- crates/jacquard-common/src/handle.rs | 0 crates/jacquard-common/src/lib.rs | 11 ++--------- crates/jacquard-common/src/link.rs | 1 - crates/jacquard-common/src/nsid.rs | 0 crates/jacquard-common/src/types.rs | 11 +++++++++++ crates/jacquard-common/src/types/aturi.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/blob.rs | 132 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/cid.rs | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/datetime.rs | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/did.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/handle.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/ident.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/integer.rs | 326 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ crates/jacquard-common/src/types/link.rs | 1 + crates/jacquard-common/src/types/nsid.rs | 0 crates/jacquard-common/src/types/tid.rs | 202 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 25 file(s) changed, 1955 insertion(s)(+), 532 deletion(s)(-) diff --git a/Cargo.lock b/Cargo.lock --- a/Cargo.lock +++ b/Cargo.lock @@ -12,6 +12,15 @@ ] [[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] name = "anstream" version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -62,10 +71,22 @@ ] [[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] name = "base-x" version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "castaway" @@ -77,10 +98,33 @@ ] [[package]] +name = "cc" +version = "1.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] [[package]] name = "cid" @@ -157,6 +201,12 @@ ] [[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] name = "core2" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -192,10 +242,71 @@ ] [[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +dependencies = [ + "equivalent", + "hashbrown", +] [[package]] name = "is_terminal_polyfill" @@ -214,12 +325,14 @@ version = "0.1.0" dependencies = [ "clap", + "jacquard-common", ] [[package]] name = "jacquard-common" version = "0.1.0" dependencies = [ + "chrono", "cid", "compact_str", "miette", @@ -227,8 +340,32 @@ "multihash", "regex", "serde", + "serde_html_form", + "serde_json", "thiserror", ] + +[[package]] +name = "js-sys" +version = "0.3.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.176" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "memchr" @@ -281,10 +418,31 @@ ] [[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] name = "once_cell_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "proc-macro2" @@ -385,6 +543,38 @@ ] [[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -452,10 +642,122 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] +name = "wasm-bindgen" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "windows-link" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] [[package]] name = "windows-sys" diff --git a/.tangled/workflows/build.yml b/.tangled/workflows/build.yml new file mode 100644 --- /dev/null +++ b/.tangled/workflows/build.yml @@ -0,0 +1,14 @@ +when: + - event: ["push", "pull_request"] + branch: ["main"] + +engine: nixery + +dependencies: + nixpkgs: + - just + +steps: + - name: build appview + command: | + nix build diff --git a/crates/jacquard-common/Cargo.toml b/crates/jacquard-common/Cargo.toml --- a/crates/jacquard-common/Cargo.toml +++ b/crates/jacquard-common/Cargo.toml @@ -6,6 +6,7 @@ description.workspace = true [dependencies] +chrono = "0.4.42" cid = { version = "0.11.1", features = ["serde", "std"] } compact_str = "0.9.0" miette = "7.6.0" @@ -13,4 +14,6 @@ multihash = "0.19.3" regex = "1.11.3" serde = { version = "1.0.227", features = ["derive"] } +serde_html_form = "0.2.8" +serde_json = "1.0.145" thiserror = "2.0.16" diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -10,3 +10,4 @@ [dependencies] clap = { workspace = true } +jacquard-common = { path = "../jacquard-common" } diff --git a/crates/jacquard-common/src/aturi.rs b/crates/jacquard-common/src/aturi.rs deleted file mode 100644 --- a/crates/jacquard-common/src/aturi.rs +++ /dev/null diff --git a/crates/jacquard-common/src/blob.rs b/crates/jacquard-common/src/blob.rs deleted file mode 100644 --- a/crates/jacquard-common/src/blob.rs +++ /dev/null @@ -1,132 +0,0 @@ -use crate::{CowStr, cid::Cid}; -use compact_str::ToCompactString; -#[allow(unused)] -use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; -#[allow(unused)] -use std::{ - borrow::Cow, - fmt, - hash::{Hash, Hasher}, - ops::Deref, - str::FromStr, -}; - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Blob<'b> { - pub r#ref: Cid<'b>, - #[serde(borrow)] - pub mime_type: MimeType<'b>, - pub size: usize, -} - -impl<'r> BlobRef<'r> { - pub fn blob(&self) -> &Blob<'r> { - match self { - BlobRef::Blob(blob) => blob, - } - } -} - -/// Current, typed blob reference. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -#[serde(tag = "$type", rename_all = "lowercase")] -pub enum BlobRef<'r> { - #[serde(borrow)] - Blob(Blob<'r>), -} - -/// Wrapper for file type -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -#[serde(transparent)] -pub struct MimeType<'m>(pub CowStr<'m>); - -impl<'m> MimeType<'m> { - /// Fallible constructor, validates, borrows from input - pub fn new(mime_type: &'m str) -> Result, &'static str> { - Ok(Self(CowStr::Borrowed(mime_type))) - } - - /// Fallible constructor from an existing CowStr, borrows - pub fn from_cowstr(mime_type: CowStr<'m>) -> Result, &'static str> { - Ok(Self(mime_type)) - } - - /// Infallible constructor - pub fn raw(mime_type: &'m str) -> Self { - Self(CowStr::Borrowed(mime_type)) - } - - pub fn as_str(&self) -> &str { - { - let this = &self.0; - this - } - } -} - -impl FromStr for MimeType<'_> { - type Err = &'static str; - - /// Has to take ownership due to the lifetime constraints of the FromStr trait. - fn from_str(s: &str) -> Result { - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) - } -} - -impl<'de, 'b> Deserialize<'de> for MimeType<'b> -where - 'de: 'b, -{ - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Deserialize::deserialize(deserializer)?; - Self::new(value).map_err(D::Error::custom) - } -} - -impl fmt::Display for MimeType<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl<'m> From> for String { - fn from(value: MimeType<'m>) -> Self { - value.0.to_string() - } -} - -impl<'m> From> for CowStr<'m> { - fn from(value: MimeType<'m>) -> Self { - value.0 - } -} - -impl From for MimeType<'static> { - fn from(value: String) -> Self { - Self(CowStr::Owned(value.to_compact_string())) - } -} - -impl<'m> From> for MimeType<'m> { - fn from(value: CowStr<'m>) -> Self { - Self(value) - } -} - -impl AsRef for MimeType<'_> { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl Deref for MimeType<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() - } -} diff --git a/crates/jacquard-common/src/cid.rs b/crates/jacquard-common/src/cid.rs deleted file mode 100644 --- a/crates/jacquard-common/src/cid.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr}; - -use compact_str::ToCompactString; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor}; - -pub use cid::Cid as IpldCid; - -use crate::CowStr; - -/// raw -pub const ATP_CID_CODEC: u64 = 0x55; - -/// SHA-256 -pub const ATP_CID_HASH: u64 = 0x12; - -/// base 32 -pub const ATP_CID_BASE: multibase::Base = multibase::Base::Base32Lower; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -/// Either the string form of a cid or the ipld form -/// For the IPLD form we also cache the string representation for later use. -/// -/// Default on deserialization matches the format (if we get bytes, we try to decode) -pub enum Cid<'c> { - Ipld { cid: IpldCid, s: CowStr<'c> }, - Str(CowStr<'c>), -} - -#[derive(Debug, thiserror::Error, miette::Diagnostic)] -pub enum Error { - #[error("Invalid IPLD CID {:?}", 0)] - Ipld(#[from] cid::Error), - #[error("{:?}", 0)] - Utf8(#[from] std::str::Utf8Error), -} - -impl<'c> Cid<'c> { - pub fn new(cid: &'c [u8]) -> Result { - if let Ok(cid) = IpldCid::try_from(cid.as_ref()) { - Ok(Self::ipld(cid)) - } else { - let cid_str = CowStr::from_utf8(cid)?; - Ok(Self::Str(cid_str)) - } - } - pub fn ipld(cid: IpldCid) -> Self { - let s = CowStr::Owned( - cid.to_string_of_base(ATP_CID_BASE) - .unwrap_or_default() - .to_compact_string(), - ); - Self::Ipld { cid, s } - } - - pub fn str(cid: &'c str) -> Self { - Self::Str(CowStr::Borrowed(cid)) - } - - pub fn cow_str(cid: CowStr<'c>) -> Self { - Self::Str(cid) - } - - pub fn to_ipld(&self) -> Result { - match self { - Cid::Ipld { cid, s: _ } => Ok(cid.clone()), - Cid::Str(cow_str) => IpldCid::try_from(cow_str.as_ref()), - } - } - - pub fn as_str(&self) -> &str { - match self { - Cid::Ipld { cid: _, s } => s.as_ref(), - Cid::Str(cow_str) => cow_str.as_ref(), - } - } -} - -impl std::fmt::Display for Cid<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Cid::Ipld { cid: _, s } => f.write_str(&s), - Cid::Str(cow_str) => f.write_str(&cow_str), - } - } -} - -impl FromStr for Cid<'_> { - type Err = Infallible; - - /// Has to take ownership due to the lifetime constraints of the FromStr trait. - fn from_str(s: &str) -> Result { - Ok(Cid::Str(CowStr::Owned(s.to_compact_string()))) - } -} - -impl Serialize for Cid<'_> { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - Cid::Ipld { cid, s: _ } => cid.serialize(serializer), - Cid::Str(cow_str) => cow_str.serialize(serializer), - } - } -} - -// TODO: take another look at this, see if we can do more borrowed and such -impl<'de> Deserialize<'de> for Cid<'_> { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - struct StringOrBytes(PhantomData T>); - - impl<'de, T> Visitor<'de> for StringOrBytes - where - T: Deserialize<'de> + FromStr + From, - { - type Value = T; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("either valid IPLD CID bytes or a str") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - Ok(FromStr::from_str(v).unwrap()) - } - - fn visit_bytes(self, v: &[u8]) -> Result - where - E: serde::de::Error, - { - let hash = cid::multihash::Multihash::from_bytes(v).map_err(|e| E::custom(e))?; - Ok(T::from(IpldCid::new_v1(ATP_CID_CODEC, hash))) - } - } - - deserializer.deserialize_any(StringOrBytes(PhantomData)) - } -} - -impl From> for String { - fn from(value: Cid) -> Self { - let cow_str = match value { - Cid::Ipld { cid: _, s } => s, - Cid::Str(cow_str) => cow_str, - }; - cow_str.to_string() - } -} - -impl<'d> From> for CowStr<'d> { - fn from(value: Cid<'d>) -> Self { - match value { - Cid::Ipld { cid: _, s } => s, - Cid::Str(cow_str) => cow_str, - } - } -} - -impl From for Cid<'_> { - fn from(value: String) -> Self { - Cid::Str(CowStr::Owned(value.to_compact_string())) - } -} - -impl<'d> From> for Cid<'d> { - fn from(value: CowStr<'d>) -> Self { - Cid::Str(value) - } -} - -impl From for Cid<'_> { - fn from(value: IpldCid) -> Self { - Cid::ipld(value) - } -} - -impl AsRef for Cid<'_> { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl Deref for Cid<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() - } -} diff --git a/crates/jacquard-common/src/cowstr.rs b/crates/jacquard-common/src/cowstr.rs --- a/crates/jacquard-common/src/cowstr.rs +++ b/crates/jacquard-common/src/cowstr.rs @@ -1,11 +1,10 @@ use compact_str::CompactString; -use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; +use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, fmt, hash::{Hash, Hasher}, ops::Deref, - str::FromStr, }; use crate::IntoStatic; @@ -207,56 +206,6 @@ CowStr::Owned(s) => CowStr::Owned(s), } } -} - -/// Common trait implementations for Lexicon string formats that are newtype wrappers -/// around `String`. -macro_rules! string_newtype { - ($name:ident) => { - impl FromStr for $name<'_> { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - Self::new(s) - } - } - - impl<'de> Deserialize<'de> for $name<'de> { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Deserialize::deserialize(deserializer)?; - Self::new(value).map_err(D::Error::custom) - } - } - - impl From<$name<'_>> for String { - fn from(value: $name) -> Self { - value.0.to_string() - } - } - - impl From<$name> for CowStr<'s> { - fn from(value: $name) -> Self { - value.0 - } - } - - impl AsRef for $name<'_> { - fn as_ref(&self) -> &str { - self.as_str() - } - } - - impl Deref for $name<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() - } - } - }; } impl Serialize for CowStr<'_> { diff --git a/crates/jacquard-common/src/did.rs b/crates/jacquard-common/src/did.rs deleted file mode 100644 --- a/crates/jacquard-common/src/did.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::fmt; -use std::sync::LazyLock; -use std::{ops::Deref, str::FromStr}; - -use compact_str::ToCompactString; -use serde::{Deserialize, Deserializer, Serialize, de::Error}; - -use crate::{CowStr, IntoStatic}; -use regex::Regex; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] -#[serde(transparent)] -pub struct Did<'d>(CowStr<'d>); - -pub static DID_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap()); - -impl<'d> Did<'d> { - /// Fallible constructor, validates, borrows from input - pub fn new(did: &'d str) -> Result { - if did.len() > 2048 { - Err("DID too long") - } else if !DID_REGEX.is_match(did) { - Err("Invalid DID") - } else { - Ok(Self(CowStr::Borrowed(did))) - } - } - - /// Fallible constructor from an existing CowStr, clones and takes - pub fn from_cowstr(did: CowStr<'d>) -> Result, &'static str> { - if did.len() > 2048 { - Err("DID too long") - } else if !DID_REGEX.is_match(&did) { - Err("Invalid DID") - } else { - Ok(Self(did.into_static())) - } - } - - /// Infallible constructor for when you *know* the string is a valid DID. - /// Will panic on invalid DIDs. If you're manually decoding atproto records - /// or API values you know are valid (rather than using serde), this is the one to use. - /// The From and From impls use the same logic. - pub fn raw(did: &'d str) -> Self { - if did.len() > 2048 { - panic!("DID too long") - } else if !DID_REGEX.is_match(did) { - panic!("Invalid DID") - } else { - Self(CowStr::Borrowed(did)) - } - } - - /// Infallible constructor for when you *know* the string is a valid DID. - /// Marked unsafe because responsibility for upholding the invariant is on the developer. - pub unsafe fn unchecked(did: &'d str) -> Self { - Self(CowStr::Borrowed(did)) - } - - pub fn as_str(&self) -> &str { - { - let this = &self.0; - this - } - } -} - -impl FromStr for Did<'_> { - type Err = &'static str; - - /// Has to take ownership due to the lifetime constraints of the FromStr trait. - /// Prefer `Did::new()` or `Did::raw` if you want to borrow. - fn from_str(s: &str) -> Result { - Self::from_cowstr(CowStr::Owned(s.to_compact_string())) - } -} - -impl<'de> Deserialize<'de> for Did<'de> { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = Deserialize::deserialize(deserializer)?; - Self::new(value).map_err(D::Error::custom) - } -} - -impl fmt::Display for Did<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl<'d> From> for String { - fn from(value: Did<'d>) -> Self { - value.0.to_string() - } -} - -impl<'d> From> for CowStr<'d> { - fn from(value: Did<'d>) -> Self { - value.0 - } -} - -impl From for Did<'static> { - fn from(value: String) -> Self { - if value.len() > 2048 { - panic!("DID too long") - } else if !DID_REGEX.is_match(&value) { - panic!("Invalid DID") - } else { - Self(CowStr::Owned(value.to_compact_string())) - } - } -} - -impl<'d> From> for Did<'d> { - fn from(value: CowStr<'d>) -> Self { - if value.len() > 2048 { - panic!("DID too long") - } else if !DID_REGEX.is_match(&value) { - panic!("Invalid DID") - } else { - Self(value) - } - } -} - -impl AsRef for Did<'_> { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl Deref for Did<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - self.as_str() - } -} diff --git a/crates/jacquard-common/src/handle.rs b/crates/jacquard-common/src/handle.rs deleted file mode 100644 --- a/crates/jacquard-common/src/handle.rs +++ /dev/null diff --git a/crates/jacquard-common/src/lib.rs b/crates/jacquard-common/src/lib.rs --- a/crates/jacquard-common/src/lib.rs +++ b/crates/jacquard-common/src/lib.rs @@ -1,16 +1,9 @@ -pub mod aturi; #[macro_use] pub mod cowstr; #[macro_use] -pub mod blob; -pub mod cid; - -pub mod did; -pub mod handle; -#[macro_use] pub mod into_static; -pub mod link; -pub mod nsid; + +pub mod types; pub use cowstr::CowStr; pub use into_static::IntoStatic; diff --git a/crates/jacquard-common/src/link.rs b/crates/jacquard-common/src/link.rs deleted file mode 100644 --- a/crates/jacquard-common/src/link.rs +++ /dev/null @@ -1,1 +0,0 @@ -// strongref, blobref(s), cid links diff --git a/crates/jacquard-common/src/nsid.rs b/crates/jacquard-common/src/nsid.rs deleted file mode 100644 --- a/crates/jacquard-common/src/nsid.rs +++ /dev/null diff --git a/crates/jacquard-common/src/types.rs b/crates/jacquard-common/src/types.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types.rs @@ -0,0 +1,11 @@ +pub mod aturi; +pub mod blob; +pub mod cid; +pub mod datetime; +pub mod did; +pub mod handle; +pub mod ident; +pub mod integer; +pub mod link; +pub mod nsid; +pub mod tid; diff --git a/crates/jacquard-common/src/types/aturi.rs b/crates/jacquard-common/src/types/aturi.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/aturi.rs @@ -0,0 +1,148 @@ +use std::fmt; +use std::sync::LazyLock; +use std::{ops::Deref, str::FromStr}; + +use compact_str::ToCompactString; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; + +use crate::{CowStr, IntoStatic}; +use regex::Regex; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] +#[serde(transparent)] +pub struct AtUri<'a>(CowStr<'a>); + +pub static AT_URI_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^$").unwrap()); + +impl<'a> AtUri<'a> { + /// Fallible constructor, validates, borrows from input + pub fn new(uri: &'a str) -> Result { + if uri.len() > 2048 { + Err("AT_URI too long") + } else if !AT_URI_REGEX.is_match(uri) { + Err("Invalid AT_URI") + } else { + Ok(Self(CowStr::Borrowed(uri))) + } + } + + /// Fallible constructor from an existing CowStr, clones and takes + pub fn from_cowstr(uri: CowStr<'a>) -> Result, &'static str> { + if uri.len() > 2048 { + Err("AT_URI too long") + } else if !AT_URI_REGEX.is_match(&uri) { + Err("Invalid AT_URI") + } else { + Ok(Self(uri.into_static())) + } + } + + /// Infallible constructor for when you *know* the string slice is a valid at:// uri. + /// Will panic on invalid URIs. If you're manually decoding atproto records + /// or API values you know are valid (rather than using serde), this is the one to use. + /// The From and From impls use the same logic. + pub fn raw(uri: &'a str) -> Self { + if uri.len() > 2048 { + panic!("AT_URI too long") + } else if !AT_URI_REGEX.is_match(uri) { + panic!("Invalid AT_URI") + } else { + Self(CowStr::Borrowed(uri)) + } + } + + /// Infallible constructor for when you *know* the string is a valid AT_URI. + /// Marked unsafe because responsibility for upholding the invariant is on the developer. + pub unsafe fn unchecked(uri: &'a str) -> Self { + Self(CowStr::Borrowed(uri)) + } + + pub fn as_str(&self) -> &str { + { + let this = &self.0; + this + } + } +} + +impl FromStr for AtUri<'_> { + type Err = &'static str; + + /// Has to take ownership due to the lifetime constraints of the FromStr trait. + /// Prefer `AtUri::new()` or `AtUri::raw` if you want to borrow. + fn from_str(s: &str) -> Result { + Self::from_cowstr(CowStr::Owned(s.to_compact_string())) + } +} + +impl<'ae> Deserialize<'ae> for AtUri<'ae> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'ae>, + { + let value = Deserialize::deserialize(deserializer)?; + Self::new(value).map_err(D::Error::custom) + } +} + +impl fmt::Display for AtUri<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl<'a> From> for String { + fn from(value: AtUri<'a>) -> Self { + value.0.to_string() + } +} + +impl<'s> From<&'s AtUri<'_>> for &'s str { + fn from(value: &'s AtUri<'_>) -> Self { + value.0.as_ref() + } +} + +impl<'a> From> for CowStr<'a> { + fn from(value: AtUri<'a>) -> Self { + value.0 + } +} + +impl From for AtUri<'static> { + fn from(value: String) -> Self { + if value.len() > 2048 { + panic!("AT_URI too long") + } else if !AT_URI_REGEX.is_match(&value) { + panic!("Invalid AT_URI") + } else { + Self(CowStr::Owned(value.to_compact_string())) + } + } +} + +impl<'a> From> for AtUri<'a> { + fn from(value: CowStr<'a>) -> Self { + if value.len() > 2048 { + panic!("AT_URI too long") + } else if !AT_URI_REGEX.is_match(&value) { + panic!("Invalid AT_URI") + } else { + Self(value) + } + } +} + +impl AsRef for AtUri<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for AtUri<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} diff --git a/crates/jacquard-common/src/types/blob.rs b/crates/jacquard-common/src/types/blob.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/blob.rs @@ -0,0 +1,132 @@ +use crate::{CowStr, types::cid::Cid}; +use compact_str::ToCompactString; +#[allow(unused)] +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error}; +#[allow(unused)] +use std::{ + borrow::Cow, + fmt, + hash::{Hash, Hasher}, + ops::Deref, + str::FromStr, +}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Blob<'b> { + pub r#ref: Cid<'b>, + #[serde(borrow)] + pub mime_type: MimeType<'b>, + pub size: usize, +} + +impl<'r> BlobRef<'r> { + pub fn blob(&self) -> &Blob<'r> { + match self { + BlobRef::Blob(blob) => blob, + } + } +} + +/// Current, typed blob reference. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(tag = "$type", rename_all = "lowercase")] +pub enum BlobRef<'r> { + #[serde(borrow)] + Blob(Blob<'r>), +} + +/// Wrapper for file type +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] +#[serde(transparent)] +pub struct MimeType<'m>(pub CowStr<'m>); + +impl<'m> MimeType<'m> { + /// Fallible constructor, validates, borrows from input + pub fn new(mime_type: &'m str) -> Result, &'static str> { + Ok(Self(CowStr::Borrowed(mime_type))) + } + + /// Fallible constructor from an existing CowStr, borrows + pub fn from_cowstr(mime_type: CowStr<'m>) -> Result, &'static str> { + Ok(Self(mime_type)) + } + + /// Infallible constructor + pub fn raw(mime_type: &'m str) -> Self { + Self(CowStr::Borrowed(mime_type)) + } + + pub fn as_str(&self) -> &str { + { + let this = &self.0; + this + } + } +} + +impl FromStr for MimeType<'_> { + type Err = &'static str; + + /// Has to take ownership due to the lifetime constraints of the FromStr trait. + fn from_str(s: &str) -> Result { + Self::from_cowstr(CowStr::Owned(s.to_compact_string())) + } +} + +impl<'de, 'b> Deserialize<'de> for MimeType<'b> +where + 'de: 'b, +{ + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Deserialize::deserialize(deserializer)?; + Self::new(value).map_err(D::Error::custom) + } +} + +impl fmt::Display for MimeType<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl<'m> From> for String { + fn from(value: MimeType<'m>) -> Self { + value.0.to_string() + } +} + +impl<'m> From> for CowStr<'m> { + fn from(value: MimeType<'m>) -> Self { + value.0 + } +} + +impl From for MimeType<'static> { + fn from(value: String) -> Self { + Self(CowStr::Owned(value.to_compact_string())) + } +} + +impl<'m> From> for MimeType<'m> { + fn from(value: CowStr<'m>) -> Self { + Self(value) + } +} + +impl AsRef for MimeType<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for MimeType<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} diff --git a/crates/jacquard-common/src/types/cid.rs b/crates/jacquard-common/src/types/cid.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/cid.rs @@ -0,0 +1,195 @@ +use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr}; + +use compact_str::ToCompactString; +use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor}; + +pub use cid::Cid as IpldCid; + +use crate::CowStr; + +/// raw +pub const ATP_CID_CODEC: u64 = 0x55; + +/// SHA-256 +pub const ATP_CID_HASH: u64 = 0x12; + +/// base 32 +pub const ATP_CID_BASE: multibase::Base = multibase::Base::Base32Lower; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// Either the string form of a cid or the ipld form +/// For the IPLD form we also cache the string representation for later use. +/// +/// Default on deserialization matches the format (if we get bytes, we try to decode) +pub enum Cid<'c> { + Ipld { cid: IpldCid, s: CowStr<'c> }, + Str(CowStr<'c>), +} + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error("Invalid IPLD CID {:?}", 0)] + Ipld(#[from] cid::Error), + #[error("{:?}", 0)] + Utf8(#[from] std::str::Utf8Error), +} + +impl<'c> Cid<'c> { + pub fn new(cid: &'c [u8]) -> Result { + if let Ok(cid) = IpldCid::try_from(cid.as_ref()) { + Ok(Self::ipld(cid)) + } else { + let cid_str = CowStr::from_utf8(cid)?; + Ok(Self::Str(cid_str)) + } + } + pub fn ipld(cid: IpldCid) -> Self { + let s = CowStr::Owned( + cid.to_string_of_base(ATP_CID_BASE) + .unwrap_or_default() + .to_compact_string(), + ); + Self::Ipld { cid, s } + } + + pub fn str(cid: &'c str) -> Self { + Self::Str(CowStr::Borrowed(cid)) + } + + pub fn cow_str(cid: CowStr<'c>) -> Self { + Self::Str(cid) + } + + pub fn to_ipld(&self) -> Result { + match self { + Cid::Ipld { cid, s: _ } => Ok(cid.clone()), + Cid::Str(cow_str) => IpldCid::try_from(cow_str.as_ref()), + } + } + + pub fn as_str(&self) -> &str { + match self { + Cid::Ipld { cid: _, s } => s.as_ref(), + Cid::Str(cow_str) => cow_str.as_ref(), + } + } +} + +impl std::fmt::Display for Cid<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Cid::Ipld { cid: _, s } => f.write_str(&s), + Cid::Str(cow_str) => f.write_str(&cow_str), + } + } +} + +impl FromStr for Cid<'_> { + type Err = Infallible; + + /// Has to take ownership due to the lifetime constraints of the FromStr trait. + fn from_str(s: &str) -> Result { + Ok(Cid::Str(CowStr::Owned(s.to_compact_string()))) + } +} + +impl Serialize for Cid<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Cid::Ipld { cid, s: _ } => cid.serialize(serializer), + Cid::Str(cow_str) => cow_str.serialize(serializer), + } + } +} + +// TODO: take another look at this, see if we can do more borrowed and such +impl<'de> Deserialize<'de> for Cid<'_> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StringOrBytes(PhantomData T>); + + impl<'de, T> Visitor<'de> for StringOrBytes + where + T: Deserialize<'de> + FromStr + From, + { + type Value = T; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("either valid IPLD CID bytes or a str") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(FromStr::from_str(v).unwrap()) + } + + fn visit_bytes(self, v: &[u8]) -> Result + where + E: serde::de::Error, + { + let hash = cid::multihash::Multihash::from_bytes(v).map_err(|e| E::custom(e))?; + Ok(T::from(IpldCid::new_v1(ATP_CID_CODEC, hash))) + } + } + + deserializer.deserialize_any(StringOrBytes(PhantomData)) + } +} + +impl From> for String { + fn from(value: Cid) -> Self { + let cow_str = match value { + Cid::Ipld { cid: _, s } => s, + Cid::Str(cow_str) => cow_str, + }; + cow_str.to_string() + } +} + +impl<'d> From> for CowStr<'d> { + fn from(value: Cid<'d>) -> Self { + match value { + Cid::Ipld { cid: _, s } => s, + Cid::Str(cow_str) => cow_str, + } + } +} + +impl From for Cid<'_> { + fn from(value: String) -> Self { + Cid::Str(CowStr::Owned(value.to_compact_string())) + } +} + +impl<'d> From> for Cid<'d> { + fn from(value: CowStr<'d>) -> Self { + Cid::Str(value) + } +} + +impl From for Cid<'_> { + fn from(value: IpldCid) -> Self { + Cid::ipld(value) + } +} + +impl AsRef for Cid<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for Cid<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} diff --git a/crates/jacquard-common/src/types/datetime.rs b/crates/jacquard-common/src/types/datetime.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/datetime.rs @@ -0,0 +1,166 @@ +use std::sync::LazyLock; +use std::{cmp, str::FromStr}; + +use chrono::DurationRound; +use compact_str::ToCompactString; +use serde::Serializer; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; + +use crate::{CowStr, IntoStatic}; +use regex::Regex; + +pub static ISO8601_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+[0-9]{2}|\-[0-9][1-9]):[0-9]{2})$").unwrap() +}); + +/// A Lexicon timestamp. +#[derive(Clone, Debug, Eq)] +pub struct Datetime { + /// Serialized form. Preserved during parsing to ensure round-trip re-serialization. + serialized: CowStr<'static>, + /// Parsed form. + dt: chrono::DateTime, +} + +impl PartialEq for Datetime { + fn eq(&self, other: &Self) -> bool { + self.dt == other.dt + } +} + +impl Ord for Datetime { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.dt.cmp(&other.dt) + } +} + +impl PartialOrd for Datetime { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Datetime { + /// Returns a `Datetime` which corresponds to the current date and time in UTC. + /// + /// The timestamp uses microsecond precision. + pub fn now() -> Self { + Self::new(chrono::Utc::now().fixed_offset()) + } + + /// Constructs a new Lexicon timestamp. + /// + /// The timestamp is rounded to microsecond precision. + pub fn new(dt: chrono::DateTime) -> Self { + let dt = dt + .duration_round(chrono::Duration::microseconds(1)) + .expect("delta does not exceed limits"); + // This serialization format is compatible with ISO 8601. + let serialized = CowStr::Owned( + dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true) + .to_compact_string(), + ); + Self { serialized, dt } + } + + /// Infallibly parses a new Lexicon timestamp from a compatible str reference + /// + /// Panics if invalid. Use the fallible trait implementations or deserialize for input + /// you cannot reasonably trust to be properly formatted. + pub fn raw_str(s: impl AsRef) -> Self { + let s = s.as_ref(); + if ISO8601_REGEX.is_match(s) { + let dt = chrono::DateTime::parse_from_rfc3339(s).expect("valid ISO8601 time string"); + Self { + serialized: CowStr::Borrowed(s).into_static(), + dt, + } + } else { + panic!("atproto datetime should be valid ISO8601") + } + } + + /// Extracts a string slice containing the entire `Datetime`. + #[inline] + #[must_use] + pub fn as_str(&self) -> &str { + self.serialized.as_ref() + } +} + +impl FromStr for Datetime { + type Err = chrono::ParseError; + + fn from_str(s: &str) -> Result { + // The `chrono` crate only supports RFC 3339 parsing, but Lexicon restricts + // datetimes to the subset that is also valid under ISO 8601. Apply a regex that + // validates enough of the relevant ISO 8601 format that the RFC 3339 parser can + // do the rest. + if ISO8601_REGEX.is_match(s) { + let dt = chrono::DateTime::parse_from_rfc3339(s)?; + Ok(Self { + serialized: CowStr::Borrowed(s).into_static(), + dt, + }) + } else { + // Simulate an invalid `ParseError`. + Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid")) + } + } +} + +impl<'de> Deserialize<'de> for Datetime { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value: String = Deserialize::deserialize(deserializer)?; + Self::from_str(&value).map_err(D::Error::custom) + } +} +impl Serialize for Datetime { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.serialized) + } +} + +impl AsRef> for Datetime { + fn as_ref(&self) -> &chrono::DateTime { + &self.dt + } +} + +impl TryFrom for Datetime { + type Error = chrono::ParseError; + fn try_from(value: String) -> Result { + if ISO8601_REGEX.is_match(&value) { + let dt = chrono::DateTime::parse_from_rfc3339(&value)?; + Ok(Self { + serialized: CowStr::Owned(value.to_compact_string()), + dt, + }) + } else { + // Simulate an invalid `ParseError`. + Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid")) + } + } +} + +impl TryFrom> for Datetime { + type Error = chrono::ParseError; + fn try_from(value: CowStr<'_>) -> Result { + if ISO8601_REGEX.is_match(&value) { + let dt = chrono::DateTime::parse_from_rfc3339(&value)?; + Ok(Self { + serialized: value.into_static(), + dt, + }) + } else { + // Simulate an invalid `ParseError`. + Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid")) + } + } +} diff --git a/crates/jacquard-common/src/types/did.rs b/crates/jacquard-common/src/types/did.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/did.rs @@ -0,0 +1,143 @@ +use std::fmt; +use std::sync::LazyLock; +use std::{ops::Deref, str::FromStr}; + +use compact_str::ToCompactString; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; + +use crate::{CowStr, IntoStatic}; +use regex::Regex; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] +#[serde(transparent)] +pub struct Did<'d>(CowStr<'d>); + +pub static DID_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$").unwrap()); + +impl<'d> Did<'d> { + /// Fallible constructor, validates, borrows from input + pub fn new(did: &'d str) -> Result { + if did.len() > 2048 { + Err("DID too long") + } else if !DID_REGEX.is_match(did) { + Err("Invalid DID") + } else { + Ok(Self(CowStr::Borrowed(did))) + } + } + + /// Fallible constructor from an existing CowStr, takes ownership + pub fn from_cowstr(did: CowStr<'d>) -> Result, &'static str> { + if did.len() > 2048 { + Err("DID too long") + } else if !DID_REGEX.is_match(&did) { + Err("Invalid DID") + } else { + Ok(Self(did.into_static())) + } + } + + /// Infallible constructor for when you *know* the string is a valid DID. + /// Will panic on invalid DIDs. If you're manually decoding atproto records + /// or API values you know are valid (rather than using serde), this is the one to use. + /// The From and From impls use the same logic. + pub fn raw(did: &'d str) -> Self { + if did.len() > 2048 { + panic!("DID too long") + } else if !DID_REGEX.is_match(did) { + panic!("Invalid DID") + } else { + Self(CowStr::Borrowed(did)) + } + } + + /// Infallible constructor for when you *know* the string is a valid DID. + /// Marked unsafe because responsibility for upholding the invariant is on the developer. + pub unsafe fn unchecked(did: &'d str) -> Self { + Self(CowStr::Borrowed(did)) + } + + pub fn as_str(&self) -> &str { + { + let this = &self.0; + this + } + } +} + +impl FromStr for Did<'_> { + type Err = &'static str; + + /// Has to take ownership due to the lifetime constraints of the FromStr trait. + /// Prefer `Did::new()` or `Did::raw` if you want to borrow. + fn from_str(s: &str) -> Result { + Self::from_cowstr(CowStr::Borrowed(s).into_static()) + } +} + +impl<'de> Deserialize<'de> for Did<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Deserialize::deserialize(deserializer)?; + Self::new(value).map_err(D::Error::custom) + } +} + +impl fmt::Display for Did<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl<'d> From> for String { + fn from(value: Did<'d>) -> Self { + value.0.to_string() + } +} + +impl<'d> From> for CowStr<'d> { + fn from(value: Did<'d>) -> Self { + value.0 + } +} + +impl From for Did<'static> { + fn from(value: String) -> Self { + if value.len() > 2048 { + panic!("DID too long") + } else if !DID_REGEX.is_match(&value) { + panic!("Invalid DID") + } else { + Self(CowStr::Owned(value.to_compact_string())) + } + } +} + +impl<'d> From> for Did<'d> { + fn from(value: CowStr<'d>) -> Self { + if value.len() > 2048 { + panic!("DID too long") + } else if !DID_REGEX.is_match(&value) { + panic!("Invalid DID") + } else { + Self(value) + } + } +} + +impl AsRef for Did<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for Did<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} diff --git a/crates/jacquard-common/src/types/handle.rs b/crates/jacquard-common/src/types/handle.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/handle.rs @@ -0,0 +1,160 @@ +use std::fmt; +use std::sync::LazyLock; +use std::{ops::Deref, str::FromStr}; + +use compact_str::ToCompactString; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; + +use crate::{CowStr, IntoStatic}; +use regex::Regex; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)] +#[serde(transparent)] +pub struct Handle<'h>(CowStr<'h>); + +pub static HANDLE_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$").unwrap() +}); + +impl<'h> Handle<'h> { + /// Fallible constructor, validates, borrows from input + /// + /// Accepts (and strips) preceding '@' if present + pub fn new(handle: &'h str) -> Result { + let handle = handle.strip_prefix('@').unwrap_or(handle); + if handle.len() > 2048 { + Err("handle too long") + } else if !HANDLE_REGEX.is_match(handle) { + Err("Invalid handle") + } else { + Ok(Self(CowStr::Borrowed(handle))) + } + } + + /// Fallible constructor from an existing CowStr, takes ownership + /// + /// Accepts (and strips) preceding '@' if present + pub fn from_cowstr(handle: CowStr<'h>) -> Result, &'static str> { + let handle = if let Some(handle) = handle.strip_prefix('@') { + CowStr::Borrowed(handle) + } else { + handle + }; + if handle.len() > 2048 { + Err("handle too long") + } else if !HANDLE_REGEX.is_match(&handle) { + Err("Invalid handle") + } else { + Ok(Self(handle.into_static())) + } + } + + /// Infallible constructor for when you *know* the string is a valid handle. + /// Will panic on invalid handles. If you're manually decoding atproto records + /// or API values you know are valid (rather than using serde), this is the one to use. + /// The From and From impls use the same logic. + /// + /// Accepts (and strips) preceding '@' if present + pub fn raw(handle: &'h str) -> Self { + let handle = handle.strip_prefix('@').unwrap_or(handle); + if handle.len() > 2048 { + panic!("handle too long") + } else if !HANDLE_REGEX.is_match(handle) { + panic!("Invalid handle") + } else { + Self(CowStr::Borrowed(handle)) + } + } + + /// Infallible constructor for when you *know* the string is a valid handle. + /// Marked unsafe because responsibility for upholding the invariant is on the developer. + /// + /// Accepts (and strips) preceding '@' if present + pub unsafe fn unchecked(handle: &'h str) -> Self { + let handle = handle.strip_prefix('@').unwrap_or(handle); + Self(CowStr::Borrowed(handle)) + } + + pub fn as_str(&self) -> &str { + { + let this = &self.0; + this + } + } +} + +impl FromStr for Handle<'_> { + type Err = &'static str; + + /// Has to take ownership due to the lifetime constraints of the FromStr trait. + /// Prefer `Handle::new()` or `Handle::raw` if you want to borrow. + fn from_str(s: &str) -> Result { + Self::from_cowstr(CowStr::Borrowed(s).into_static()) + } +} + +impl<'de> Deserialize<'de> for Handle<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Deserialize::deserialize(deserializer)?; + Self::new(value).map_err(D::Error::custom) + } +} + +impl fmt::Display for Handle<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "@{}", self.0) + } +} + +impl<'h> From> for String { + fn from(value: Handle<'h>) -> Self { + value.0.to_string() + } +} + +impl<'h> From> for CowStr<'h> { + fn from(value: Handle<'h>) -> Self { + value.0 + } +} + +impl From for Handle<'static> { + fn from(value: String) -> Self { + if value.len() > 2048 { + panic!("handle too long") + } else if !HANDLE_REGEX.is_match(&value) { + panic!("Invalid handle") + } else { + Self(CowStr::Owned(value.to_compact_string())) + } + } +} + +impl<'h> From> for Handle<'h> { + fn from(value: CowStr<'h>) -> Self { + if value.len() > 2048 { + panic!("handle too long") + } else if !HANDLE_REGEX.is_match(&value) { + panic!("Invalid handle") + } else { + Self(value) + } + } +} + +impl AsRef for Handle<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for Handle<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} diff --git a/crates/jacquard-common/src/types/ident.rs b/crates/jacquard-common/src/types/ident.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/ident.rs @@ -0,0 +1,148 @@ +use crate::types::did::Did; +use crate::types::handle::Handle; +use std::fmt; +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; + +use crate::CowStr; + +/// An AT Protocol identifier. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] +#[serde(untagged)] +pub enum AtIdentifier<'i> { + #[serde(borrow)] + Did(Did<'i>), + Handle(Handle<'i>), +} + +impl<'i> AtIdentifier<'i> { + /// Fallible constructor, validates, borrows from input + pub fn new(ident: &'i str) -> Result { + if let Ok(did) = ident.parse() { + Ok(AtIdentifier::Did(did)) + } else { + ident.parse().map(AtIdentifier::Handle) + } + } + + /// Fallible constructor from an existing CowStr, borrows + pub fn from_cowstr(ident: CowStr<'i>) -> Result, &'static str> { + if let Ok(did) = ident.parse() { + Ok(AtIdentifier::Did(did)) + } else { + ident.parse().map(AtIdentifier::Handle) + } + } + + /// Infallible constructor for when you *know* the string is a valid identifier. + /// Will panic on invalid identifiers. If you're manually decoding atproto records + /// or API values you know are valid (rather than using serde), this is the one to use. + /// The From and From impls use the same logic. + pub fn raw(ident: &'i str) -> Self { + if let Ok(did) = ident.parse() { + AtIdentifier::Did(did) + } else { + ident + .parse() + .map(AtIdentifier::Handle) + .expect("valid handle") + } + } + + /// Infallible constructor for when you *know* the string is a valid identifier. + /// Marked unsafe because responsibility for upholding the invariant is on the developer. + /// + /// Will validate DIDs, but will treat anything else as a valid handle + pub unsafe fn unchecked(ident: &'i str) -> Self { + if let Ok(did) = ident.parse() { + AtIdentifier::Did(did) + } else { + unsafe { AtIdentifier::Handle(Handle::unchecked(ident)) } + } + } + + pub fn as_str(&self) -> &str { + match self { + AtIdentifier::Did(did) => did.as_str(), + AtIdentifier::Handle(handle) => handle.as_str(), + } + } +} + +impl<'i> From> for AtIdentifier<'i> { + fn from(did: Did<'i>) -> Self { + AtIdentifier::Did(did) + } +} + +impl<'i> From> for AtIdentifier<'i> { + fn from(handle: Handle<'i>) -> Self { + AtIdentifier::Handle(handle) + } +} + +impl FromStr for AtIdentifier<'_> { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + if let Ok(did) = s.parse() { + Ok(AtIdentifier::Did(did)) + } else { + s.parse().map(AtIdentifier::Handle) + } + } +} + +impl fmt::Display for AtIdentifier<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AtIdentifier::Did(did) => did.fmt(f), + AtIdentifier::Handle(handle) => handle.fmt(f), + } + } +} + +impl From for AtIdentifier<'static> { + fn from(value: String) -> Self { + if let Ok(did) = value.parse() { + AtIdentifier::Did(did) + } else { + value + .parse() + .map(AtIdentifier::Handle) + .expect("valid handle") + } + } +} + +impl<'i> From> for AtIdentifier<'i> { + fn from(value: CowStr<'i>) -> Self { + if let Ok(did) = value.parse() { + AtIdentifier::Did(did) + } else { + value + .parse() + .map(AtIdentifier::Handle) + .expect("valid handle") + } + } +} + +impl<'i> From> for String { + fn from(value: AtIdentifier) -> Self { + match value { + AtIdentifier::Did(did) => did.into(), + AtIdentifier::Handle(handle) => handle.into(), + } + } +} + +impl AsRef for AtIdentifier<'_> { + fn as_ref(&self) -> &str { + match self { + AtIdentifier::Did(did) => did.as_ref(), + AtIdentifier::Handle(handle) => handle.as_ref(), + } + } +} diff --git a/crates/jacquard-common/src/types/integer.rs b/crates/jacquard-common/src/types/integer.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/integer.rs @@ -0,0 +1,326 @@ +//! Lexicon integer types with minimum or maximum acceptable values. +//! Copied from [atrium](https://github.com/atrium-rs/atrium/blob/main/atrium-api/src/types/integer.rs), because this they got right + +use std::num::{NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64}; +use std::str::FromStr; + +use serde::{Deserialize, de::Error}; + +macro_rules! uint { + ($primitive:ident, $nz:ident, $lim:ident, $lim_nz:ident, $bounded:ident) => { + /// An unsigned integer with a maximum value of `MAX`. + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)] + #[repr(transparent)] + #[serde(transparent)] + pub struct $lim($primitive); + + impl $lim { + /// The smallest value that can be represented by this limited integer type. + pub const MIN: Self = Self(<$primitive>::MIN); + + /// The largest value that can be represented by this limited integer type. + pub const MAX: Self = Self(MAX); + + fn new(value: $primitive) -> Result { + if value > MAX { + Err(format!("value is greater than {}", MAX)) + } else { + Ok(Self(value)) + } + } + } + + impl FromStr for $lim { + type Err = String; + + fn from_str(src: &str) -> Result { + Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?) + } + } + + impl TryFrom<$primitive> for $lim { + type Error = String; + + fn try_from(value: $primitive) -> Result { + Self::new(value) + } + } + + impl<'de, const MAX: $primitive> Deserialize<'de> for $lim { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom) + } + } + + impl From<$lim> for $primitive { + fn from(value: $lim) -> Self { + value.0 + } + } + + /// An unsigned integer with a minimum value of 1 and a maximum value of `MAX`. + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)] + #[repr(transparent)] + #[serde(transparent)] + pub struct $lim_nz($nz); + + impl $lim_nz { + /// The smallest value that can be represented by this limited non-zero + /// integer type. + pub const MIN: Self = Self($nz::MIN); + + /// The largest value that can be represented by this limited non-zero integer + /// type. + pub const MAX: Self = Self(unsafe { $nz::new_unchecked(MAX) }); + + fn new(value: $primitive) -> Result { + if value > MAX { + Err(format!("value is greater than {}", MAX)) + } else if let Some(value) = $nz::new(value) { + Ok(Self(value)) + } else { + Err("value is zero".into()) + } + } + } + + impl FromStr for $lim_nz { + type Err = String; + + fn from_str(src: &str) -> Result { + Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?) + } + } + + impl TryFrom<$primitive> for $lim_nz { + type Error = String; + + fn try_from(value: $primitive) -> Result { + Self::new(value) + } + } + + impl<'de, const MAX: $primitive> Deserialize<'de> for $lim_nz { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom) + } + } + + impl From<$lim_nz> for $nz { + fn from(value: $lim_nz) -> Self { + value.0 + } + } + + impl From<$lim_nz> for $primitive { + fn from(value: $lim_nz) -> Self { + value.0.into() + } + } + + /// An unsigned integer with a minimum value of `MIN` and a maximum value of `MAX`. + /// + /// `MIN` must be non-zero. + #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)] + #[repr(transparent)] + #[serde(transparent)] + pub struct $bounded($nz); + + impl $bounded { + /// The smallest value that can be represented by this bounded integer type. + pub const MIN: Self = Self(unsafe { $nz::new_unchecked(MIN) }); + + /// The largest value that can be represented by this bounded integer type. + pub const MAX: Self = Self(unsafe { $nz::new_unchecked(MAX) }); + + fn new(value: $primitive) -> Result { + if value < MIN { + Err(format!("value is less than {}", MIN)) + } else if value > MAX { + Err(format!("value is greater than {}", MAX)) + } else if let Some(value) = $nz::new(value) { + Ok(Self(value)) + } else { + Err("value is zero".into()) + } + } + } + + impl TryFrom<$primitive> + for $bounded + { + type Error = String; + + fn try_from(value: $primitive) -> Result { + Self::new(value) + } + } + + impl FromStr for $bounded { + type Err = String; + + fn from_str(src: &str) -> Result { + Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?) + } + } + + impl<'de, const MIN: $primitive, const MAX: $primitive> Deserialize<'de> + for $bounded + { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom) + } + } + + impl From<$bounded> for $nz { + fn from(value: $bounded) -> Self { + value.0 + } + } + + impl From<$bounded> for $primitive { + fn from(value: $bounded) -> Self { + value.0.into() + } + } + }; +} + +uint!(u8, NonZeroU8, LimitedU8, LimitedNonZeroU8, BoundedU8); +uint!(u16, NonZeroU16, LimitedU16, LimitedNonZeroU16, BoundedU16); +uint!(u32, NonZeroU32, LimitedU32, LimitedNonZeroU32, BoundedU32); +uint!(u64, NonZeroU64, LimitedU64, LimitedNonZeroU64, BoundedU64); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn u8_min_max() { + assert_eq!(Ok(LimitedU8::<10>::MIN), 0.try_into()); + assert_eq!(Ok(LimitedU8::<10>::MAX), 10.try_into()); + assert_eq!(Ok(LimitedNonZeroU8::<10>::MIN), 1.try_into()); + assert_eq!(Ok(LimitedNonZeroU8::<10>::MAX), 10.try_into()); + assert_eq!(Ok(BoundedU8::<7, 10>::MIN), 7.try_into()); + assert_eq!(Ok(BoundedU8::<7, 10>::MAX), 10.try_into()); + } + + #[test] + fn u8_from_str() { + { + type LU8 = LimitedU8<10>; + assert_eq!(Ok(LU8::MIN), "0".parse()); + assert_eq!(Ok(LU8::MAX), "10".parse()); + assert_eq!(Err("value is greater than 10".into()), "11".parse::()); + } + { + type LU8 = LimitedNonZeroU8<10>; + assert_eq!(Ok(LU8::MIN), "1".parse()); + assert_eq!(Ok(LU8::MAX), "10".parse()); + assert_eq!(Err("value is greater than 10".into()), "11".parse::()); + } + { + type BU8 = BoundedU8<7, 10>; + assert_eq!(Err("value is less than 7".into()), "6".parse::()); + assert_eq!(Ok(BU8::MIN), "7".parse()); + assert_eq!(Ok(BU8::MAX), "10".parse()); + assert_eq!(Err("value is greater than 10".into()), "11".parse::()); + } + } + + #[test] + fn deserialize_u8_from_str() { + { + #[derive(Deserialize, Debug)] + struct Foo { + bar: LimitedU8<10>, + } + + match serde_json::from_str::(r#"{"bar": 0}"#) { + Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MIN), + Err(e) => panic!("failed to deserialize: {e}"), + } + match serde_json::from_str::(r#"{"bar": "0"}"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert!(e.to_string().contains("invalid type: string")), + } + match serde_html_form::from_str::(r#"bar=0"#) { + Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MIN), + Err(e) => panic!("failed to deserialize: {e}"), + } + match serde_html_form::from_str::(r#"bar=10"#) { + Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MAX), + Err(e) => panic!("failed to deserialize: {e}"), + } + match serde_html_form::from_str::(r#"bar=11"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is greater than 10"), + } + } + + { + #[derive(Deserialize, Debug)] + struct Foo { + bar: LimitedNonZeroU8<10>, + } + + match serde_json::from_str::(r#"{"bar": 0}"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is zero at line 1 column 10"), + } + match serde_json::from_str::(r#"{"bar": "0"}"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert!(e.to_string().contains("invalid type: string")), + } + match serde_html_form::from_str::(r#"bar=0"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is zero"), + } + match serde_html_form::from_str::(r#"bar=10"#) { + Ok(foo) => assert_eq!(foo.bar, LimitedNonZeroU8::<10>::MAX), + Err(e) => panic!("failed to deserialize: {e}"), + } + match serde_html_form::from_str::(r#"bar=11"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is greater than 10"), + } + } + + { + #[derive(Deserialize, Debug)] + struct Foo { + bar: BoundedU8<1, 10>, + } + + match serde_json::from_str::(r#"{"bar": 0}"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is less than 1 at line 1 column 10"), + } + match serde_json::from_str::(r#"{"bar": "0"}"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert!(e.to_string().contains("invalid type: string")), + } + match serde_html_form::from_str::(r#"bar=0"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is less than 1"), + } + match serde_html_form::from_str::(r#"bar=10"#) { + Ok(foo) => assert_eq!(foo.bar, BoundedU8::<1, 10>::MAX), + Err(e) => panic!("failed to deserialize: {e}"), + } + match serde_html_form::from_str::(r#"bar=11"#) { + Ok(_) => panic!("deserialization should fail"), + Err(e) => assert_eq!(e.to_string(), "value is greater than 10"), + } + } + } +} diff --git a/crates/jacquard-common/src/types/link.rs b/crates/jacquard-common/src/types/link.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/link.rs @@ -0,0 +1,1 @@ +// strongref, blobref(s), cid links diff --git a/crates/jacquard-common/src/types/nsid.rs b/crates/jacquard-common/src/types/nsid.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/nsid.rs diff --git a/crates/jacquard-common/src/types/tid.rs b/crates/jacquard-common/src/types/tid.rs new file mode 100644 --- /dev/null +++ b/crates/jacquard-common/src/types/tid.rs @@ -0,0 +1,202 @@ +use std::fmt; +use std::sync::LazyLock; +use std::{ops::Deref, str::FromStr}; + +use compact_str::{CompactString, ToCompactString}; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; + +use crate::types::integer::LimitedU32; +use crate::{CowStr, IntoStatic}; +use regex::Regex; + +fn s32_encode(mut i: u64) -> CowStr<'static> { + const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz"; + + let mut s = CompactString::with_capacity(13); + for _ in 0..13 { + let c = i & 0x1F; + s.push(S32_CHAR[c as usize] as char); + + i >>= 5; + } + + // Reverse the string to convert it to big-endian format. + CowStr::Owned(s.chars().rev().collect()) +} + +static TID_REGEX: LazyLock = LazyLock::new(|| { + Regex::new(r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$").unwrap() +}); + +/// A [Timestamp Identifier]. +/// +/// [Timestamp Identifier]: https://atproto.com/specs/tid +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)] +#[serde(transparent)] +pub struct Tid<'t>(CowStr<'t>); + +impl<'t> Tid<'t> { + /// Parses a `TID` from the given string. + pub fn new(tid: &'t str) -> Result { + if tid.len() != 13 { + Err("TID must be 13 characters") + } else if !TID_REGEX.is_match(&tid) { + Err("Invalid TID") + } else { + Ok(Self(CowStr::Owned(tid.to_compact_string()))) + } + } + + /// Fallible constructor from an existing CowStr, takes ownership + pub fn from_cowstr(tid: CowStr<'t>) -> Result, &'static str> { + if tid.len() != 13 { + Err("TID must be 13 characters") + } else if !TID_REGEX.is_match(&tid) { + Err("Invalid TID") + } else { + Ok(Self(tid.into_static())) + } + } + + /// Infallible constructor for when you *know* the string is a valid TID. + /// Will panic on invalid TID. If you're manually decoding atproto records + /// or API values you know are valid (rather than using serde), this is the one to use. + /// The From and From impls use the same logic. + pub fn raw(tid: &'t str) -> Self { + if tid.len() != 13 { + panic!("TID must be 13 characters") + } else if !TID_REGEX.is_match(&tid) { + panic!("Invalid TID") + } else { + Self(CowStr::Borrowed(tid)) + } + } + + /// Infallible constructor for when you *know* the string is a valid TID. + /// Marked unsafe because responsibility for upholding the invariant is on the developer. + pub unsafe fn unchecked(tid: &'t str) -> Self { + Self(CowStr::Borrowed(tid)) + } + + /// Construct a new timestamp with the specified clock ID. + /// + /// If you have multiple clock sources, you can use `clkid` to distinguish between them + /// and hint to other implementations that the timestamp cannot be compared with other + /// timestamps from other sources. + /// If you are only using a single clock source, you can just specify `0` for `clkid`. + pub fn from_datetime(clkid: LimitedU32<1023>, time: chrono::DateTime) -> Self { + let time = time.timestamp_micros() as u64; + + // The TID is laid out as follows: + // 0TTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTCCCCCCCCCC + let tid = (time << 10) & 0x7FFF_FFFF_FFFF_FC00 | (Into::::into(clkid) as u64 & 0x3FF); + Self(s32_encode(tid)) + } + + /// Construct a new [Tid] that represents the current time. + /// + /// If you have multiple clock sources, you can use `clkid` to distinguish between them + /// and hint to other implementations that the timestamp cannot be compared with other + /// timestamps from other sources. + /// If you are only using a single clock source, you can just specify `0` for `clkid`. + /// + /// _Warning:_ It's possible that this function will return the same time more than once. + /// If it's important that these values be unique, you will want to repeatedly call this + /// function until a different time is returned. + pub fn now(clkid: LimitedU32<1023>) -> Self { + Self::from_datetime(clkid, chrono::Utc::now()) + } + + /// Construct a new [Tid] that represents the current time with clkid 0. + /// + /// _Warning:_ It's possible that this function will return the same time more than once. + /// If it's important that these values be unique, you will want to repeatedly call this + /// function until a different time is returned. + pub fn now_0() -> Self { + Self::from_datetime(LimitedU32::from_str("0").unwrap(), chrono::Utc::now()) + } + + /// Returns the TID as a string slice. + pub fn as_str(&self) -> &str { + { + let this = &self.0; + this + } + } +} + +impl FromStr for Tid<'_> { + type Err = &'static str; + + /// Has to take ownership due to the lifetime constraints of the FromStr trait. + /// Prefer `Did::new()` or `Did::raw` if you want to borrow. + fn from_str(s: &str) -> Result { + Self::from_cowstr(CowStr::Borrowed(s).into_static()) + } +} + +impl<'de> Deserialize<'de> for Tid<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Deserialize::deserialize(deserializer)?; + Self::new(value).map_err(D::Error::custom) + } +} + +impl fmt::Display for Tid<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl<'t> From> for String { + fn from(value: Tid<'t>) -> Self { + value.0.to_string() + } +} + +impl<'t> From> for CowStr<'t> { + fn from(value: Tid<'t>) -> Self { + value.0 + } +} + +impl From for Tid<'static> { + fn from(value: String) -> Self { + if value.len() != 13 { + panic!("TID must be 13 characters") + } else if !TID_REGEX.is_match(&value) { + panic!("Invalid TID") + } else { + Self(CowStr::Owned(value.to_compact_string())) + } + } +} + +impl<'t> From> for Tid<'t> { + fn from(value: CowStr<'t>) -> Self { + if value.len() != 13 { + panic!("TID must be 13 characters") + } else if !TID_REGEX.is_match(&value) { + panic!("Invalid TID") + } else { + Self(value) + } + } +} + +impl AsRef for Tid<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Deref for Tid<'_> { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.as_str() + } +} -- tangled.sh