+23
-27
Cargo.lock
+23
-27
Cargo.lock
···
83
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
84
85
[[package]]
86
-
name = "bumpalo"
87
-
version = "3.19.0"
88
source = "registry+https://github.com/rust-lang/crates.io-index"
89
-
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
90
91
[[package]]
92
-
name = "castaway"
93
-
version = "0.2.4"
94
source = "registry+https://github.com/rust-lang/crates.io-index"
95
-
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
96
-
dependencies = [
97
-
"rustversion",
98
-
]
99
100
[[package]]
101
name = "cc"
···
112
version = "1.0.3"
113
source = "registry+https://github.com/rust-lang/crates.io-index"
114
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
115
116
[[package]]
117
name = "chrono"
···
187
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
188
189
[[package]]
190
-
name = "compact_str"
191
-
version = "0.9.0"
192
-
source = "registry+https://github.com/rust-lang/crates.io-index"
193
-
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
194
-
dependencies = [
195
-
"castaway",
196
-
"cfg-if",
197
-
"itoa",
198
-
"rustversion",
199
-
"ryu",
200
-
"static_assertions",
201
-
]
202
-
203
-
[[package]]
204
name = "core-foundation-sys"
205
version = "0.8.7"
206
source = "registry+https://github.com/rust-lang/crates.io-index"
···
334
dependencies = [
335
"chrono",
336
"cid",
337
-
"compact_str",
338
"miette",
339
"multibase",
340
"multihash",
···
342
"serde",
343
"serde_html_form",
344
"serde_json",
345
"thiserror",
346
]
347
···
575
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
576
577
[[package]]
578
-
name = "static_assertions"
579
-
version = "1.1.0"
580
source = "registry+https://github.com/rust-lang/crates.io-index"
581
-
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
582
583
[[package]]
584
name = "strsim"
···
83
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
84
85
[[package]]
86
+
name = "borsh"
87
+
version = "1.5.7"
88
source = "registry+https://github.com/rust-lang/crates.io-index"
89
+
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
90
+
dependencies = [
91
+
"cfg_aliases",
92
+
]
93
94
[[package]]
95
+
name = "bumpalo"
96
+
version = "3.19.0"
97
source = "registry+https://github.com/rust-lang/crates.io-index"
98
+
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
99
100
[[package]]
101
name = "cc"
···
112
version = "1.0.3"
113
source = "registry+https://github.com/rust-lang/crates.io-index"
114
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
115
+
116
+
[[package]]
117
+
name = "cfg_aliases"
118
+
version = "0.2.1"
119
+
source = "registry+https://github.com/rust-lang/crates.io-index"
120
+
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
121
122
[[package]]
123
name = "chrono"
···
193
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
194
195
[[package]]
196
name = "core-foundation-sys"
197
version = "0.8.7"
198
source = "registry+https://github.com/rust-lang/crates.io-index"
···
326
dependencies = [
327
"chrono",
328
"cid",
329
"miette",
330
"multibase",
331
"multihash",
···
333
"serde",
334
"serde_html_form",
335
"serde_json",
336
+
"smol_str",
337
"thiserror",
338
]
339
···
567
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
568
569
[[package]]
570
+
name = "smol_str"
571
+
version = "0.3.2"
572
source = "registry+https://github.com/rust-lang/crates.io-index"
573
+
checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
574
+
dependencies = [
575
+
"borsh",
576
+
"serde",
577
+
]
578
579
[[package]]
580
name = "strsim"
+7
Cargo.toml
+7
Cargo.toml
···
7
edition = "2024"
8
version = "0.1.0"
9
authors = ["Orual <orual@nonbinary.computer>"]
10
+
repository = "https://tangled.org/@nonbinary.computer/jacquard"
11
+
keywords = ["atproto", "at protocol", "bluesky", "api", "client"]
12
+
categories = ["api-bindings", "web-programming::http-client"]
13
+
readme = "README.md"
14
+
documentation = "https://docs.rs/jacquard"
15
+
exclude = [".direnv"]
16
+
17
18
description = "A simple Rust project using Nix"
19
+26
README.md
+26
README.md
···
29
```
30
31
There's also a [`justfile`](https://just.systems/) for Makefile-esque commands to be run inside of the devShell, and you can generally `cargo ...` or `just ...` whatever just fine if you don't want to use Nix and have the prerequisites installed.
32
+
33
+
34
+
35
+
### String types
36
+
Something of a note to self. Developing a pattern with the string types (may macro-ify at some point). Each needs:
37
+
- new(): constructing from a string slice with the right lifetime that borrows
38
+
- new_owned(): constructing from an impl AsRef<str>, taking ownership
39
+
- new_static(): construction from a &'static str, using SmolStr's/CowStr's new_static() constructor to not allocate
40
+
- raw(): same as new() but panics instead of erroring
41
+
- unchecked(): same as new() but doesn't validate. marked unsafe.
42
+
- as_str(): does what it says on the tin
43
+
#### Traits:
44
+
- Serialize + Deserialize (custom impl for latter, sometimes for former)
45
+
- FromStr
46
+
- Display
47
+
- Debug, PartialEq, Eq, Hash, Clone
48
+
- From<T> for String, CowStr, SmolStr,
49
+
- From<String>, From<CowStr>, From<SmolStr>, or TryFrom if likely enough to fail in practice to make panics common
50
+
- AsRef<str>
51
+
- Deref with Target = str (usually)
52
+
53
+
Use `#[repr(transparent)]` as much as possible. Main exception is at-uri type and components.
54
+
Use SmolStr directly as the inner type if most or all of the instances will be under 24 bytes, save lifetime headaches.
55
+
Use CowStr for longer to allow for borrowing from input.
56
+
57
+
TODO: impl IntoStatic trait to take ownership of string types
+1
-1
crates/jacquard-common/Cargo.toml
+1
-1
crates/jacquard-common/Cargo.toml
···
8
[dependencies]
9
chrono = "0.4.42"
10
cid = { version = "0.11.1", features = ["serde", "std"] }
11
-
compact_str = "0.9.0"
12
miette = "7.6.0"
13
multibase = "0.9.1"
14
multihash = "0.19.3"
···
16
serde = { version = "1.0.227", features = ["derive"] }
17
serde_html_form = "0.2.8"
18
serde_json = "1.0.145"
19
thiserror = "2.0.16"
···
8
[dependencies]
9
chrono = "0.4.42"
10
cid = { version = "0.11.1", features = ["serde", "std"] }
11
miette = "7.6.0"
12
multibase = "0.9.1"
13
multihash = "0.19.3"
···
15
serde = { version = "1.0.227", features = ["derive"] }
16
serde_html_form = "0.2.8"
17
serde_json = "1.0.145"
18
+
smol_str = { version = "0.3.2", features = ["serde"] }
19
thiserror = "2.0.16"
+13
-9
crates/jacquard-common/src/cowstr.rs
+13
-9
crates/jacquard-common/src/cowstr.rs
···
1
-
use compact_str::CompactString;
2
use serde::{Deserialize, Serialize};
3
use std::{
4
borrow::Cow,
5
fmt,
···
10
use crate::IntoStatic;
11
12
/// Shamelessly copied from https://github.com/bearcove/merde
13
-
/// A copy-on-write string type that uses [`CompactString`] for
14
/// the "owned" variant.
15
///
16
/// The standard [`Cow`] type cannot be used, since
17
-
/// `<str as ToOwned>::Owned` is `String`, and not `CompactString`.
18
#[derive(Clone)]
19
pub enum CowStr<'s> {
20
Borrowed(&'s str),
21
-
Owned(CompactString),
22
}
23
24
impl CowStr<'static> {
···
26
/// if the `compact_str` feature is disabled, or if the string is longer
27
/// than `MAX_INLINE_SIZE`.
28
pub fn copy_from_str(s: &str) -> Self {
29
-
Self::Owned(CompactString::from(s))
30
}
31
}
32
···
38
39
#[inline]
40
pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> {
41
-
Ok(Self::Owned(CompactString::from_utf8(s)?))
42
}
43
44
#[inline]
45
pub fn from_utf8_lossy(s: &'s [u8]) -> Self {
46
-
Self::Owned(CompactString::from_utf8_lossy(s))
47
}
48
49
/// # Safety
···
51
/// This function is unsafe because it does not check that the bytes are valid UTF-8.
52
#[inline]
53
pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self {
54
-
unsafe { Self::Owned(CompactString::from_utf8_unchecked(s)) }
55
}
56
}
57
···
133
fn from(s: CowStr<'_>) -> Self {
134
match s {
135
CowStr::Borrowed(s) => s.into(),
136
-
CowStr::Owned(s) => s.into(),
137
}
138
}
139
}
···
1
use serde::{Deserialize, Serialize};
2
+
use smol_str::SmolStr;
3
use std::{
4
borrow::Cow,
5
fmt,
···
10
use crate::IntoStatic;
11
12
/// Shamelessly copied from https://github.com/bearcove/merde
13
+
/// A copy-on-write immutable string type that uses [`SmolStr`] for
14
/// the "owned" variant.
15
///
16
/// The standard [`Cow`] type cannot be used, since
17
+
/// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`.
18
#[derive(Clone)]
19
pub enum CowStr<'s> {
20
Borrowed(&'s str),
21
+
Owned(SmolStr),
22
}
23
24
impl CowStr<'static> {
···
26
/// if the `compact_str` feature is disabled, or if the string is longer
27
/// than `MAX_INLINE_SIZE`.
28
pub fn copy_from_str(s: &str) -> Self {
29
+
Self::Owned(SmolStr::from(s))
30
+
}
31
+
32
+
pub fn new_static(s: &'static str) -> Self {
33
+
Self::Owned(SmolStr::new_static(s))
34
}
35
}
36
···
42
43
#[inline]
44
pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> {
45
+
Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s)?)))
46
}
47
48
#[inline]
49
pub fn from_utf8_lossy(s: &'s [u8]) -> Self {
50
+
Self::Owned(String::from_utf8_lossy(&s).into())
51
}
52
53
/// # Safety
···
55
/// This function is unsafe because it does not check that the bytes are valid UTF-8.
56
#[inline]
57
pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self {
58
+
unsafe { Self::Owned(SmolStr::new(std::str::from_utf8_unchecked(s))) }
59
}
60
}
61
···
137
fn from(s: CowStr<'_>) -> Self {
138
match s {
139
CowStr::Borrowed(s) => s.into(),
140
+
CowStr::Owned(s) => String::from(s).into_boxed_str(),
141
}
142
}
143
}
+8
crates/jacquard-common/src/types.rs
+8
crates/jacquard-common/src/types.rs
···
1
pub mod aturi;
2
pub mod blob;
3
pub mod cid;
4
+
pub mod collection;
5
pub mod datetime;
6
pub mod did;
7
pub mod handle;
···
9
pub mod integer;
10
pub mod link;
11
pub mod nsid;
12
+
pub mod recordkey;
13
pub mod tid;
14
+
15
+
/// Trait for a constant string literal type
16
+
pub trait Literal: Clone + Copy + PartialEq + Eq + Send + Sync + 'static {
17
+
/// The string literal
18
+
const LITERAL: &'static str;
19
+
}
+200
-73
crates/jacquard-common/src/types/aturi.rs
+200
-73
crates/jacquard-common/src/types/aturi.rs
···
1
use std::fmt;
2
use std::sync::LazyLock;
3
use std::{ops::Deref, str::FromStr};
4
5
-
use compact_str::ToCompactString;
6
-
use serde::{Deserialize, Deserializer, Serialize, de::Error};
7
8
-
use crate::{CowStr, IntoStatic};
9
-
use regex::Regex;
10
11
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)]
12
-
#[serde(transparent)]
13
-
pub struct AtUri<'a>(CowStr<'a>);
14
15
-
pub static AT_URI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^$").unwrap());
16
17
-
impl<'a> AtUri<'a> {
18
/// Fallible constructor, validates, borrows from input
19
-
pub fn new(uri: &'a str) -> Result<Self, &'static str> {
20
-
if uri.len() > 2048 {
21
-
Err("AT_URI too long")
22
-
} else if !AT_URI_REGEX.is_match(uri) {
23
-
Err("Invalid AT_URI")
24
} else {
25
-
Ok(Self(CowStr::Borrowed(uri)))
26
}
27
}
28
29
-
/// Fallible constructor from an existing CowStr, clones and takes
30
-
pub fn from_cowstr(uri: CowStr<'a>) -> Result<AtUri<'a>, &'static str> {
31
-
if uri.len() > 2048 {
32
-
Err("AT_URI too long")
33
-
} else if !AT_URI_REGEX.is_match(&uri) {
34
-
Err("Invalid AT_URI")
35
} else {
36
-
Ok(Self(uri.into_static()))
37
}
38
}
39
40
-
/// Infallible constructor for when you *know* the string slice is a valid at:// uri.
41
-
/// Will panic on invalid URIs. If you're manually decoding atproto records
42
-
/// or API values you know are valid (rather than using serde), this is the one to use.
43
-
/// The From<String> and From<CowStr> impls use the same logic.
44
-
pub fn raw(uri: &'a str) -> Self {
45
-
if uri.len() > 2048 {
46
-
panic!("AT_URI too long")
47
-
} else if !AT_URI_REGEX.is_match(uri) {
48
-
panic!("Invalid AT_URI")
49
} else {
50
-
Self(CowStr::Borrowed(uri))
51
}
52
}
53
54
-
/// Infallible constructor for when you *know* the string is a valid AT_URI.
55
-
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
56
-
pub unsafe fn unchecked(uri: &'a str) -> Self {
57
-
Self(CowStr::Borrowed(uri))
58
}
59
60
pub fn as_str(&self) -> &str {
61
{
62
-
let this = &self.0;
63
this
64
}
65
}
···
69
type Err = &'static str;
70
71
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
72
-
/// Prefer `AtUri::new()` or `AtUri::raw` if you want to borrow.
73
fn from_str(s: &str) -> Result<Self, Self::Err> {
74
-
Self::from_cowstr(CowStr::Owned(s.to_compact_string()))
75
}
76
}
77
78
-
impl<'ae> Deserialize<'ae> for AtUri<'ae> {
79
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80
where
81
-
D: Deserializer<'ae>,
82
{
83
let value = Deserialize::deserialize(deserializer)?;
84
Self::new(value).map_err(D::Error::custom)
85
}
86
}
87
88
-
impl fmt::Display for AtUri<'_> {
89
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90
-
f.write_str(&self.0)
91
}
92
}
93
94
-
impl<'a> From<AtUri<'a>> for String {
95
-
fn from(value: AtUri<'a>) -> Self {
96
-
value.0.to_string()
97
}
98
}
99
100
-
impl<'s> From<&'s AtUri<'_>> for &'s str {
101
-
fn from(value: &'s AtUri<'_>) -> Self {
102
-
value.0.as_ref()
103
}
104
}
105
106
-
impl<'a> From<AtUri<'a>> for CowStr<'a> {
107
-
fn from(value: AtUri<'a>) -> Self {
108
-
value.0
109
}
110
}
111
112
-
impl From<String> for AtUri<'static> {
113
-
fn from(value: String) -> Self {
114
-
if value.len() > 2048 {
115
-
panic!("AT_URI too long")
116
-
} else if !AT_URI_REGEX.is_match(&value) {
117
-
panic!("Invalid AT_URI")
118
-
} else {
119
-
Self(CowStr::Owned(value.to_compact_string()))
120
-
}
121
}
122
}
123
124
-
impl<'a> From<CowStr<'a>> for AtUri<'a> {
125
-
fn from(value: CowStr<'a>) -> Self {
126
-
if value.len() > 2048 {
127
-
panic!("AT_URI too long")
128
-
} else if !AT_URI_REGEX.is_match(&value) {
129
-
panic!("Invalid AT_URI")
130
-
} else {
131
-
Self(value)
132
-
}
133
}
134
}
135
136
impl AsRef<str> for AtUri<'_> {
137
fn as_ref(&self) -> &str {
138
-
self.as_str()
139
}
140
}
141
···
143
type Target = str;
144
145
fn deref(&self) -> &Self::Target {
146
-
self.as_str()
147
}
148
}
···
1
+
use crate::CowStr;
2
+
use crate::types::ident::AtIdentifier;
3
+
use crate::types::nsid::Nsid;
4
+
use crate::types::recordkey::{RecordKey, Rkey};
5
+
use regex::Regex;
6
+
use serde::Serializer;
7
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
8
+
use smol_str::ToSmolStr;
9
use std::fmt;
10
use std::sync::LazyLock;
11
use std::{ops::Deref, str::FromStr};
12
13
+
/// at:// URI type
14
+
///
15
+
/// based on the regex here: https://github.com/bluesky-social/atproto/blob/main/packages/syntax/src/aturi_validation.ts
16
+
///
17
+
/// Doesn't support the query segment, but then neither does the Typescript SDK
18
+
///
19
+
/// TODO: support IntoStatic on string types. For composites like this where all borrow from (present) input,
20
+
/// perhaps use some careful unsafe to launder the lifetimes.
21
+
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
22
+
pub struct AtUri<'u> {
23
+
uri: CowStr<'u>,
24
+
pub authority: AtIdentifier<'u>,
25
+
pub path: Option<UriPath<'u>>,
26
+
pub fragment: Option<CowStr<'u>>,
27
+
}
28
29
+
/// at:// URI path component (current subset)
30
+
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
31
+
pub struct UriPath<'u> {
32
+
pub collection: Nsid<'u>,
33
+
pub rkey: Option<RecordKey<Rkey<'u>>>,
34
+
}
35
36
+
pub type UriPathBuf = UriPath<'static>;
37
38
+
pub static ATURI_REGEX: LazyLock<Regex> = LazyLock::new(|| {
39
+
Regex::new(r##"^at://(?<authority>[a-zA-Z0-9._:%-]+)(/(?<collection>[a-zA-Z0-9-.]+)(/(?<rkey>[a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(#(?<fragment>/[a-zA-Z0-9._~:@!$&%')(*+,;=-[]/\]*))?$"##).unwrap()
40
+
});
41
42
+
impl<'u> AtUri<'u> {
43
/// Fallible constructor, validates, borrows from input
44
+
pub fn new(uri: &'u str) -> Result<Self, &'static str> {
45
+
if let Some(parts) = ATURI_REGEX.captures(uri) {
46
+
if let Some(authority) = parts.name("authority") {
47
+
let authority = AtIdentifier::new(authority.as_str())?;
48
+
let path = if let Some(collection) = parts.name("collection") {
49
+
let collection = Nsid::new(collection.as_str())?;
50
+
let rkey = if let Some(rkey) = parts.name("rkey") {
51
+
let rkey = RecordKey::from(Rkey::new(rkey.as_str())?);
52
+
Some(rkey)
53
+
} else {
54
+
None
55
+
};
56
+
Some(UriPath { collection, rkey })
57
+
} else {
58
+
None
59
+
};
60
+
let fragment = parts.name("fragment").map(|fragment| {
61
+
let fragment = CowStr::Borrowed(fragment.as_str());
62
+
fragment
63
+
});
64
+
Ok(AtUri {
65
+
uri: CowStr::Borrowed(uri),
66
+
authority,
67
+
path,
68
+
fragment,
69
+
})
70
+
} else {
71
+
Err("at:// URI missing authority")
72
+
}
73
} else {
74
+
Err("Invalid at:// URI via regex")
75
}
76
}
77
78
+
pub fn new_owned(uri: impl AsRef<str>) -> Result<Self, &'static str> {
79
+
let uri = uri.as_ref();
80
+
if let Some(parts) = ATURI_REGEX.captures(uri) {
81
+
if let Some(authority) = parts.name("authority") {
82
+
let authority = AtIdentifier::new_owned(authority.as_str())?;
83
+
let path = if let Some(collection) = parts.name("collection") {
84
+
let collection = Nsid::new_owned(collection.as_str())?;
85
+
let rkey = if let Some(rkey) = parts.name("rkey") {
86
+
let rkey = RecordKey::from(Rkey::new_owned(rkey.as_str())?);
87
+
Some(rkey)
88
+
} else {
89
+
None
90
+
};
91
+
Some(UriPath { collection, rkey })
92
+
} else {
93
+
None
94
+
};
95
+
let fragment = parts.name("fragment").map(|fragment| {
96
+
let fragment = CowStr::Owned(fragment.as_str().to_smolstr());
97
+
fragment
98
+
});
99
+
Ok(AtUri {
100
+
uri: CowStr::Owned(uri.to_smolstr()),
101
+
authority,
102
+
path,
103
+
fragment,
104
+
})
105
+
} else {
106
+
Err("at:// URI missing authority")
107
+
}
108
} else {
109
+
Err("Invalid at:// URI via regex")
110
}
111
}
112
113
+
pub fn new_static(uri: &'static str) -> Result<AtUri<'static>, &'static str> {
114
+
let uri = uri.as_ref();
115
+
if let Some(parts) = ATURI_REGEX.captures(uri) {
116
+
if let Some(authority) = parts.name("authority") {
117
+
let authority = AtIdentifier::new_static(authority.as_str())?;
118
+
let path = if let Some(collection) = parts.name("collection") {
119
+
let collection = Nsid::new_static(collection.as_str())?;
120
+
let rkey = if let Some(rkey) = parts.name("rkey") {
121
+
let rkey = RecordKey::from(Rkey::new_static(rkey.as_str())?);
122
+
Some(rkey)
123
+
} else {
124
+
None
125
+
};
126
+
Some(UriPath { collection, rkey })
127
+
} else {
128
+
None
129
+
};
130
+
let fragment = parts.name("fragment").map(|fragment| {
131
+
let fragment = CowStr::new_static(fragment.as_str());
132
+
fragment
133
+
});
134
+
Ok(AtUri {
135
+
uri: CowStr::new_static(uri),
136
+
authority,
137
+
path,
138
+
fragment,
139
+
})
140
+
} else {
141
+
Err("at:// URI missing authority")
142
+
}
143
} else {
144
+
Err("Invalid at:// URI via regex")
145
}
146
}
147
148
+
pub unsafe fn unchecked(uri: &'u str) -> Self {
149
+
if let Some(parts) = ATURI_REGEX.captures(uri) {
150
+
if let Some(authority) = parts.name("authority") {
151
+
let authority = unsafe { AtIdentifier::unchecked(authority.as_str()) };
152
+
let path = if let Some(collection) = parts.name("collection") {
153
+
let collection = unsafe { Nsid::unchecked(collection.as_str()) };
154
+
let rkey = if let Some(rkey) = parts.name("rkey") {
155
+
let rkey = RecordKey::from(unsafe { Rkey::unchecked(rkey.as_str()) });
156
+
Some(rkey)
157
+
} else {
158
+
None
159
+
};
160
+
Some(UriPath { collection, rkey })
161
+
} else {
162
+
None
163
+
};
164
+
let fragment = parts.name("fragment").map(|fragment| {
165
+
let fragment = CowStr::Borrowed(fragment.as_str());
166
+
fragment
167
+
});
168
+
AtUri {
169
+
uri: CowStr::Borrowed(uri),
170
+
authority,
171
+
path,
172
+
fragment,
173
+
}
174
+
} else {
175
+
Self {
176
+
uri: CowStr::Borrowed(uri),
177
+
authority: unsafe { AtIdentifier::unchecked(uri) },
178
+
path: None,
179
+
fragment: None,
180
+
}
181
+
}
182
+
} else {
183
+
Self {
184
+
uri: CowStr::Borrowed(uri),
185
+
authority: unsafe { AtIdentifier::unchecked(uri) },
186
+
path: None,
187
+
fragment: None,
188
+
}
189
+
}
190
}
191
192
pub fn as_str(&self) -> &str {
193
{
194
+
let this = &self.uri;
195
this
196
}
197
}
···
201
type Err = &'static str;
202
203
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
204
+
/// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow.
205
fn from_str(s: &str) -> Result<Self, Self::Err> {
206
+
Self::new_owned(s)
207
}
208
}
209
210
+
impl<'de> Deserialize<'de> for AtUri<'de> {
211
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
212
where
213
+
D: Deserializer<'de>,
214
{
215
let value = Deserialize::deserialize(deserializer)?;
216
Self::new(value).map_err(D::Error::custom)
217
}
218
}
219
220
+
impl Serialize for AtUri<'_> {
221
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
222
+
where
223
+
S: Serializer,
224
+
{
225
+
serializer.serialize_str(&self.uri)
226
}
227
}
228
229
+
impl fmt::Display for AtUri<'_> {
230
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231
+
f.write_str(&self.uri)
232
}
233
}
234
235
+
impl<'d> From<AtUri<'d>> for String {
236
+
fn from(value: AtUri<'d>) -> Self {
237
+
value.uri.to_string()
238
}
239
}
240
241
+
impl<'d> From<AtUri<'d>> for CowStr<'d> {
242
+
fn from(value: AtUri<'d>) -> Self {
243
+
value.uri
244
}
245
}
246
247
+
impl TryFrom<String> for AtUri<'static> {
248
+
type Error = &'static str;
249
+
250
+
fn try_from(value: String) -> Result<Self, Self::Error> {
251
+
Self::new_owned(&value)
252
}
253
}
254
255
+
impl<'d> TryFrom<CowStr<'d>> for AtUri<'d> {
256
+
type Error = &'static str;
257
+
/// TODO: rewrite to avoid taking ownership/cloning
258
+
fn try_from(value: CowStr<'d>) -> Result<Self, Self::Error> {
259
+
Self::new_owned(value)
260
}
261
}
262
263
impl AsRef<str> for AtUri<'_> {
264
fn as_ref(&self) -> &str {
265
+
&self.uri.as_ref()
266
}
267
}
268
···
270
type Target = str;
271
272
fn deref(&self) -> &Self::Target {
273
+
self.uri.as_ref()
274
}
275
}
+23
-5
crates/jacquard-common/src/types/blob.rs
+23
-5
crates/jacquard-common/src/types/blob.rs
···
1
-
use crate::{CowStr, types::cid::Cid};
2
-
use compact_str::ToCompactString;
3
#[allow(unused)]
4
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
5
#[allow(unused)]
6
use std::{
7
borrow::Cow,
···
39
/// Wrapper for file type
40
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
41
#[serde(transparent)]
42
pub struct MimeType<'m>(pub CowStr<'m>);
43
44
impl<'m> MimeType<'m> {
45
/// Fallible constructor, validates, borrows from input
46
pub fn new(mime_type: &'m str) -> Result<MimeType<'m>, &'static str> {
47
Ok(Self(CowStr::Borrowed(mime_type)))
48
}
49
50
/// Fallible constructor from an existing CowStr, borrows
···
66
}
67
68
impl FromStr for MimeType<'_> {
69
-
type Err = &'static str;
70
71
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
72
fn from_str(s: &str) -> Result<Self, Self::Err> {
73
-
Self::from_cowstr(CowStr::Owned(s.to_compact_string()))
74
}
75
}
76
···
107
108
impl From<String> for MimeType<'static> {
109
fn from(value: String) -> Self {
110
-
Self(CowStr::Owned(value.to_compact_string()))
111
}
112
}
113
···
1
+
use crate::{CowStr, IntoStatic, types::cid::Cid};
2
#[allow(unused)]
3
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
4
+
use smol_str::ToSmolStr;
5
+
use std::convert::Infallible;
6
#[allow(unused)]
7
use std::{
8
borrow::Cow,
···
40
/// Wrapper for file type
41
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
42
#[serde(transparent)]
43
+
#[repr(transparent)]
44
pub struct MimeType<'m>(pub CowStr<'m>);
45
46
impl<'m> MimeType<'m> {
47
/// Fallible constructor, validates, borrows from input
48
pub fn new(mime_type: &'m str) -> Result<MimeType<'m>, &'static str> {
49
Ok(Self(CowStr::Borrowed(mime_type)))
50
+
}
51
+
52
+
pub fn new_owned(mime_type: impl AsRef<str>) -> Self {
53
+
Self(CowStr::Owned(mime_type.as_ref().to_smolstr()))
54
+
}
55
+
56
+
pub fn new_static(mime_type: &'static str) -> Self {
57
+
Self(CowStr::new_static(mime_type))
58
}
59
60
/// Fallible constructor from an existing CowStr, borrows
···
76
}
77
78
impl FromStr for MimeType<'_> {
79
+
type Err = Infallible;
80
81
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
82
fn from_str(s: &str) -> Result<Self, Self::Err> {
83
+
Ok(Self::new_owned(s))
84
+
}
85
+
}
86
+
87
+
impl IntoStatic for MimeType<'_> {
88
+
type Output = MimeType<'static>;
89
+
90
+
fn into_static(self) -> Self::Output {
91
+
MimeType(self.0.into_static())
92
}
93
}
94
···
125
126
impl From<String> for MimeType<'static> {
127
fn from(value: String) -> Self {
128
+
Self(CowStr::Owned(value.to_smolstr()))
129
}
130
}
131
+21
-10
crates/jacquard-common/src/types/cid.rs
+21
-10
crates/jacquard-common/src/types/cid.rs
···
1
-
use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr};
2
-
3
-
use compact_str::ToCompactString;
4
-
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
5
-
6
pub use cid::Cid as IpldCid;
7
-
8
-
use crate::CowStr;
9
10
/// raw
11
pub const ATP_CID_CODEC: u64 = 0x55;
···
47
let s = CowStr::Owned(
48
cid.to_string_of_base(ATP_CID_BASE)
49
.unwrap_or_default()
50
-
.to_compact_string(),
51
);
52
Self::Ipld { cid, s }
53
}
···
89
90
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
91
fn from_str(s: &str) -> Result<Self, Self::Err> {
92
-
Ok(Cid::Str(CowStr::Owned(s.to_compact_string())))
93
}
94
}
95
···
164
165
impl From<String> for Cid<'_> {
166
fn from(value: String) -> Self {
167
-
Cid::Str(CowStr::Owned(value.to_compact_string()))
168
}
169
}
170
···
1
+
use crate::{CowStr, IntoStatic};
2
pub use cid::Cid as IpldCid;
3
+
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Visitor};
4
+
use smol_str::ToSmolStr;
5
+
use std::{convert::Infallible, fmt, marker::PhantomData, ops::Deref, str::FromStr};
6
7
/// raw
8
pub const ATP_CID_CODEC: u64 = 0x55;
···
44
let s = CowStr::Owned(
45
cid.to_string_of_base(ATP_CID_BASE)
46
.unwrap_or_default()
47
+
.to_smolstr(),
48
);
49
Self::Ipld { cid, s }
50
}
···
86
87
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
88
fn from_str(s: &str) -> Result<Self, Self::Err> {
89
+
Ok(Cid::Str(CowStr::Owned(s.to_smolstr())))
90
+
}
91
+
}
92
+
93
+
impl IntoStatic for Cid<'_> {
94
+
type Output = Cid<'static>;
95
+
96
+
fn into_static(self) -> Self::Output {
97
+
match self {
98
+
Cid::Ipld { cid, s } => Cid::Ipld {
99
+
cid,
100
+
s: s.into_static(),
101
+
},
102
+
Cid::Str(cow_str) => Cid::Str(cow_str.into_static()),
103
+
}
104
}
105
}
106
···
175
176
impl From<String> for Cid<'_> {
177
fn from(value: String) -> Self {
178
+
Cid::Str(CowStr::Owned(value.to_smolstr()))
179
}
180
}
181
+52
crates/jacquard-common/src/types/collection.rs
+52
crates/jacquard-common/src/types/collection.rs
···
···
1
+
use core::fmt;
2
+
3
+
use serde::{Serialize, de};
4
+
5
+
use crate::types::{
6
+
aturi::UriPath,
7
+
nsid::Nsid,
8
+
recordkey::{RecordKey, RecordKeyType, Rkey},
9
+
};
10
+
11
+
/// Trait for a collection of records that can be stored in a repository.
12
+
///
13
+
/// The records all have the same Lexicon schema.
14
+
pub trait Collection: fmt::Debug {
15
+
/// The NSID for the Lexicon that defines the schema of records in this collection.
16
+
const NSID: &'static str;
17
+
18
+
/// This collection's record type.
19
+
type Record: fmt::Debug + de::DeserializeOwned + Serialize;
20
+
21
+
/// Returns the [`Nsid`] for the Lexicon that defines the schema of records in this
22
+
/// collection.
23
+
///
24
+
/// This is a convenience method that parses [`Self::NSID`].
25
+
///
26
+
/// # Panics
27
+
///
28
+
/// Panics if [`Self::NSID`] is not a valid NSID.
29
+
///
30
+
/// [`Nsid`]: string::Nsid
31
+
fn nsid() -> crate::types::nsid::Nsid<'static> {
32
+
Nsid::new_static(Self::NSID).expect("should be valid NSID")
33
+
}
34
+
35
+
/// Returns the repo path for a record in this collection with the given record key.
36
+
///
37
+
/// Per the [Repo Data Structure v3] specification:
38
+
/// > Repo paths currently have a fixed structure of `<collection>/<record-key>`. This
39
+
/// > means a valid, normalized [`Nsid`], followed by a `/`, followed by a valid
40
+
/// > [`RecordKey`].
41
+
///
42
+
/// [Repo Data Structure v3]: https://atproto.com/specs/repository#repo-data-structure-v3
43
+
/// [`Nsid`]: string::Nsid
44
+
fn repo_path<'u, T: RecordKeyType>(
45
+
rkey: &'u crate::types::recordkey::RecordKey<T>,
46
+
) -> UriPath<'u> {
47
+
UriPath {
48
+
collection: Self::nsid(),
49
+
rkey: Some(RecordKey::from(Rkey::raw(rkey.as_ref()))),
50
+
}
51
+
}
52
+
}
+5
-6
crates/jacquard-common/src/types/datetime.rs
+5
-6
crates/jacquard-common/src/types/datetime.rs
···
1
-
use std::sync::LazyLock;
2
-
use std::{cmp, str::FromStr};
3
-
4
use chrono::DurationRound;
5
-
use compact_str::ToCompactString;
6
use serde::Serializer;
7
use serde::{Deserialize, Deserializer, Serialize, de::Error};
8
9
use crate::{CowStr, IntoStatic};
10
use regex::Regex;
···
58
// This serialization format is compatible with ISO 8601.
59
let serialized = CowStr::Owned(
60
dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
61
-
.to_compact_string(),
62
);
63
Self { serialized, dt }
64
}
···
139
if ISO8601_REGEX.is_match(&value) {
140
let dt = chrono::DateTime::parse_from_rfc3339(&value)?;
141
Ok(Self {
142
-
serialized: CowStr::Owned(value.to_compact_string()),
143
dt,
144
})
145
} else {
···
1
use chrono::DurationRound;
2
use serde::Serializer;
3
use serde::{Deserialize, Deserializer, Serialize, de::Error};
4
+
use smol_str::ToSmolStr;
5
+
use std::sync::LazyLock;
6
+
use std::{cmp, str::FromStr};
7
8
use crate::{CowStr, IntoStatic};
9
use regex::Regex;
···
57
// This serialization format is compatible with ISO 8601.
58
let serialized = CowStr::Owned(
59
dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
60
+
.to_smolstr(),
61
);
62
Self { serialized, dt }
63
}
···
138
if ISO8601_REGEX.is_match(&value) {
139
let dt = chrono::DateTime::parse_from_rfc3339(&value)?;
140
Ok(Self {
141
+
serialized: CowStr::Owned(value.to_smolstr()),
142
dt,
143
})
144
} else {
+53
-14
crates/jacquard-common/src/types/did.rs
+53
-14
crates/jacquard-common/src/types/did.rs
···
1
use std::fmt;
2
use std::sync::LazyLock;
3
use std::{ops::Deref, str::FromStr};
4
5
-
use compact_str::ToCompactString;
6
-
use serde::{Deserialize, Deserializer, Serialize, de::Error};
7
-
8
-
use crate::{CowStr, IntoStatic};
9
-
use regex::Regex;
10
-
11
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)]
12
#[serde(transparent)]
13
pub struct Did<'d>(CowStr<'d>);
14
15
pub static DID_REGEX: LazyLock<Regex> =
···
18
impl<'d> Did<'d> {
19
/// Fallible constructor, validates, borrows from input
20
pub fn new(did: &'d str) -> Result<Self, &'static str> {
21
if did.len() > 2048 {
22
Err("DID too long")
23
} else if !DID_REGEX.is_match(did) {
···
27
}
28
}
29
30
-
/// Fallible constructor from an existing CowStr, takes ownership
31
-
pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> {
32
if did.len() > 2048 {
33
Err("DID too long")
34
-
} else if !DID_REGEX.is_match(&did) {
35
Err("Invalid DID")
36
} else {
37
-
Ok(Self(did.into_static()))
38
}
39
}
40
···
43
/// or API values you know are valid (rather than using serde), this is the one to use.
44
/// The From<String> and From<CowStr> impls use the same logic.
45
pub fn raw(did: &'d str) -> Self {
46
if did.len() > 2048 {
47
panic!("DID too long")
48
} else if !DID_REGEX.is_match(did) {
···
72
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
73
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
74
fn from_str(s: &str) -> Result<Self, Self::Err> {
75
-
Self::from_cowstr(CowStr::Borrowed(s).into_static())
76
}
77
}
78
···
92
}
93
}
94
95
impl<'d> From<Did<'d>> for String {
96
fn from(value: Did<'d>) -> Self {
97
value.0.to_string()
···
106
107
impl From<String> for Did<'static> {
108
fn from(value: String) -> Self {
109
if value.len() > 2048 {
110
panic!("DID too long")
111
} else if !DID_REGEX.is_match(&value) {
112
panic!("Invalid DID")
113
} else {
114
-
Self(CowStr::Owned(value.to_compact_string()))
115
}
116
}
117
}
118
119
impl<'d> From<CowStr<'d>> for Did<'d> {
120
fn from(value: CowStr<'d>) -> Self {
121
if value.len() > 2048 {
122
panic!("DID too long")
123
} else if !DID_REGEX.is_match(&value) {
124
panic!("Invalid DID")
125
} else {
126
-
Self(value)
127
}
128
}
129
}
···
1
+
use crate::{CowStr, IntoStatic};
2
+
use regex::Regex;
3
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
4
+
use smol_str::ToSmolStr;
5
use std::fmt;
6
use std::sync::LazyLock;
7
use std::{ops::Deref, str::FromStr};
8
9
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
10
#[serde(transparent)]
11
+
#[repr(transparent)]
12
pub struct Did<'d>(CowStr<'d>);
13
14
pub static DID_REGEX: LazyLock<Regex> =
···
17
impl<'d> Did<'d> {
18
/// Fallible constructor, validates, borrows from input
19
pub fn new(did: &'d str) -> Result<Self, &'static str> {
20
+
let did = did.strip_prefix("at://").unwrap_or(did);
21
if did.len() > 2048 {
22
Err("DID too long")
23
} else if !DID_REGEX.is_match(did) {
···
27
}
28
}
29
30
+
/// Fallible constructor, validates, takes ownership
31
+
pub fn new_owned(did: impl AsRef<str>) -> Result<Self, &'static str> {
32
+
let did = did.as_ref();
33
+
let did = did.strip_prefix("at://").unwrap_or(did);
34
if did.len() > 2048 {
35
Err("DID too long")
36
+
} else if !DID_REGEX.is_match(did) {
37
+
Err("Invalid DID")
38
+
} else {
39
+
Ok(Self(CowStr::Owned(did.to_smolstr())))
40
+
}
41
+
}
42
+
43
+
/// Fallible constructor, validates, doesn't allocate
44
+
pub fn new_static(did: &'static str) -> Result<Self, &'static str> {
45
+
let did = did.strip_prefix("at://").unwrap_or(did);
46
+
if did.len() > 2048 {
47
+
Err("DID too long")
48
+
} else if !DID_REGEX.is_match(did) {
49
Err("Invalid DID")
50
} else {
51
+
Ok(Self(CowStr::new_static(did)))
52
}
53
}
54
···
57
/// or API values you know are valid (rather than using serde), this is the one to use.
58
/// The From<String> and From<CowStr> impls use the same logic.
59
pub fn raw(did: &'d str) -> Self {
60
+
let did = did.strip_prefix("at://").unwrap_or(did);
61
if did.len() > 2048 {
62
panic!("DID too long")
63
} else if !DID_REGEX.is_match(did) {
···
87
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
88
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
89
fn from_str(s: &str) -> Result<Self, Self::Err> {
90
+
Self::new_owned(s)
91
+
}
92
+
}
93
+
94
+
impl IntoStatic for Did<'_> {
95
+
type Output = Did<'static>;
96
+
97
+
fn into_static(self) -> Self::Output {
98
+
Did(self.0.into_static())
99
}
100
}
101
···
115
}
116
}
117
118
+
impl fmt::Debug for Did<'_> {
119
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120
+
write!(f, "at://{}", self.0)
121
+
}
122
+
}
123
+
124
impl<'d> From<Did<'d>> for String {
125
fn from(value: Did<'d>) -> Self {
126
value.0.to_string()
···
135
136
impl From<String> for Did<'static> {
137
fn from(value: String) -> Self {
138
+
let value = if let Some(did) = value.strip_prefix("at://") {
139
+
CowStr::Borrowed(did)
140
+
} else {
141
+
value.into()
142
+
};
143
if value.len() > 2048 {
144
panic!("DID too long")
145
} else if !DID_REGEX.is_match(&value) {
146
panic!("Invalid DID")
147
} else {
148
+
Self(value.into_static())
149
}
150
}
151
}
152
153
impl<'d> From<CowStr<'d>> for Did<'d> {
154
fn from(value: CowStr<'d>) -> Self {
155
+
let value = if let Some(did) = value.strip_prefix("at://") {
156
+
CowStr::Borrowed(did)
157
+
} else {
158
+
value
159
+
};
160
if value.len() > 2048 {
161
panic!("DID too long")
162
} else if !DID_REGEX.is_match(&value) {
163
panic!("Invalid DID")
164
} else {
165
+
Self(value.into_static())
166
}
167
}
168
}
+88
-30
crates/jacquard-common/src/types/handle.rs
+88
-30
crates/jacquard-common/src/types/handle.rs
···
1
use std::fmt;
2
use std::sync::LazyLock;
3
use std::{ops::Deref, str::FromStr};
4
5
-
use compact_str::ToCompactString;
6
-
use serde::{Deserialize, Deserializer, Serialize, de::Error};
7
-
8
-
use crate::{CowStr, IntoStatic};
9
-
use regex::Regex;
10
-
11
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)]
12
#[serde(transparent)]
13
pub struct Handle<'h>(CowStr<'h>);
14
15
pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
16
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()
17
});
18
19
impl<'h> Handle<'h> {
20
/// Fallible constructor, validates, borrows from input
21
///
22
/// Accepts (and strips) preceding '@' if present
23
pub fn new(handle: &'h str) -> Result<Self, &'static str> {
24
-
let handle = handle.strip_prefix('@').unwrap_or(handle);
25
-
if handle.len() > 2048 {
26
Err("handle too long")
27
} else if !HANDLE_REGEX.is_match(handle) {
28
Err("Invalid handle")
···
31
}
32
}
33
34
-
/// Fallible constructor from an existing CowStr, takes ownership
35
-
///
36
-
/// Accepts (and strips) preceding '@' if present
37
-
pub fn from_cowstr(handle: CowStr<'h>) -> Result<Handle<'h>, &'static str> {
38
-
let handle = if let Some(handle) = handle.strip_prefix('@') {
39
-
CowStr::Borrowed(handle)
40
-
} else {
41
-
handle
42
-
};
43
-
if handle.len() > 2048 {
44
Err("handle too long")
45
-
} else if !HANDLE_REGEX.is_match(&handle) {
46
Err("Invalid handle")
47
} else {
48
-
Ok(Self(handle.into_static()))
49
}
50
}
51
52
/// Infallible constructor for when you *know* the string is a valid handle.
53
/// Will panic on invalid handles. If you're manually decoding atproto records
54
/// or API values you know are valid (rather than using serde), this is the one to use.
···
56
///
57
/// Accepts (and strips) preceding '@' if present
58
pub fn raw(handle: &'h str) -> Self {
59
-
let handle = handle.strip_prefix('@').unwrap_or(handle);
60
-
if handle.len() > 2048 {
61
panic!("handle too long")
62
} else if !HANDLE_REGEX.is_match(handle) {
63
panic!("Invalid handle")
···
71
///
72
/// Accepts (and strips) preceding '@' if present
73
pub unsafe fn unchecked(handle: &'h str) -> Self {
74
-
let handle = handle.strip_prefix('@').unwrap_or(handle);
75
Self(CowStr::Borrowed(handle))
76
}
77
···
89
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
90
/// Prefer `Handle::new()` or `Handle::raw` if you want to borrow.
91
fn from_str(s: &str) -> Result<Self, Self::Err> {
92
-
Self::from_cowstr(CowStr::Borrowed(s).into_static())
93
}
94
}
95
···
105
106
impl fmt::Display for Handle<'_> {
107
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108
-
write!(f, "@{}", self.0)
109
}
110
}
111
···
123
124
impl From<String> for Handle<'static> {
125
fn from(value: String) -> Self {
126
-
if value.len() > 2048 {
127
panic!("handle too long")
128
} else if !HANDLE_REGEX.is_match(&value) {
129
panic!("Invalid handle")
130
} else {
131
-
Self(CowStr::Owned(value.to_compact_string()))
132
}
133
}
134
}
135
136
impl<'h> From<CowStr<'h>> for Handle<'h> {
137
fn from(value: CowStr<'h>) -> Self {
138
-
if value.len() > 2048 {
139
panic!("handle too long")
140
} else if !HANDLE_REGEX.is_match(&value) {
141
panic!("Invalid handle")
142
} else {
143
-
Self(value)
144
}
145
}
146
}
···
1
+
use crate::{CowStr, IntoStatic};
2
+
use regex::Regex;
3
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
4
+
use smol_str::ToSmolStr;
5
use std::fmt;
6
use std::sync::LazyLock;
7
use std::{ops::Deref, str::FromStr};
8
9
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
10
#[serde(transparent)]
11
+
#[repr(transparent)]
12
pub struct Handle<'h>(CowStr<'h>);
13
14
pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
15
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()
16
});
17
18
+
/// AT Protocol handle
19
impl<'h> Handle<'h> {
20
/// Fallible constructor, validates, borrows from input
21
///
22
/// Accepts (and strips) preceding '@' if present
23
pub fn new(handle: &'h str) -> Result<Self, &'static str> {
24
+
let handle = handle
25
+
.strip_prefix("at://")
26
+
.unwrap_or(handle)
27
+
.strip_prefix('@')
28
+
.unwrap_or(handle);
29
+
if handle.len() > 253 {
30
Err("handle too long")
31
} else if !HANDLE_REGEX.is_match(handle) {
32
Err("Invalid handle")
···
35
}
36
}
37
38
+
/// Fallible constructor, validates, takes ownership
39
+
pub fn new_owned(handle: impl AsRef<str>) -> Result<Self, &'static str> {
40
+
let handle = handle.as_ref();
41
+
let handle = handle
42
+
.strip_prefix("at://")
43
+
.unwrap_or(handle)
44
+
.strip_prefix('@')
45
+
.unwrap_or(handle);
46
+
if handle.len() > 253 {
47
Err("handle too long")
48
+
} else if !HANDLE_REGEX.is_match(handle) {
49
Err("Invalid handle")
50
} else {
51
+
Ok(Self(CowStr::Owned(handle.to_smolstr())))
52
}
53
}
54
55
+
/// Fallible constructor, validates, doesn't allocate
56
+
pub fn new_static(handle: &'static str) -> Result<Self, &'static str> {
57
+
let handle = handle
58
+
.strip_prefix("at://")
59
+
.unwrap_or(handle)
60
+
.strip_prefix('@')
61
+
.unwrap_or(handle);
62
+
if handle.len() > 253 {
63
+
Err("handle too long")
64
+
} else if !HANDLE_REGEX.is_match(handle) {
65
+
Err("Invalid handle")
66
+
} else {
67
+
Ok(Self(CowStr::new_static(handle)))
68
+
}
69
+
}
70
/// Infallible constructor for when you *know* the string is a valid handle.
71
/// Will panic on invalid handles. If you're manually decoding atproto records
72
/// or API values you know are valid (rather than using serde), this is the one to use.
···
74
///
75
/// Accepts (and strips) preceding '@' if present
76
pub fn raw(handle: &'h str) -> Self {
77
+
let handle = handle
78
+
.strip_prefix("at://")
79
+
.unwrap_or(handle)
80
+
.strip_prefix('@')
81
+
.unwrap_or(handle);
82
+
if handle.len() > 253 {
83
panic!("handle too long")
84
} else if !HANDLE_REGEX.is_match(handle) {
85
panic!("Invalid handle")
···
93
///
94
/// Accepts (and strips) preceding '@' if present
95
pub unsafe fn unchecked(handle: &'h str) -> Self {
96
+
let handle = handle
97
+
.strip_prefix("at://")
98
+
.unwrap_or(handle)
99
+
.strip_prefix('@')
100
+
.unwrap_or(handle);
101
Self(CowStr::Borrowed(handle))
102
}
103
···
115
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
116
/// Prefer `Handle::new()` or `Handle::raw` if you want to borrow.
117
fn from_str(s: &str) -> Result<Self, Self::Err> {
118
+
Self::new_owned(s)
119
+
}
120
+
}
121
+
122
+
impl IntoStatic for Handle<'_> {
123
+
type Output = Handle<'static>;
124
+
125
+
fn into_static(self) -> Self::Output {
126
+
Handle(self.0.into_static())
127
}
128
}
129
···
139
140
impl fmt::Display for Handle<'_> {
141
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142
+
write!(f, "{}", self.0)
143
+
}
144
+
}
145
+
146
+
impl fmt::Debug for Handle<'_> {
147
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148
+
write!(f, "at://{}", self.0)
149
}
150
}
151
···
163
164
impl From<String> for Handle<'static> {
165
fn from(value: String) -> Self {
166
+
let value = if let Some(handle) = value
167
+
.strip_prefix("at://")
168
+
.unwrap_or(&value)
169
+
.strip_prefix('@')
170
+
{
171
+
CowStr::Borrowed(handle)
172
+
} else {
173
+
value.into()
174
+
};
175
+
if value.len() > 253 {
176
panic!("handle too long")
177
} else if !HANDLE_REGEX.is_match(&value) {
178
panic!("Invalid handle")
179
} else {
180
+
Self(value.into_static())
181
}
182
}
183
}
184
185
impl<'h> From<CowStr<'h>> for Handle<'h> {
186
fn from(value: CowStr<'h>) -> Self {
187
+
let value = if let Some(handle) = value
188
+
.strip_prefix("at://")
189
+
.unwrap_or(&value)
190
+
.strip_prefix('@')
191
+
{
192
+
CowStr::Borrowed(handle)
193
+
} else {
194
+
value
195
+
};
196
+
if value.len() > 253 {
197
panic!("handle too long")
198
} else if !HANDLE_REGEX.is_match(&value) {
199
panic!("Invalid handle")
200
} else {
201
+
Self(value.into_static())
202
}
203
}
204
}
+26
-5
crates/jacquard-common/src/types/ident.rs
+26
-5
crates/jacquard-common/src/types/ident.rs
···
1
-
use crate::types::did::Did;
2
use crate::types::handle::Handle;
3
use std::fmt;
4
use std::str::FromStr;
5
···
26
}
27
}
28
29
-
/// Fallible constructor from an existing CowStr, borrows
30
-
pub fn from_cowstr(ident: CowStr<'i>) -> Result<AtIdentifier<'i>, &'static str> {
31
-
if let Ok(did) = ident.parse() {
32
Ok(AtIdentifier::Did(did))
33
} else {
34
-
ident.parse().map(AtIdentifier::Handle)
35
}
36
}
37
···
90
Ok(AtIdentifier::Did(did))
91
} else {
92
s.parse().map(AtIdentifier::Handle)
93
}
94
}
95
}
···
1
use crate::types::handle::Handle;
2
+
use crate::{IntoStatic, types::did::Did};
3
use std::fmt;
4
use std::str::FromStr;
5
···
26
}
27
}
28
29
+
/// Fallible constructor, validates, takes ownership
30
+
pub fn new_owned(ident: impl AsRef<str>) -> Result<Self, &'static str> {
31
+
let ident = ident.as_ref();
32
+
if let Ok(did) = Did::new_owned(ident) {
33
Ok(AtIdentifier::Did(did))
34
} else {
35
+
Ok(AtIdentifier::Handle(Handle::new_owned(ident)?))
36
+
}
37
+
}
38
+
39
+
/// Fallible constructor, validates, doesn't allocate
40
+
pub fn new_static(ident: &'static str) -> Result<AtIdentifier<'static>, &'static str> {
41
+
if let Ok(did) = Did::new_static(ident) {
42
+
Ok(AtIdentifier::Did(did))
43
+
} else {
44
+
Ok(AtIdentifier::Handle(Handle::new_static(ident)?))
45
}
46
}
47
···
100
Ok(AtIdentifier::Did(did))
101
} else {
102
s.parse().map(AtIdentifier::Handle)
103
+
}
104
+
}
105
+
}
106
+
107
+
impl IntoStatic for AtIdentifier<'_> {
108
+
type Output = AtIdentifier<'static>;
109
+
110
+
fn into_static(self) -> Self::Output {
111
+
match self {
112
+
AtIdentifier::Did(did) => AtIdentifier::Did(did.into_static()),
113
+
AtIdentifier::Handle(handle) => AtIdentifier::Handle(handle.into_static()),
114
}
115
}
116
}
+209
crates/jacquard-common/src/types/nsid.rs
+209
crates/jacquard-common/src/types/nsid.rs
···
1
+
use crate::types::recordkey::RecordKeyType;
2
+
use crate::{CowStr, IntoStatic};
3
+
use regex::Regex;
4
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
5
+
use smol_str::{SmolStr, ToSmolStr};
6
+
use std::fmt;
7
+
use std::sync::LazyLock;
8
+
use std::{ops::Deref, str::FromStr};
9
10
+
/// Namespaced Identifier (NSID)
11
+
///
12
+
/// Stored as SmolStr to ease lifetime issues and because, despite the fact that NSIDs *can* be 317 characters, most are quite short
13
+
/// TODO: consider if this should go back to CowStr, or be broken up into segments
14
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
15
+
#[serde(transparent)]
16
+
#[repr(transparent)]
17
+
pub struct Nsid<'n>(CowStr<'n>);
18
+
19
+
pub static NSID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
20
+
Regex::new(r"^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z][a-zA-Z0-9]{0,62})$").unwrap()
21
+
});
22
+
23
+
impl<'n> Nsid<'n> {
24
+
/// Fallible constructor, validates, borrows from input
25
+
pub fn new(nsid: &'n str) -> Result<Self, &'static str> {
26
+
if nsid.len() > 317 {
27
+
Err("NSID too long")
28
+
} else if !NSID_REGEX.is_match(nsid) {
29
+
Err("Invalid NSID")
30
+
} else {
31
+
Ok(Self(CowStr::Borrowed(nsid)))
32
+
}
33
+
}
34
+
35
+
/// Fallible constructor, validates, borrows from input
36
+
pub fn new_owned(nsid: impl AsRef<str>) -> Result<Self, &'static str> {
37
+
let nsid = nsid.as_ref();
38
+
if nsid.len() > 317 {
39
+
Err("NSID too long")
40
+
} else if !NSID_REGEX.is_match(nsid) {
41
+
Err("Invalid NSID")
42
+
} else {
43
+
Ok(Self(CowStr::Owned(nsid.to_smolstr())))
44
+
}
45
+
}
46
+
47
+
/// Fallible constructor, validates, doesn't allocate
48
+
pub fn new_static(nsid: &'static str) -> Result<Self, &'static str> {
49
+
if nsid.len() > 317 {
50
+
Err("NSID too long")
51
+
} else if !NSID_REGEX.is_match(nsid) {
52
+
Err("Invalid NSID")
53
+
} else {
54
+
Ok(Self(CowStr::new_static(nsid)))
55
+
}
56
+
}
57
+
58
+
/// Infallible constructor for when you *know* the string is a valid NSID.
59
+
/// Will panic on invalid NSIDs. If you're manually decoding atproto records
60
+
/// or API values you know are valid (rather than using serde), this is the one to use.
61
+
/// The From<String> and From<CowStr> impls use the same logic.
62
+
pub fn raw(nsid: &'n str) -> Self {
63
+
if nsid.len() > 317 {
64
+
panic!("NSID too long")
65
+
} else if !NSID_REGEX.is_match(nsid) {
66
+
panic!("Invalid NSID")
67
+
} else {
68
+
Self(CowStr::Borrowed(nsid))
69
+
}
70
+
}
71
+
72
+
/// Infallible constructor for when you *know* the string is a valid NSID.
73
+
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
74
+
pub unsafe fn unchecked(nsid: &'n str) -> Self {
75
+
Self(CowStr::Borrowed(nsid))
76
+
}
77
+
78
+
/// Returns the domain authority part of the NSID.
79
+
pub fn domain_authority(&self) -> &str {
80
+
let split = self.0.rfind('.').expect("enforced by constructor");
81
+
&self.0[..split]
82
+
}
83
+
84
+
/// Returns the name segment of the NSID.
85
+
pub fn name(&self) -> &str {
86
+
let split = self.0.rfind('.').expect("enforced by constructor");
87
+
&self.0[split + 1..]
88
+
}
89
+
90
+
pub fn as_str(&self) -> &str {
91
+
{
92
+
let this = &self.0;
93
+
this
94
+
}
95
+
}
96
+
}
97
+
98
+
impl<'n> FromStr for Nsid<'n> {
99
+
type Err = &'static str;
100
+
101
+
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
102
+
/// Prefer `Nsid::new()` or `Nsid::raw` if you want to borrow.
103
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
104
+
Self::new_owned(s)
105
+
}
106
+
}
107
+
108
+
impl IntoStatic for Nsid<'_> {
109
+
type Output = Nsid<'static>;
110
+
111
+
fn into_static(self) -> Self::Output {
112
+
Nsid(self.0.into_static())
113
+
}
114
+
}
115
+
116
+
impl<'de> Deserialize<'de> for Nsid<'de> {
117
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
118
+
where
119
+
D: Deserializer<'de>,
120
+
{
121
+
let value: &str = Deserialize::deserialize(deserializer)?;
122
+
Self::new(value).map_err(D::Error::custom)
123
+
}
124
+
}
125
+
126
+
impl fmt::Display for Nsid<'_> {
127
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128
+
f.write_str(&self.0)
129
+
}
130
+
}
131
+
132
+
impl fmt::Debug for Nsid<'_> {
133
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134
+
write!(f, "at://{}", self.0)
135
+
}
136
+
}
137
+
138
+
impl<'n> From<Nsid<'n>> for String {
139
+
fn from(value: Nsid) -> Self {
140
+
value.0.to_string()
141
+
}
142
+
}
143
+
144
+
impl<'n> From<Nsid<'n>> for CowStr<'n> {
145
+
fn from(value: Nsid<'n>) -> Self {
146
+
value.0
147
+
}
148
+
}
149
+
150
+
impl From<Nsid<'_>> for SmolStr {
151
+
fn from(value: Nsid) -> Self {
152
+
value.0.to_smolstr()
153
+
}
154
+
}
155
+
156
+
impl<'n> From<String> for Nsid<'n> {
157
+
fn from(value: String) -> Self {
158
+
if value.len() > 317 {
159
+
panic!("NSID too long")
160
+
} else if !NSID_REGEX.is_match(&value) {
161
+
panic!("Invalid NSID")
162
+
} else {
163
+
Self(CowStr::Owned(value.to_smolstr()))
164
+
}
165
+
}
166
+
}
167
+
168
+
impl<'n> From<CowStr<'n>> for Nsid<'n> {
169
+
fn from(value: CowStr<'n>) -> Self {
170
+
if value.len() > 317 {
171
+
panic!("NSID too long")
172
+
} else if !NSID_REGEX.is_match(&value) {
173
+
panic!("Invalid NSID")
174
+
} else {
175
+
Self(value)
176
+
}
177
+
}
178
+
}
179
+
180
+
impl From<SmolStr> for Nsid<'_> {
181
+
fn from(value: SmolStr) -> Self {
182
+
if value.len() > 317 {
183
+
panic!("NSID too long")
184
+
} else if !NSID_REGEX.is_match(&value) {
185
+
panic!("Invalid NSID")
186
+
} else {
187
+
Self(CowStr::Owned(value))
188
+
}
189
+
}
190
+
}
191
+
192
+
impl AsRef<str> for Nsid<'_> {
193
+
fn as_ref(&self) -> &str {
194
+
self.as_str()
195
+
}
196
+
}
197
+
198
+
impl Deref for Nsid<'_> {
199
+
type Target = str;
200
+
201
+
fn deref(&self) -> &Self::Target {
202
+
self.as_str()
203
+
}
204
+
}
205
+
206
+
unsafe impl RecordKeyType for Nsid<'_> {
207
+
fn as_str(&self) -> &str {
208
+
self.as_str()
209
+
}
210
+
}
+402
crates/jacquard-common/src/types/recordkey.rs
+402
crates/jacquard-common/src/types/recordkey.rs
···
···
1
+
use crate::types::Literal;
2
+
use crate::{CowStr, IntoStatic};
3
+
use regex::Regex;
4
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
5
+
use smol_str::{SmolStr, ToSmolStr};
6
+
use std::fmt;
7
+
use std::marker::PhantomData;
8
+
use std::sync::LazyLock;
9
+
use std::{ops::Deref, str::FromStr};
10
+
11
+
/// Trait for generic typed record keys
12
+
///
13
+
/// This is deliberately public (so that consumers can develop specialized record key types),
14
+
/// but is marked as unsafe, because the implementer is expected to uphold the invariants
15
+
/// required by this trait, namely compliance with the [spec](https://atproto.com/specs/record-key)
16
+
/// as described by [`RKEY_REGEX`](RKEY_REGEX).
17
+
///
18
+
/// This crate provides implementations for TID, NSID, literals, and generic strings
19
+
pub unsafe trait RecordKeyType: Clone + Serialize {
20
+
fn as_str(&self) -> &str;
21
+
}
22
+
23
+
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)]
24
+
#[serde(transparent)]
25
+
#[repr(transparent)]
26
+
pub struct RecordKey<T: RecordKeyType>(pub T);
27
+
28
+
impl<T> From<T> for RecordKey<Rkey<'_>>
29
+
where
30
+
T: RecordKeyType,
31
+
{
32
+
fn from(value: T) -> Self {
33
+
RecordKey(Rkey::from_str(value.as_str()).expect("Invalid rkey"))
34
+
}
35
+
}
36
+
37
+
impl<T> AsRef<str> for RecordKey<T>
38
+
where
39
+
T: RecordKeyType,
40
+
{
41
+
fn as_ref(&self) -> &str {
42
+
self.0.as_str()
43
+
}
44
+
}
45
+
46
+
impl<T> IntoStatic for RecordKey<T>
47
+
where
48
+
T: IntoStatic + RecordKeyType,
49
+
T::Output: RecordKeyType,
50
+
{
51
+
type Output = RecordKey<T::Output>;
52
+
53
+
fn into_static(self) -> Self::Output {
54
+
RecordKey(self.0.into_static())
55
+
}
56
+
}
57
+
58
+
/// ATProto Record Key (type `any`)
59
+
/// Catch-all for any string meeting the overall Record Key requirements detailed https://atproto.com/specs/record-key
60
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
61
+
#[serde(transparent)]
62
+
#[repr(transparent)]
63
+
pub struct Rkey<'r>(CowStr<'r>);
64
+
65
+
unsafe impl<'r> RecordKeyType for Rkey<'r> {
66
+
fn as_str(&self) -> &str {
67
+
self.0.as_ref()
68
+
}
69
+
}
70
+
71
+
pub static RKEY_REGEX: LazyLock<Regex> =
72
+
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9.\-_:~]{1,512}$").unwrap());
73
+
74
+
/// AT Protocol rkey
75
+
impl<'r> Rkey<'r> {
76
+
/// Fallible constructor, validates, borrows from input
77
+
pub fn new(rkey: &'r str) -> Result<Self, &'static str> {
78
+
if [".", ".."].contains(&rkey) {
79
+
Err("Disallowed rkey")
80
+
} else if !RKEY_REGEX.is_match(rkey) {
81
+
Err("Invalid rkey")
82
+
} else {
83
+
Ok(Self(CowStr::Borrowed(rkey)))
84
+
}
85
+
}
86
+
87
+
/// Fallible constructor, validates, borrows from input
88
+
pub fn new_owned(rkey: impl AsRef<str>) -> Result<Self, &'static str> {
89
+
let rkey = rkey.as_ref();
90
+
if [".", ".."].contains(&rkey) {
91
+
Err("Disallowed rkey")
92
+
} else if !RKEY_REGEX.is_match(rkey) {
93
+
Err("Invalid rkey")
94
+
} else {
95
+
Ok(Self(CowStr::Owned(rkey.to_smolstr())))
96
+
}
97
+
}
98
+
99
+
/// Fallible constructor, validates, doesn't allocate
100
+
pub fn new_static(rkey: &'static str) -> Result<Self, &'static str> {
101
+
if [".", ".."].contains(&rkey) {
102
+
Err("Disallowed rkey")
103
+
} else if !RKEY_REGEX.is_match(rkey) {
104
+
Err("Invalid rkey")
105
+
} else {
106
+
Ok(Self(CowStr::new_static(rkey)))
107
+
}
108
+
}
109
+
110
+
/// Infallible constructor for when you *know* the string is a valid rkey.
111
+
/// Will panic on invalid rkeys. If you're manually decoding atproto records
112
+
/// or API values you know are valid (rather than using serde), this is the one to use.
113
+
/// The From impls use the same logic.
114
+
pub fn raw(rkey: &'r str) -> Self {
115
+
if [".", ".."].contains(&rkey) {
116
+
panic!("Disallowed rkey")
117
+
} else if !RKEY_REGEX.is_match(rkey) {
118
+
panic!("Invalid rkey")
119
+
} else {
120
+
Self(CowStr::Borrowed(rkey))
121
+
}
122
+
}
123
+
124
+
/// Infallible constructor for when you *know* the string is a valid rkey.
125
+
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
126
+
pub unsafe fn unchecked(rkey: &'r str) -> Self {
127
+
Self(CowStr::Borrowed(rkey))
128
+
}
129
+
130
+
pub fn as_str(&self) -> &str {
131
+
{
132
+
let this = &self.0;
133
+
this
134
+
}
135
+
}
136
+
}
137
+
138
+
impl<'r> FromStr for Rkey<'r> {
139
+
type Err = &'static str;
140
+
141
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
142
+
if [".", ".."].contains(&s) {
143
+
Err("Disallowed rkey")
144
+
} else if !RKEY_REGEX.is_match(s) {
145
+
Err("Invalid rkey")
146
+
} else {
147
+
Ok(Self(CowStr::Owned(s.to_smolstr())))
148
+
}
149
+
}
150
+
}
151
+
152
+
impl IntoStatic for Rkey<'_> {
153
+
type Output = Rkey<'static>;
154
+
155
+
fn into_static(self) -> Self::Output {
156
+
Rkey(self.0.into_static())
157
+
}
158
+
}
159
+
160
+
impl<'de> Deserialize<'de> for Rkey<'de> {
161
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
162
+
where
163
+
D: Deserializer<'de>,
164
+
{
165
+
let value: &str = Deserialize::deserialize(deserializer)?;
166
+
Self::new(value).map_err(D::Error::custom)
167
+
}
168
+
}
169
+
170
+
impl fmt::Display for Rkey<'_> {
171
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172
+
f.write_str(&self.0)
173
+
}
174
+
}
175
+
176
+
impl fmt::Debug for Rkey<'_> {
177
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178
+
write!(f, "record-key:{}", self.0)
179
+
}
180
+
}
181
+
182
+
impl From<Rkey<'_>> for String {
183
+
fn from(value: Rkey<'_>) -> Self {
184
+
value.0.to_string()
185
+
}
186
+
}
187
+
188
+
impl<'r> From<Rkey<'r>> for CowStr<'r> {
189
+
fn from(value: Rkey<'r>) -> Self {
190
+
value.0
191
+
}
192
+
}
193
+
194
+
impl<'r> From<Rkey<'r>> for SmolStr {
195
+
fn from(value: Rkey) -> Self {
196
+
value.0.to_smolstr()
197
+
}
198
+
}
199
+
200
+
impl<'r> From<String> for Rkey<'r> {
201
+
fn from(value: String) -> Self {
202
+
if [".", ".."].contains(&value.as_str()) {
203
+
panic!("Disallowed rkey")
204
+
} else if !RKEY_REGEX.is_match(&value) {
205
+
panic!("Invalid rkey")
206
+
} else {
207
+
Self(CowStr::Owned(value.to_smolstr()))
208
+
}
209
+
}
210
+
}
211
+
212
+
impl<'r> From<CowStr<'r>> for Rkey<'r> {
213
+
fn from(value: CowStr<'r>) -> Self {
214
+
if [".", ".."].contains(&value.as_ref()) {
215
+
panic!("Disallowed rkey")
216
+
} else if !RKEY_REGEX.is_match(&value) {
217
+
panic!("Invalid rkey")
218
+
} else {
219
+
Self(value)
220
+
}
221
+
}
222
+
}
223
+
224
+
impl AsRef<str> for Rkey<'_> {
225
+
fn as_ref(&self) -> &str {
226
+
self.as_str()
227
+
}
228
+
}
229
+
230
+
impl Deref for Rkey<'_> {
231
+
type Target = str;
232
+
233
+
fn deref(&self) -> &Self::Target {
234
+
self.0.as_ref()
235
+
}
236
+
}
237
+
238
+
/// ATProto Record Key (type `literal:<value>`)
239
+
/// Zero-sized type, literal is associated constant of type parameter
240
+
///
241
+
/// TODO: macro to construct arbitrary ones of these and the associated marker struct
242
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
243
+
pub struct LiteralKey<T: Literal = SelfRecord> {
244
+
literal: PhantomData<T>,
245
+
}
246
+
247
+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
248
+
pub struct SelfRecord;
249
+
250
+
impl Literal for SelfRecord {
251
+
const LITERAL: &'static str = "self";
252
+
}
253
+
254
+
unsafe impl<T: Literal> RecordKeyType for LiteralKey<T> {
255
+
fn as_str(&self) -> &str {
256
+
T::LITERAL
257
+
}
258
+
}
259
+
260
+
/// AT Protocol rkey
261
+
impl<T: Literal> LiteralKey<T> {
262
+
/// Fallible constructor, validates, borrows from input
263
+
pub fn new(rkey: impl AsRef<str>) -> Result<Self, &'static str> {
264
+
let rkey = rkey.as_ref();
265
+
if !rkey.eq_ignore_ascii_case(T::LITERAL) {
266
+
Err("Invalid literal rkey - does not match literal")
267
+
} else if [".", ".."].contains(&rkey) {
268
+
Err("Disallowed rkey")
269
+
} else if !RKEY_REGEX.is_match(rkey) {
270
+
Err("Invalid rkey")
271
+
} else {
272
+
Ok(Self {
273
+
literal: PhantomData,
274
+
})
275
+
}
276
+
}
277
+
278
+
/// Infallible constructor for when you *know* the string is a valid rkey.
279
+
/// Will panic on invalid rkeys. If you're manually decoding atproto records
280
+
/// or API values you know are valid (rather than using serde), this is the one to use.
281
+
/// The From<String> and From<CowStr> impls use the same logic.
282
+
pub fn raw(rkey: &str) -> Self {
283
+
if !rkey.eq_ignore_ascii_case(T::LITERAL) {
284
+
panic!(
285
+
"Invalid literal rkey - does not match literal {}",
286
+
T::LITERAL
287
+
)
288
+
} else if [".", ".."].contains(&rkey.as_ref()) {
289
+
panic!("Disallowed rkey")
290
+
} else if !RKEY_REGEX.is_match(rkey) {
291
+
panic!("Invalid rkey")
292
+
} else {
293
+
Self {
294
+
literal: PhantomData,
295
+
}
296
+
}
297
+
}
298
+
299
+
/// Infallible type constructor
300
+
///
301
+
/// # Safety
302
+
/// Does not validate that the literal is a valid record key
303
+
pub unsafe fn t() -> Self {
304
+
Self {
305
+
literal: PhantomData,
306
+
}
307
+
}
308
+
309
+
pub fn as_str(&self) -> &str {
310
+
T::LITERAL
311
+
}
312
+
}
313
+
314
+
impl<T: Literal> FromStr for LiteralKey<T> {
315
+
type Err = &'static str;
316
+
317
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
318
+
Self::new(s)
319
+
}
320
+
}
321
+
322
+
impl<'de, T: Literal> Deserialize<'de> for LiteralKey<T> {
323
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
324
+
where
325
+
D: Deserializer<'de>,
326
+
{
327
+
let value: &str = Deserialize::deserialize(deserializer)?;
328
+
Self::new(value).map_err(D::Error::custom)
329
+
}
330
+
}
331
+
332
+
impl<T: Literal> fmt::Display for LiteralKey<T> {
333
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334
+
f.write_str(T::LITERAL)
335
+
}
336
+
}
337
+
338
+
impl<T: Literal> fmt::Debug for LiteralKey<T> {
339
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340
+
write!(f, "literal:{}", T::LITERAL)
341
+
}
342
+
}
343
+
344
+
impl<'r, T: Literal> From<LiteralKey<T>> for String {
345
+
fn from(_value: LiteralKey<T>) -> Self {
346
+
T::LITERAL.to_string()
347
+
}
348
+
}
349
+
350
+
impl<'r, T: Literal> From<LiteralKey<T>> for CowStr<'r> {
351
+
fn from(_value: LiteralKey<T>) -> Self {
352
+
CowStr::Borrowed(T::LITERAL)
353
+
}
354
+
}
355
+
356
+
impl<T: Literal> TryFrom<String> for LiteralKey<T> {
357
+
type Error = &'static str;
358
+
fn try_from(value: String) -> Result<Self, Self::Error> {
359
+
if !value.eq_ignore_ascii_case(T::LITERAL) {
360
+
Err("Invalid literal rkey - does not match literal")
361
+
} else if [".", ".."].contains(&value.as_str()) {
362
+
Err("Disallowed rkey")
363
+
} else if !RKEY_REGEX.is_match(&value) {
364
+
Err("Invalid rkey")
365
+
} else {
366
+
Ok(Self {
367
+
literal: PhantomData,
368
+
})
369
+
}
370
+
}
371
+
}
372
+
373
+
impl<'r, T: Literal> TryFrom<CowStr<'r>> for LiteralKey<T> {
374
+
type Error = &'static str;
375
+
fn try_from(value: CowStr<'r>) -> Result<Self, Self::Error> {
376
+
if !value.eq_ignore_ascii_case(T::LITERAL) {
377
+
Err("Invalid literal rkey - does not match literal")
378
+
} else if [".", ".."].contains(&value.as_ref()) {
379
+
Err("Disallowed rkey")
380
+
} else if !RKEY_REGEX.is_match(&value) {
381
+
Err("Invalid rkey")
382
+
} else {
383
+
Ok(Self {
384
+
literal: PhantomData,
385
+
})
386
+
}
387
+
}
388
+
}
389
+
390
+
impl<T: Literal> AsRef<str> for LiteralKey<T> {
391
+
fn as_ref(&self) -> &str {
392
+
self.as_str()
393
+
}
394
+
}
395
+
396
+
impl<T: Literal> Deref for LiteralKey<T> {
397
+
type Target = str;
398
+
399
+
fn deref(&self) -> &Self::Target {
400
+
self.as_str()
401
+
}
402
+
}
+38
-43
crates/jacquard-common/src/types/tid.rs
+38
-43
crates/jacquard-common/src/types/tid.rs
···
1
use std::fmt;
2
use std::sync::LazyLock;
3
use std::{ops::Deref, str::FromStr};
4
5
-
use compact_str::{CompactString, ToCompactString};
6
-
use serde::{Deserialize, Deserializer, Serialize, de::Error};
7
-
8
use crate::types::integer::LimitedU32;
9
-
use crate::{CowStr, IntoStatic};
10
use regex::Regex;
11
12
-
fn s32_encode(mut i: u64) -> CowStr<'static> {
13
const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
14
15
-
let mut s = CompactString::with_capacity(13);
16
for _ in 0..13 {
17
let c = i & 0x1F;
18
s.push(S32_CHAR[c as usize] as char);
···
20
i >>= 5;
21
}
22
23
-
// Reverse the string to convert it to big-endian format.
24
-
CowStr::Owned(s.chars().rev().collect())
25
}
26
27
static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
···
33
/// [Timestamp Identifier]: https://atproto.com/specs/tid
34
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
35
#[serde(transparent)]
36
-
pub struct Tid<'t>(CowStr<'t>);
37
38
-
impl<'t> Tid<'t> {
39
/// Parses a `TID` from the given string.
40
-
pub fn new(tid: &'t str) -> Result<Self, &'static str> {
41
-
if tid.len() != 13 {
42
-
Err("TID must be 13 characters")
43
-
} else if !TID_REGEX.is_match(&tid) {
44
-
Err("Invalid TID")
45
-
} else {
46
-
Ok(Self(CowStr::Owned(tid.to_compact_string())))
47
-
}
48
-
}
49
-
50
-
/// Fallible constructor from an existing CowStr, takes ownership
51
-
pub fn from_cowstr(tid: CowStr<'t>) -> Result<Tid<'t>, &'static str> {
52
if tid.len() != 13 {
53
Err("TID must be 13 characters")
54
-
} else if !TID_REGEX.is_match(&tid) {
55
Err("Invalid TID")
56
} else {
57
-
Ok(Self(tid.into_static()))
58
}
59
}
60
···
62
/// Will panic on invalid TID. If you're manually decoding atproto records
63
/// or API values you know are valid (rather than using serde), this is the one to use.
64
/// The From<String> and From<CowStr> impls use the same logic.
65
-
pub fn raw(tid: &'t str) -> Self {
66
if tid.len() != 13 {
67
panic!("TID must be 13 characters")
68
} else if !TID_REGEX.is_match(&tid) {
69
panic!("Invalid TID")
70
} else {
71
-
Self(CowStr::Borrowed(tid))
72
}
73
}
74
75
/// Infallible constructor for when you *know* the string is a valid TID.
76
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
77
-
pub unsafe fn unchecked(tid: &'t str) -> Self {
78
-
Self(CowStr::Borrowed(tid))
79
}
80
81
/// Construct a new timestamp with the specified clock ID.
···
121
}
122
}
123
124
-
impl FromStr for Tid<'_> {
125
type Err = &'static str;
126
127
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
128
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
129
fn from_str(s: &str) -> Result<Self, Self::Err> {
130
-
Self::from_cowstr(CowStr::Borrowed(s).into_static())
131
}
132
}
133
134
-
impl<'de> Deserialize<'de> for Tid<'de> {
135
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136
where
137
D: Deserializer<'de>,
138
{
139
-
let value = Deserialize::deserialize(deserializer)?;
140
Self::new(value).map_err(D::Error::custom)
141
}
142
}
143
144
-
impl fmt::Display for Tid<'_> {
145
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146
f.write_str(&self.0)
147
}
148
}
149
150
-
impl<'t> From<Tid<'t>> for String {
151
-
fn from(value: Tid<'t>) -> Self {
152
value.0.to_string()
153
}
154
}
155
156
-
impl<'t> From<Tid<'t>> for CowStr<'t> {
157
-
fn from(value: Tid<'t>) -> Self {
158
value.0
159
}
160
}
161
162
-
impl From<String> for Tid<'static> {
163
fn from(value: String) -> Self {
164
if value.len() != 13 {
165
panic!("TID must be 13 characters")
166
} else if !TID_REGEX.is_match(&value) {
167
panic!("Invalid TID")
168
} else {
169
-
Self(CowStr::Owned(value.to_compact_string()))
170
}
171
}
172
}
173
174
-
impl<'t> From<CowStr<'t>> for Tid<'t> {
175
fn from(value: CowStr<'t>) -> Self {
176
if value.len() != 13 {
177
panic!("TID must be 13 characters")
178
} else if !TID_REGEX.is_match(&value) {
179
panic!("Invalid TID")
180
} else {
181
-
Self(value)
182
}
183
}
184
}
185
186
-
impl AsRef<str> for Tid<'_> {
187
fn as_ref(&self) -> &str {
188
self.as_str()
189
}
190
}
191
192
-
impl Deref for Tid<'_> {
193
type Target = str;
194
195
fn deref(&self) -> &Self::Target {
···
1
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
2
+
use smol_str::{SmolStr, SmolStrBuilder};
3
use std::fmt;
4
use std::sync::LazyLock;
5
use std::{ops::Deref, str::FromStr};
6
7
+
use crate::CowStr;
8
use crate::types::integer::LimitedU32;
9
use regex::Regex;
10
11
+
fn s32_encode(mut i: u64) -> SmolStr {
12
const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
13
14
+
let mut s = SmolStrBuilder::new();
15
for _ in 0..13 {
16
let c = i & 0x1F;
17
s.push(S32_CHAR[c as usize] as char);
···
19
i >>= 5;
20
}
21
22
+
let mut builder = SmolStrBuilder::new();
23
+
for c in s.finish().chars().rev() {
24
+
builder.push(c);
25
+
}
26
+
builder.finish()
27
}
28
29
static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
···
35
/// [Timestamp Identifier]: https://atproto.com/specs/tid
36
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
37
#[serde(transparent)]
38
+
#[repr(transparent)]
39
+
pub struct Tid(SmolStr);
40
41
+
impl Tid {
42
/// Parses a `TID` from the given string.
43
+
pub fn new(tid: impl AsRef<str>) -> Result<Self, &'static str> {
44
+
let tid = tid.as_ref();
45
if tid.len() != 13 {
46
Err("TID must be 13 characters")
47
+
} else if !TID_REGEX.is_match(&tid.as_ref()) {
48
Err("Invalid TID")
49
} else {
50
+
Ok(Self(SmolStr::new_inline(&tid)))
51
}
52
}
53
···
55
/// Will panic on invalid TID. If you're manually decoding atproto records
56
/// or API values you know are valid (rather than using serde), this is the one to use.
57
/// The From<String> and From<CowStr> impls use the same logic.
58
+
pub fn raw(tid: impl AsRef<str>) -> Self {
59
+
let tid = tid.as_ref();
60
if tid.len() != 13 {
61
panic!("TID must be 13 characters")
62
} else if !TID_REGEX.is_match(&tid) {
63
panic!("Invalid TID")
64
} else {
65
+
Self(SmolStr::new_inline(tid))
66
}
67
}
68
69
/// Infallible constructor for when you *know* the string is a valid TID.
70
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
71
+
pub unsafe fn unchecked(tid: impl AsRef<str>) -> Self {
72
+
let tid = tid.as_ref();
73
+
Self(SmolStr::new_inline(tid))
74
}
75
76
/// Construct a new timestamp with the specified clock ID.
···
116
}
117
}
118
119
+
impl FromStr for Tid {
120
type Err = &'static str;
121
122
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
123
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
124
fn from_str(s: &str) -> Result<Self, Self::Err> {
125
+
Self::new(s)
126
}
127
}
128
129
+
impl<'de> Deserialize<'de> for Tid {
130
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
131
where
132
D: Deserializer<'de>,
133
{
134
+
let value: &str = Deserialize::deserialize(deserializer)?;
135
Self::new(value).map_err(D::Error::custom)
136
}
137
}
138
139
+
impl fmt::Display for Tid {
140
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141
f.write_str(&self.0)
142
}
143
}
144
145
+
impl From<Tid> for String {
146
+
fn from(value: Tid) -> Self {
147
value.0.to_string()
148
}
149
}
150
151
+
impl From<Tid> for SmolStr {
152
+
fn from(value: Tid) -> Self {
153
value.0
154
}
155
}
156
157
+
impl From<String> for Tid {
158
fn from(value: String) -> Self {
159
if value.len() != 13 {
160
panic!("TID must be 13 characters")
161
} else if !TID_REGEX.is_match(&value) {
162
panic!("Invalid TID")
163
} else {
164
+
Self(SmolStr::new_inline(&value))
165
}
166
}
167
}
168
169
+
impl<'t> From<CowStr<'t>> for Tid {
170
fn from(value: CowStr<'t>) -> Self {
171
if value.len() != 13 {
172
panic!("TID must be 13 characters")
173
} else if !TID_REGEX.is_match(&value) {
174
panic!("Invalid TID")
175
} else {
176
+
Self(SmolStr::new_inline(&value))
177
}
178
}
179
}
180
181
+
impl AsRef<str> for Tid {
182
fn as_ref(&self) -> &str {
183
self.as_str()
184
}
185
}
186
187
+
impl Deref for Tid {
188
type Target = str;
189
190
fn deref(&self) -> &Self::Target {