+23
-27
Cargo.lock
+23
-27
Cargo.lock
···
83
83
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
84
84
85
85
[[package]]
86
-
name = "bumpalo"
87
-
version = "3.19.0"
86
+
name = "borsh"
87
+
version = "1.5.7"
88
88
source = "registry+https://github.com/rust-lang/crates.io-index"
89
-
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
89
+
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
90
+
dependencies = [
91
+
"cfg_aliases",
92
+
]
90
93
91
94
[[package]]
92
-
name = "castaway"
93
-
version = "0.2.4"
95
+
name = "bumpalo"
96
+
version = "3.19.0"
94
97
source = "registry+https://github.com/rust-lang/crates.io-index"
95
-
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
96
-
dependencies = [
97
-
"rustversion",
98
-
]
98
+
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
99
99
100
100
[[package]]
101
101
name = "cc"
···
112
112
version = "1.0.3"
113
113
source = "registry+https://github.com/rust-lang/crates.io-index"
114
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"
115
121
116
122
[[package]]
117
123
name = "chrono"
···
187
193
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
188
194
189
195
[[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
196
name = "core-foundation-sys"
205
197
version = "0.8.7"
206
198
source = "registry+https://github.com/rust-lang/crates.io-index"
···
334
326
dependencies = [
335
327
"chrono",
336
328
"cid",
337
-
"compact_str",
338
329
"miette",
339
330
"multibase",
340
331
"multihash",
···
342
333
"serde",
343
334
"serde_html_form",
344
335
"serde_json",
336
+
"smol_str",
345
337
"thiserror",
346
338
]
347
339
···
575
567
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
576
568
577
569
[[package]]
578
-
name = "static_assertions"
579
-
version = "1.1.0"
570
+
name = "smol_str"
571
+
version = "0.3.2"
580
572
source = "registry+https://github.com/rust-lang/crates.io-index"
581
-
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
573
+
checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d"
574
+
dependencies = [
575
+
"borsh",
576
+
"serde",
577
+
]
582
578
583
579
[[package]]
584
580
name = "strsim"
+7
Cargo.toml
+7
Cargo.toml
···
7
7
edition = "2024"
8
8
version = "0.1.0"
9
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
+
10
17
11
18
description = "A simple Rust project using Nix"
12
19
+26
README.md
+26
README.md
···
29
29
```
30
30
31
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
8
[dependencies]
9
9
chrono = "0.4.42"
10
10
cid = { version = "0.11.1", features = ["serde", "std"] }
11
-
compact_str = "0.9.0"
12
11
miette = "7.6.0"
13
12
multibase = "0.9.1"
14
13
multihash = "0.19.3"
···
16
15
serde = { version = "1.0.227", features = ["derive"] }
17
16
serde_html_form = "0.2.8"
18
17
serde_json = "1.0.145"
18
+
smol_str = { version = "0.3.2", features = ["serde"] }
19
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
1
use serde::{Deserialize, Serialize};
2
+
use smol_str::SmolStr;
3
3
use std::{
4
4
borrow::Cow,
5
5
fmt,
···
10
10
use crate::IntoStatic;
11
11
12
12
/// Shamelessly copied from https://github.com/bearcove/merde
13
-
/// A copy-on-write string type that uses [`CompactString`] for
13
+
/// A copy-on-write immutable string type that uses [`SmolStr`] for
14
14
/// the "owned" variant.
15
15
///
16
16
/// The standard [`Cow`] type cannot be used, since
17
-
/// `<str as ToOwned>::Owned` is `String`, and not `CompactString`.
17
+
/// `<str as ToOwned>::Owned` is `String`, and not `SmolStr`.
18
18
#[derive(Clone)]
19
19
pub enum CowStr<'s> {
20
20
Borrowed(&'s str),
21
-
Owned(CompactString),
21
+
Owned(SmolStr),
22
22
}
23
23
24
24
impl CowStr<'static> {
···
26
26
/// if the `compact_str` feature is disabled, or if the string is longer
27
27
/// than `MAX_INLINE_SIZE`.
28
28
pub fn copy_from_str(s: &str) -> Self {
29
-
Self::Owned(CompactString::from(s))
29
+
Self::Owned(SmolStr::from(s))
30
+
}
31
+
32
+
pub fn new_static(s: &'static str) -> Self {
33
+
Self::Owned(SmolStr::new_static(s))
30
34
}
31
35
}
32
36
···
38
42
39
43
#[inline]
40
44
pub fn from_utf8_owned(s: Vec<u8>) -> Result<Self, std::str::Utf8Error> {
41
-
Ok(Self::Owned(CompactString::from_utf8(s)?))
45
+
Ok(Self::Owned(SmolStr::new(std::str::from_utf8(&s)?)))
42
46
}
43
47
44
48
#[inline]
45
49
pub fn from_utf8_lossy(s: &'s [u8]) -> Self {
46
-
Self::Owned(CompactString::from_utf8_lossy(s))
50
+
Self::Owned(String::from_utf8_lossy(&s).into())
47
51
}
48
52
49
53
/// # Safety
···
51
55
/// This function is unsafe because it does not check that the bytes are valid UTF-8.
52
56
#[inline]
53
57
pub unsafe fn from_utf8_unchecked(s: &'s [u8]) -> Self {
54
-
unsafe { Self::Owned(CompactString::from_utf8_unchecked(s)) }
58
+
unsafe { Self::Owned(SmolStr::new(std::str::from_utf8_unchecked(s))) }
55
59
}
56
60
}
57
61
···
133
137
fn from(s: CowStr<'_>) -> Self {
134
138
match s {
135
139
CowStr::Borrowed(s) => s.into(),
136
-
CowStr::Owned(s) => s.into(),
140
+
CowStr::Owned(s) => String::from(s).into_boxed_str(),
137
141
}
138
142
}
139
143
}
+8
crates/jacquard-common/src/types.rs
+8
crates/jacquard-common/src/types.rs
···
1
1
pub mod aturi;
2
2
pub mod blob;
3
3
pub mod cid;
4
+
pub mod collection;
4
5
pub mod datetime;
5
6
pub mod did;
6
7
pub mod handle;
···
8
9
pub mod integer;
9
10
pub mod link;
10
11
pub mod nsid;
12
+
pub mod recordkey;
11
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 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;
1
9
use std::fmt;
2
10
use std::sync::LazyLock;
3
11
use std::{ops::Deref, str::FromStr};
4
12
5
-
use compact_str::ToCompactString;
6
-
use serde::{Deserialize, Deserializer, Serialize, de::Error};
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
+
}
7
28
8
-
use crate::{CowStr, IntoStatic};
9
-
use regex::Regex;
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
+
}
10
35
11
-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Hash)]
12
-
#[serde(transparent)]
13
-
pub struct AtUri<'a>(CowStr<'a>);
36
+
pub type UriPathBuf = UriPath<'static>;
14
37
15
-
pub static AT_URI_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^$").unwrap());
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
+
});
16
41
17
-
impl<'a> AtUri<'a> {
42
+
impl<'u> AtUri<'u> {
18
43
/// 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")
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
+
}
24
73
} else {
25
-
Ok(Self(CowStr::Borrowed(uri)))
74
+
Err("Invalid at:// URI via regex")
26
75
}
27
76
}
28
77
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")
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
+
}
35
108
} else {
36
-
Ok(Self(uri.into_static()))
109
+
Err("Invalid at:// URI via regex")
37
110
}
38
111
}
39
112
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")
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
+
}
49
143
} else {
50
-
Self(CowStr::Borrowed(uri))
144
+
Err("Invalid at:// URI via regex")
51
145
}
52
146
}
53
147
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))
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
+
}
58
190
}
59
191
60
192
pub fn as_str(&self) -> &str {
61
193
{
62
-
let this = &self.0;
194
+
let this = &self.uri;
63
195
this
64
196
}
65
197
}
···
69
201
type Err = &'static str;
70
202
71
203
/// 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.
204
+
/// Prefer `AtUri::new()` or `AtUri::raw()` if you want to borrow.
73
205
fn from_str(s: &str) -> Result<Self, Self::Err> {
74
-
Self::from_cowstr(CowStr::Owned(s.to_compact_string()))
206
+
Self::new_owned(s)
75
207
}
76
208
}
77
209
78
-
impl<'ae> Deserialize<'ae> for AtUri<'ae> {
210
+
impl<'de> Deserialize<'de> for AtUri<'de> {
79
211
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
80
212
where
81
-
D: Deserializer<'ae>,
213
+
D: Deserializer<'de>,
82
214
{
83
215
let value = Deserialize::deserialize(deserializer)?;
84
216
Self::new(value).map_err(D::Error::custom)
85
217
}
86
218
}
87
219
88
-
impl fmt::Display for AtUri<'_> {
89
-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90
-
f.write_str(&self.0)
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)
91
226
}
92
227
}
93
228
94
-
impl<'a> From<AtUri<'a>> for String {
95
-
fn from(value: AtUri<'a>) -> Self {
96
-
value.0.to_string()
229
+
impl fmt::Display for AtUri<'_> {
230
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231
+
f.write_str(&self.uri)
97
232
}
98
233
}
99
234
100
-
impl<'s> From<&'s AtUri<'_>> for &'s str {
101
-
fn from(value: &'s AtUri<'_>) -> Self {
102
-
value.0.as_ref()
235
+
impl<'d> From<AtUri<'d>> for String {
236
+
fn from(value: AtUri<'d>) -> Self {
237
+
value.uri.to_string()
103
238
}
104
239
}
105
240
106
-
impl<'a> From<AtUri<'a>> for CowStr<'a> {
107
-
fn from(value: AtUri<'a>) -> Self {
108
-
value.0
241
+
impl<'d> From<AtUri<'d>> for CowStr<'d> {
242
+
fn from(value: AtUri<'d>) -> Self {
243
+
value.uri
109
244
}
110
245
}
111
246
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
-
}
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)
121
252
}
122
253
}
123
254
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
-
}
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)
133
260
}
134
261
}
135
262
136
263
impl AsRef<str> for AtUri<'_> {
137
264
fn as_ref(&self) -> &str {
138
-
self.as_str()
265
+
&self.uri.as_ref()
139
266
}
140
267
}
141
268
···
143
270
type Target = str;
144
271
145
272
fn deref(&self) -> &Self::Target {
146
-
self.as_str()
273
+
self.uri.as_ref()
147
274
}
148
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;
1
+
use crate::{CowStr, IntoStatic, types::cid::Cid};
3
2
#[allow(unused)]
4
3
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
4
+
use smol_str::ToSmolStr;
5
+
use std::convert::Infallible;
5
6
#[allow(unused)]
6
7
use std::{
7
8
borrow::Cow,
···
39
40
/// Wrapper for file type
40
41
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
41
42
#[serde(transparent)]
43
+
#[repr(transparent)]
42
44
pub struct MimeType<'m>(pub CowStr<'m>);
43
45
44
46
impl<'m> MimeType<'m> {
45
47
/// Fallible constructor, validates, borrows from input
46
48
pub fn new(mime_type: &'m str) -> Result<MimeType<'m>, &'static str> {
47
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))
48
58
}
49
59
50
60
/// Fallible constructor from an existing CowStr, borrows
···
66
76
}
67
77
68
78
impl FromStr for MimeType<'_> {
69
-
type Err = &'static str;
79
+
type Err = Infallible;
70
80
71
81
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
72
82
fn from_str(s: &str) -> Result<Self, Self::Err> {
73
-
Self::from_cowstr(CowStr::Owned(s.to_compact_string()))
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())
74
92
}
75
93
}
76
94
···
107
125
108
126
impl From<String> for MimeType<'static> {
109
127
fn from(value: String) -> Self {
110
-
Self(CowStr::Owned(value.to_compact_string()))
128
+
Self(CowStr::Owned(value.to_smolstr()))
111
129
}
112
130
}
113
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
-
1
+
use crate::{CowStr, IntoStatic};
6
2
pub use cid::Cid as IpldCid;
7
-
8
-
use crate::CowStr;
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};
9
6
10
7
/// raw
11
8
pub const ATP_CID_CODEC: u64 = 0x55;
···
47
44
let s = CowStr::Owned(
48
45
cid.to_string_of_base(ATP_CID_BASE)
49
46
.unwrap_or_default()
50
-
.to_compact_string(),
47
+
.to_smolstr(),
51
48
);
52
49
Self::Ipld { cid, s }
53
50
}
···
89
86
90
87
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
91
88
fn from_str(s: &str) -> Result<Self, Self::Err> {
92
-
Ok(Cid::Str(CowStr::Owned(s.to_compact_string())))
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
+
}
93
104
}
94
105
}
95
106
···
164
175
165
176
impl From<String> for Cid<'_> {
166
177
fn from(value: String) -> Self {
167
-
Cid::Str(CowStr::Owned(value.to_compact_string()))
178
+
Cid::Str(CowStr::Owned(value.to_smolstr()))
168
179
}
169
180
}
170
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
1
use chrono::DurationRound;
5
-
use compact_str::ToCompactString;
6
2
use serde::Serializer;
7
3
use serde::{Deserialize, Deserializer, Serialize, de::Error};
4
+
use smol_str::ToSmolStr;
5
+
use std::sync::LazyLock;
6
+
use std::{cmp, str::FromStr};
8
7
9
8
use crate::{CowStr, IntoStatic};
10
9
use regex::Regex;
···
58
57
// This serialization format is compatible with ISO 8601.
59
58
let serialized = CowStr::Owned(
60
59
dt.to_rfc3339_opts(chrono::SecondsFormat::Micros, true)
61
-
.to_compact_string(),
60
+
.to_smolstr(),
62
61
);
63
62
Self { serialized, dt }
64
63
}
···
139
138
if ISO8601_REGEX.is_match(&value) {
140
139
let dt = chrono::DateTime::parse_from_rfc3339(&value)?;
141
140
Ok(Self {
142
-
serialized: CowStr::Owned(value.to_compact_string()),
141
+
serialized: CowStr::Owned(value.to_smolstr()),
143
142
dt,
144
143
})
145
144
} else {
+53
-14
crates/jacquard-common/src/types/did.rs
+53
-14
crates/jacquard-common/src/types/did.rs
···
1
+
use crate::{CowStr, IntoStatic};
2
+
use regex::Regex;
3
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
4
+
use smol_str::ToSmolStr;
1
5
use std::fmt;
2
6
use std::sync::LazyLock;
3
7
use std::{ops::Deref, str::FromStr};
4
8
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)]
9
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
12
10
#[serde(transparent)]
11
+
#[repr(transparent)]
13
12
pub struct Did<'d>(CowStr<'d>);
14
13
15
14
pub static DID_REGEX: LazyLock<Regex> =
···
18
17
impl<'d> Did<'d> {
19
18
/// Fallible constructor, validates, borrows from input
20
19
pub fn new(did: &'d str) -> Result<Self, &'static str> {
20
+
let did = did.strip_prefix("at://").unwrap_or(did);
21
21
if did.len() > 2048 {
22
22
Err("DID too long")
23
23
} else if !DID_REGEX.is_match(did) {
···
27
27
}
28
28
}
29
29
30
-
/// Fallible constructor from an existing CowStr, takes ownership
31
-
pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> {
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);
32
34
if did.len() > 2048 {
33
35
Err("DID too long")
34
-
} else if !DID_REGEX.is_match(&did) {
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) {
35
49
Err("Invalid DID")
36
50
} else {
37
-
Ok(Self(did.into_static()))
51
+
Ok(Self(CowStr::new_static(did)))
38
52
}
39
53
}
40
54
···
43
57
/// or API values you know are valid (rather than using serde), this is the one to use.
44
58
/// The From<String> and From<CowStr> impls use the same logic.
45
59
pub fn raw(did: &'d str) -> Self {
60
+
let did = did.strip_prefix("at://").unwrap_or(did);
46
61
if did.len() > 2048 {
47
62
panic!("DID too long")
48
63
} else if !DID_REGEX.is_match(did) {
···
72
87
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
73
88
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
74
89
fn from_str(s: &str) -> Result<Self, Self::Err> {
75
-
Self::from_cowstr(CowStr::Borrowed(s).into_static())
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())
76
99
}
77
100
}
78
101
···
92
115
}
93
116
}
94
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
+
95
124
impl<'d> From<Did<'d>> for String {
96
125
fn from(value: Did<'d>) -> Self {
97
126
value.0.to_string()
···
106
135
107
136
impl From<String> for Did<'static> {
108
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
+
};
109
143
if value.len() > 2048 {
110
144
panic!("DID too long")
111
145
} else if !DID_REGEX.is_match(&value) {
112
146
panic!("Invalid DID")
113
147
} else {
114
-
Self(CowStr::Owned(value.to_compact_string()))
148
+
Self(value.into_static())
115
149
}
116
150
}
117
151
}
118
152
119
153
impl<'d> From<CowStr<'d>> for Did<'d> {
120
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
+
};
121
160
if value.len() > 2048 {
122
161
panic!("DID too long")
123
162
} else if !DID_REGEX.is_match(&value) {
124
163
panic!("Invalid DID")
125
164
} else {
126
-
Self(value)
165
+
Self(value.into_static())
127
166
}
128
167
}
129
168
}
+88
-30
crates/jacquard-common/src/types/handle.rs
+88
-30
crates/jacquard-common/src/types/handle.rs
···
1
+
use crate::{CowStr, IntoStatic};
2
+
use regex::Regex;
3
+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
4
+
use smol_str::ToSmolStr;
1
5
use std::fmt;
2
6
use std::sync::LazyLock;
3
7
use std::{ops::Deref, str::FromStr};
4
8
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)]
9
+
#[derive(Clone, PartialEq, Eq, Serialize, Hash)]
12
10
#[serde(transparent)]
11
+
#[repr(transparent)]
13
12
pub struct Handle<'h>(CowStr<'h>);
14
13
15
14
pub static HANDLE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
16
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()
17
16
});
18
17
18
+
/// AT Protocol handle
19
19
impl<'h> Handle<'h> {
20
20
/// Fallible constructor, validates, borrows from input
21
21
///
22
22
/// Accepts (and strips) preceding '@' if present
23
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 {
24
+
let handle = handle
25
+
.strip_prefix("at://")
26
+
.unwrap_or(handle)
27
+
.strip_prefix('@')
28
+
.unwrap_or(handle);
29
+
if handle.len() > 253 {
26
30
Err("handle too long")
27
31
} else if !HANDLE_REGEX.is_match(handle) {
28
32
Err("Invalid handle")
···
31
35
}
32
36
}
33
37
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 {
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 {
44
47
Err("handle too long")
45
-
} else if !HANDLE_REGEX.is_match(&handle) {
48
+
} else if !HANDLE_REGEX.is_match(handle) {
46
49
Err("Invalid handle")
47
50
} else {
48
-
Ok(Self(handle.into_static()))
51
+
Ok(Self(CowStr::Owned(handle.to_smolstr())))
49
52
}
50
53
}
51
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
+
}
52
70
/// Infallible constructor for when you *know* the string is a valid handle.
53
71
/// Will panic on invalid handles. If you're manually decoding atproto records
54
72
/// or API values you know are valid (rather than using serde), this is the one to use.
···
56
74
///
57
75
/// Accepts (and strips) preceding '@' if present
58
76
pub fn raw(handle: &'h str) -> Self {
59
-
let handle = handle.strip_prefix('@').unwrap_or(handle);
60
-
if handle.len() > 2048 {
77
+
let handle = handle
78
+
.strip_prefix("at://")
79
+
.unwrap_or(handle)
80
+
.strip_prefix('@')
81
+
.unwrap_or(handle);
82
+
if handle.len() > 253 {
61
83
panic!("handle too long")
62
84
} else if !HANDLE_REGEX.is_match(handle) {
63
85
panic!("Invalid handle")
···
71
93
///
72
94
/// Accepts (and strips) preceding '@' if present
73
95
pub unsafe fn unchecked(handle: &'h str) -> Self {
74
-
let handle = handle.strip_prefix('@').unwrap_or(handle);
96
+
let handle = handle
97
+
.strip_prefix("at://")
98
+
.unwrap_or(handle)
99
+
.strip_prefix('@')
100
+
.unwrap_or(handle);
75
101
Self(CowStr::Borrowed(handle))
76
102
}
77
103
···
89
115
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
90
116
/// Prefer `Handle::new()` or `Handle::raw` if you want to borrow.
91
117
fn from_str(s: &str) -> Result<Self, Self::Err> {
92
-
Self::from_cowstr(CowStr::Borrowed(s).into_static())
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())
93
127
}
94
128
}
95
129
···
105
139
106
140
impl fmt::Display for Handle<'_> {
107
141
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108
-
write!(f, "@{}", self.0)
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)
109
149
}
110
150
}
111
151
···
123
163
124
164
impl From<String> for Handle<'static> {
125
165
fn from(value: String) -> Self {
126
-
if value.len() > 2048 {
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 {
127
176
panic!("handle too long")
128
177
} else if !HANDLE_REGEX.is_match(&value) {
129
178
panic!("Invalid handle")
130
179
} else {
131
-
Self(CowStr::Owned(value.to_compact_string()))
180
+
Self(value.into_static())
132
181
}
133
182
}
134
183
}
135
184
136
185
impl<'h> From<CowStr<'h>> for Handle<'h> {
137
186
fn from(value: CowStr<'h>) -> Self {
138
-
if value.len() > 2048 {
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 {
139
197
panic!("handle too long")
140
198
} else if !HANDLE_REGEX.is_match(&value) {
141
199
panic!("Invalid handle")
142
200
} else {
143
-
Self(value)
201
+
Self(value.into_static())
144
202
}
145
203
}
146
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
1
use crate::types::handle::Handle;
2
+
use crate::{IntoStatic, types::did::Did};
3
3
use std::fmt;
4
4
use std::str::FromStr;
5
5
···
26
26
}
27
27
}
28
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() {
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) {
32
33
Ok(AtIdentifier::Did(did))
33
34
} else {
34
-
ident.parse().map(AtIdentifier::Handle)
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)?))
35
45
}
36
46
}
37
47
···
90
100
Ok(AtIdentifier::Did(did))
91
101
} else {
92
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()),
93
114
}
94
115
}
95
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};
1
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 serde::{Deserialize, Deserializer, Serialize, de::Error};
2
+
use smol_str::{SmolStr, SmolStrBuilder};
1
3
use std::fmt;
2
4
use std::sync::LazyLock;
3
5
use std::{ops::Deref, str::FromStr};
4
6
5
-
use compact_str::{CompactString, ToCompactString};
6
-
use serde::{Deserialize, Deserializer, Serialize, de::Error};
7
-
7
+
use crate::CowStr;
8
8
use crate::types::integer::LimitedU32;
9
-
use crate::{CowStr, IntoStatic};
10
9
use regex::Regex;
11
10
12
-
fn s32_encode(mut i: u64) -> CowStr<'static> {
11
+
fn s32_encode(mut i: u64) -> SmolStr {
13
12
const S32_CHAR: &[u8] = b"234567abcdefghijklmnopqrstuvwxyz";
14
13
15
-
let mut s = CompactString::with_capacity(13);
14
+
let mut s = SmolStrBuilder::new();
16
15
for _ in 0..13 {
17
16
let c = i & 0x1F;
18
17
s.push(S32_CHAR[c as usize] as char);
···
20
19
i >>= 5;
21
20
}
22
21
23
-
// Reverse the string to convert it to big-endian format.
24
-
CowStr::Owned(s.chars().rev().collect())
22
+
let mut builder = SmolStrBuilder::new();
23
+
for c in s.finish().chars().rev() {
24
+
builder.push(c);
25
+
}
26
+
builder.finish()
25
27
}
26
28
27
29
static TID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
···
33
35
/// [Timestamp Identifier]: https://atproto.com/specs/tid
34
36
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Hash)]
35
37
#[serde(transparent)]
36
-
pub struct Tid<'t>(CowStr<'t>);
38
+
#[repr(transparent)]
39
+
pub struct Tid(SmolStr);
37
40
38
-
impl<'t> Tid<'t> {
41
+
impl Tid {
39
42
/// 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> {
43
+
pub fn new(tid: impl AsRef<str>) -> Result<Self, &'static str> {
44
+
let tid = tid.as_ref();
52
45
if tid.len() != 13 {
53
46
Err("TID must be 13 characters")
54
-
} else if !TID_REGEX.is_match(&tid) {
47
+
} else if !TID_REGEX.is_match(&tid.as_ref()) {
55
48
Err("Invalid TID")
56
49
} else {
57
-
Ok(Self(tid.into_static()))
50
+
Ok(Self(SmolStr::new_inline(&tid)))
58
51
}
59
52
}
60
53
···
62
55
/// Will panic on invalid TID. If you're manually decoding atproto records
63
56
/// or API values you know are valid (rather than using serde), this is the one to use.
64
57
/// The From<String> and From<CowStr> impls use the same logic.
65
-
pub fn raw(tid: &'t str) -> Self {
58
+
pub fn raw(tid: impl AsRef<str>) -> Self {
59
+
let tid = tid.as_ref();
66
60
if tid.len() != 13 {
67
61
panic!("TID must be 13 characters")
68
62
} else if !TID_REGEX.is_match(&tid) {
69
63
panic!("Invalid TID")
70
64
} else {
71
-
Self(CowStr::Borrowed(tid))
65
+
Self(SmolStr::new_inline(tid))
72
66
}
73
67
}
74
68
75
69
/// Infallible constructor for when you *know* the string is a valid TID.
76
70
/// 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))
71
+
pub unsafe fn unchecked(tid: impl AsRef<str>) -> Self {
72
+
let tid = tid.as_ref();
73
+
Self(SmolStr::new_inline(tid))
79
74
}
80
75
81
76
/// Construct a new timestamp with the specified clock ID.
···
121
116
}
122
117
}
123
118
124
-
impl FromStr for Tid<'_> {
119
+
impl FromStr for Tid {
125
120
type Err = &'static str;
126
121
127
122
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
128
123
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
129
124
fn from_str(s: &str) -> Result<Self, Self::Err> {
130
-
Self::from_cowstr(CowStr::Borrowed(s).into_static())
125
+
Self::new(s)
131
126
}
132
127
}
133
128
134
-
impl<'de> Deserialize<'de> for Tid<'de> {
129
+
impl<'de> Deserialize<'de> for Tid {
135
130
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
136
131
where
137
132
D: Deserializer<'de>,
138
133
{
139
-
let value = Deserialize::deserialize(deserializer)?;
134
+
let value: &str = Deserialize::deserialize(deserializer)?;
140
135
Self::new(value).map_err(D::Error::custom)
141
136
}
142
137
}
143
138
144
-
impl fmt::Display for Tid<'_> {
139
+
impl fmt::Display for Tid {
145
140
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146
141
f.write_str(&self.0)
147
142
}
148
143
}
149
144
150
-
impl<'t> From<Tid<'t>> for String {
151
-
fn from(value: Tid<'t>) -> Self {
145
+
impl From<Tid> for String {
146
+
fn from(value: Tid) -> Self {
152
147
value.0.to_string()
153
148
}
154
149
}
155
150
156
-
impl<'t> From<Tid<'t>> for CowStr<'t> {
157
-
fn from(value: Tid<'t>) -> Self {
151
+
impl From<Tid> for SmolStr {
152
+
fn from(value: Tid) -> Self {
158
153
value.0
159
154
}
160
155
}
161
156
162
-
impl From<String> for Tid<'static> {
157
+
impl From<String> for Tid {
163
158
fn from(value: String) -> Self {
164
159
if value.len() != 13 {
165
160
panic!("TID must be 13 characters")
166
161
} else if !TID_REGEX.is_match(&value) {
167
162
panic!("Invalid TID")
168
163
} else {
169
-
Self(CowStr::Owned(value.to_compact_string()))
164
+
Self(SmolStr::new_inline(&value))
170
165
}
171
166
}
172
167
}
173
168
174
-
impl<'t> From<CowStr<'t>> for Tid<'t> {
169
+
impl<'t> From<CowStr<'t>> for Tid {
175
170
fn from(value: CowStr<'t>) -> Self {
176
171
if value.len() != 13 {
177
172
panic!("TID must be 13 characters")
178
173
} else if !TID_REGEX.is_match(&value) {
179
174
panic!("Invalid TID")
180
175
} else {
181
-
Self(value)
176
+
Self(SmolStr::new_inline(&value))
182
177
}
183
178
}
184
179
}
185
180
186
-
impl AsRef<str> for Tid<'_> {
181
+
impl AsRef<str> for Tid {
187
182
fn as_ref(&self) -> &str {
188
183
self.as_str()
189
184
}
190
185
}
191
186
192
-
impl Deref for Tid<'_> {
187
+
impl Deref for Tid {
193
188
type Target = str;
194
189
195
190
fn deref(&self) -> &Self::Target {