+14
.tangled/workflows/build.yml
+14
.tangled/workflows/build.yml
+302
Cargo.lock
+302
Cargo.lock
···
12
]
13
14
[[package]]
15
name = "anstream"
16
version = "0.6.20"
17
source = "registry+https://github.com/rust-lang/crates.io-index"
···
62
]
63
64
[[package]]
65
name = "base-x"
66
version = "0.2.11"
67
source = "registry+https://github.com/rust-lang/crates.io-index"
68
checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
69
70
[[package]]
71
name = "castaway"
72
version = "0.2.4"
73
source = "registry+https://github.com/rust-lang/crates.io-index"
···
77
]
78
79
[[package]]
80
name = "cfg-if"
81
version = "1.0.3"
82
source = "registry+https://github.com/rust-lang/crates.io-index"
83
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
84
85
[[package]]
86
name = "cid"
87
version = "0.11.1"
88
source = "registry+https://github.com/rust-lang/crates.io-index"
···
157
]
158
159
[[package]]
160
name = "core2"
161
version = "0.4.0"
162
source = "registry+https://github.com/rust-lang/crates.io-index"
···
192
]
193
194
[[package]]
195
name = "heck"
196
version = "0.5.0"
197
source = "registry+https://github.com/rust-lang/crates.io-index"
198
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
199
200
[[package]]
201
name = "is_terminal_polyfill"
202
version = "1.70.1"
203
source = "registry+https://github.com/rust-lang/crates.io-index"
···
214
version = "0.1.0"
215
dependencies = [
216
"clap",
217
]
218
219
[[package]]
220
name = "jacquard-common"
221
version = "0.1.0"
222
dependencies = [
223
"cid",
224
"compact_str",
225
"miette",
···
227
"multihash",
228
"regex",
229
"serde",
230
"thiserror",
231
]
232
233
[[package]]
234
name = "memchr"
···
281
]
282
283
[[package]]
284
name = "once_cell_polyfill"
285
version = "1.70.1"
286
source = "registry+https://github.com/rust-lang/crates.io-index"
287
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
288
289
[[package]]
290
name = "proc-macro2"
···
385
]
386
387
[[package]]
388
name = "static_assertions"
389
version = "1.1.0"
390
source = "registry+https://github.com/rust-lang/crates.io-index"
···
452
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
453
454
[[package]]
455
name = "windows-link"
456
version = "0.2.0"
457
source = "registry+https://github.com/rust-lang/crates.io-index"
458
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
459
460
[[package]]
461
name = "windows-sys"
···
12
]
13
14
[[package]]
15
+
name = "android_system_properties"
16
+
version = "0.1.5"
17
+
source = "registry+https://github.com/rust-lang/crates.io-index"
18
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
19
+
dependencies = [
20
+
"libc",
21
+
]
22
+
23
+
[[package]]
24
name = "anstream"
25
version = "0.6.20"
26
source = "registry+https://github.com/rust-lang/crates.io-index"
···
71
]
72
73
[[package]]
74
+
name = "autocfg"
75
+
version = "1.5.0"
76
+
source = "registry+https://github.com/rust-lang/crates.io-index"
77
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
78
+
79
+
[[package]]
80
name = "base-x"
81
version = "0.2.11"
82
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
···
98
]
99
100
[[package]]
101
+
name = "cc"
102
+
version = "1.2.39"
103
+
source = "registry+https://github.com/rust-lang/crates.io-index"
104
+
checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f"
105
+
dependencies = [
106
+
"find-msvc-tools",
107
+
"shlex",
108
+
]
109
+
110
+
[[package]]
111
name = "cfg-if"
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"
118
+
version = "0.4.42"
119
+
source = "registry+https://github.com/rust-lang/crates.io-index"
120
+
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
121
+
dependencies = [
122
+
"iana-time-zone",
123
+
"js-sys",
124
+
"num-traits",
125
+
"wasm-bindgen",
126
+
"windows-link",
127
+
]
128
+
129
+
[[package]]
130
name = "cid"
131
version = "0.11.1"
132
source = "registry+https://github.com/rust-lang/crates.io-index"
···
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"
207
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
208
+
209
+
[[package]]
210
name = "core2"
211
version = "0.4.0"
212
source = "registry+https://github.com/rust-lang/crates.io-index"
···
242
]
243
244
[[package]]
245
+
name = "equivalent"
246
+
version = "1.0.2"
247
+
source = "registry+https://github.com/rust-lang/crates.io-index"
248
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
249
+
250
+
[[package]]
251
+
name = "find-msvc-tools"
252
+
version = "0.1.2"
253
+
source = "registry+https://github.com/rust-lang/crates.io-index"
254
+
checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959"
255
+
256
+
[[package]]
257
+
name = "form_urlencoded"
258
+
version = "1.2.2"
259
+
source = "registry+https://github.com/rust-lang/crates.io-index"
260
+
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
261
+
dependencies = [
262
+
"percent-encoding",
263
+
]
264
+
265
+
[[package]]
266
+
name = "hashbrown"
267
+
version = "0.16.0"
268
+
source = "registry+https://github.com/rust-lang/crates.io-index"
269
+
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
270
+
271
+
[[package]]
272
name = "heck"
273
version = "0.5.0"
274
source = "registry+https://github.com/rust-lang/crates.io-index"
275
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
276
277
[[package]]
278
+
name = "iana-time-zone"
279
+
version = "0.1.64"
280
+
source = "registry+https://github.com/rust-lang/crates.io-index"
281
+
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
282
+
dependencies = [
283
+
"android_system_properties",
284
+
"core-foundation-sys",
285
+
"iana-time-zone-haiku",
286
+
"js-sys",
287
+
"log",
288
+
"wasm-bindgen",
289
+
"windows-core",
290
+
]
291
+
292
+
[[package]]
293
+
name = "iana-time-zone-haiku"
294
+
version = "0.1.2"
295
+
source = "registry+https://github.com/rust-lang/crates.io-index"
296
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
297
+
dependencies = [
298
+
"cc",
299
+
]
300
+
301
+
[[package]]
302
+
name = "indexmap"
303
+
version = "2.11.4"
304
+
source = "registry+https://github.com/rust-lang/crates.io-index"
305
+
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
306
+
dependencies = [
307
+
"equivalent",
308
+
"hashbrown",
309
+
]
310
+
311
+
[[package]]
312
name = "is_terminal_polyfill"
313
version = "1.70.1"
314
source = "registry+https://github.com/rust-lang/crates.io-index"
···
325
version = "0.1.0"
326
dependencies = [
327
"clap",
328
+
"jacquard-common",
329
]
330
331
[[package]]
332
name = "jacquard-common"
333
version = "0.1.0"
334
dependencies = [
335
+
"chrono",
336
"cid",
337
"compact_str",
338
"miette",
···
340
"multihash",
341
"regex",
342
"serde",
343
+
"serde_html_form",
344
+
"serde_json",
345
"thiserror",
346
]
347
+
348
+
[[package]]
349
+
name = "js-sys"
350
+
version = "0.3.81"
351
+
source = "registry+https://github.com/rust-lang/crates.io-index"
352
+
checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305"
353
+
dependencies = [
354
+
"once_cell",
355
+
"wasm-bindgen",
356
+
]
357
+
358
+
[[package]]
359
+
name = "libc"
360
+
version = "0.2.176"
361
+
source = "registry+https://github.com/rust-lang/crates.io-index"
362
+
checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
363
+
364
+
[[package]]
365
+
name = "log"
366
+
version = "0.4.28"
367
+
source = "registry+https://github.com/rust-lang/crates.io-index"
368
+
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
369
370
[[package]]
371
name = "memchr"
···
418
]
419
420
[[package]]
421
+
name = "num-traits"
422
+
version = "0.2.19"
423
+
source = "registry+https://github.com/rust-lang/crates.io-index"
424
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
425
+
dependencies = [
426
+
"autocfg",
427
+
]
428
+
429
+
[[package]]
430
+
name = "once_cell"
431
+
version = "1.21.3"
432
+
source = "registry+https://github.com/rust-lang/crates.io-index"
433
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
434
+
435
+
[[package]]
436
name = "once_cell_polyfill"
437
version = "1.70.1"
438
source = "registry+https://github.com/rust-lang/crates.io-index"
439
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
440
+
441
+
[[package]]
442
+
name = "percent-encoding"
443
+
version = "2.3.2"
444
+
source = "registry+https://github.com/rust-lang/crates.io-index"
445
+
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
446
447
[[package]]
448
name = "proc-macro2"
···
543
]
544
545
[[package]]
546
+
name = "serde_html_form"
547
+
version = "0.2.8"
548
+
source = "registry+https://github.com/rust-lang/crates.io-index"
549
+
checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f"
550
+
dependencies = [
551
+
"form_urlencoded",
552
+
"indexmap",
553
+
"itoa",
554
+
"ryu",
555
+
"serde_core",
556
+
]
557
+
558
+
[[package]]
559
+
name = "serde_json"
560
+
version = "1.0.145"
561
+
source = "registry+https://github.com/rust-lang/crates.io-index"
562
+
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
563
+
dependencies = [
564
+
"itoa",
565
+
"memchr",
566
+
"ryu",
567
+
"serde",
568
+
"serde_core",
569
+
]
570
+
571
+
[[package]]
572
+
name = "shlex"
573
+
version = "1.3.0"
574
+
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
···
642
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
643
644
[[package]]
645
+
name = "wasm-bindgen"
646
+
version = "0.2.104"
647
+
source = "registry+https://github.com/rust-lang/crates.io-index"
648
+
checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d"
649
+
dependencies = [
650
+
"cfg-if",
651
+
"once_cell",
652
+
"rustversion",
653
+
"wasm-bindgen-macro",
654
+
"wasm-bindgen-shared",
655
+
]
656
+
657
+
[[package]]
658
+
name = "wasm-bindgen-backend"
659
+
version = "0.2.104"
660
+
source = "registry+https://github.com/rust-lang/crates.io-index"
661
+
checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19"
662
+
dependencies = [
663
+
"bumpalo",
664
+
"log",
665
+
"proc-macro2",
666
+
"quote",
667
+
"syn",
668
+
"wasm-bindgen-shared",
669
+
]
670
+
671
+
[[package]]
672
+
name = "wasm-bindgen-macro"
673
+
version = "0.2.104"
674
+
source = "registry+https://github.com/rust-lang/crates.io-index"
675
+
checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119"
676
+
dependencies = [
677
+
"quote",
678
+
"wasm-bindgen-macro-support",
679
+
]
680
+
681
+
[[package]]
682
+
name = "wasm-bindgen-macro-support"
683
+
version = "0.2.104"
684
+
source = "registry+https://github.com/rust-lang/crates.io-index"
685
+
checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7"
686
+
dependencies = [
687
+
"proc-macro2",
688
+
"quote",
689
+
"syn",
690
+
"wasm-bindgen-backend",
691
+
"wasm-bindgen-shared",
692
+
]
693
+
694
+
[[package]]
695
+
name = "wasm-bindgen-shared"
696
+
version = "0.2.104"
697
+
source = "registry+https://github.com/rust-lang/crates.io-index"
698
+
checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1"
699
+
dependencies = [
700
+
"unicode-ident",
701
+
]
702
+
703
+
[[package]]
704
+
name = "windows-core"
705
+
version = "0.62.1"
706
+
source = "registry+https://github.com/rust-lang/crates.io-index"
707
+
checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9"
708
+
dependencies = [
709
+
"windows-implement",
710
+
"windows-interface",
711
+
"windows-link",
712
+
"windows-result",
713
+
"windows-strings",
714
+
]
715
+
716
+
[[package]]
717
+
name = "windows-implement"
718
+
version = "0.60.1"
719
+
source = "registry+https://github.com/rust-lang/crates.io-index"
720
+
checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0"
721
+
dependencies = [
722
+
"proc-macro2",
723
+
"quote",
724
+
"syn",
725
+
]
726
+
727
+
[[package]]
728
+
name = "windows-interface"
729
+
version = "0.59.2"
730
+
source = "registry+https://github.com/rust-lang/crates.io-index"
731
+
checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5"
732
+
dependencies = [
733
+
"proc-macro2",
734
+
"quote",
735
+
"syn",
736
+
]
737
+
738
+
[[package]]
739
name = "windows-link"
740
version = "0.2.0"
741
source = "registry+https://github.com/rust-lang/crates.io-index"
742
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
743
+
744
+
[[package]]
745
+
name = "windows-result"
746
+
version = "0.4.0"
747
+
source = "registry+https://github.com/rust-lang/crates.io-index"
748
+
checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f"
749
+
dependencies = [
750
+
"windows-link",
751
+
]
752
+
753
+
[[package]]
754
+
name = "windows-strings"
755
+
version = "0.5.0"
756
+
source = "registry+https://github.com/rust-lang/crates.io-index"
757
+
checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda"
758
+
dependencies = [
759
+
"windows-link",
760
+
]
761
762
[[package]]
763
name = "windows-sys"
+3
crates/jacquard-common/Cargo.toml
+3
crates/jacquard-common/Cargo.toml
···
6
description.workspace = true
7
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"
···
14
multihash = "0.19.3"
15
regex = "1.11.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"
crates/jacquard-common/src/aturi.rs
crates/jacquard-common/src/aturi.rs
This is a binary file and will not be displayed.
+1
-1
crates/jacquard-common/src/blob.rs
crates/jacquard-common/src/types/blob.rs
+1
-1
crates/jacquard-common/src/blob.rs
crates/jacquard-common/src/types/blob.rs
crates/jacquard-common/src/cid.rs
crates/jacquard-common/src/types/cid.rs
crates/jacquard-common/src/cid.rs
crates/jacquard-common/src/types/cid.rs
+1
-52
crates/jacquard-common/src/cowstr.rs
+1
-52
crates/jacquard-common/src/cowstr.rs
···
1
use compact_str::CompactString;
2
-
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
3
use std::{
4
borrow::Cow,
5
fmt,
6
hash::{Hash, Hasher},
7
ops::Deref,
8
-
str::FromStr,
9
};
10
11
use crate::IntoStatic;
···
207
CowStr::Owned(s) => CowStr::Owned(s),
208
}
209
}
210
-
}
211
-
212
-
/// Common trait implementations for Lexicon string formats that are newtype wrappers
213
-
/// around `String`.
214
-
macro_rules! string_newtype {
215
-
($name:ident) => {
216
-
impl FromStr for $name<'_> {
217
-
type Err = &'static str;
218
-
219
-
fn from_str(s: &str) -> Result<Self, Self::Err> {
220
-
Self::new(s)
221
-
}
222
-
}
223
-
224
-
impl<'de> Deserialize<'de> for $name<'de> {
225
-
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
226
-
where
227
-
D: Deserializer<'de>,
228
-
{
229
-
let value = Deserialize::deserialize(deserializer)?;
230
-
Self::new(value).map_err(D::Error::custom)
231
-
}
232
-
}
233
-
234
-
impl From<$name<'_>> for String {
235
-
fn from(value: $name) -> Self {
236
-
value.0.to_string()
237
-
}
238
-
}
239
-
240
-
impl From<$name> for CowStr<'s> {
241
-
fn from(value: $name) -> Self {
242
-
value.0
243
-
}
244
-
}
245
-
246
-
impl AsRef<str> for $name<'_> {
247
-
fn as_ref(&self) -> &str {
248
-
self.as_str()
249
-
}
250
-
}
251
-
252
-
impl Deref for $name<'_> {
253
-
type Target = str;
254
-
255
-
fn deref(&self) -> &Self::Target {
256
-
self.as_str()
257
-
}
258
-
}
259
-
};
260
}
261
262
impl Serialize for CowStr<'_> {
+2
-2
crates/jacquard-common/src/did.rs
crates/jacquard-common/src/types/did.rs
+2
-2
crates/jacquard-common/src/did.rs
crates/jacquard-common/src/types/did.rs
···
27
}
28
}
29
30
-
/// Fallible constructor from an existing CowStr, clones and takes
31
pub fn from_cowstr(did: CowStr<'d>) -> Result<Did<'d>, &'static str> {
32
if did.len() > 2048 {
33
Err("DID too long")
···
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::Owned(s.to_compact_string()))
76
}
77
}
78
···
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")
···
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
crates/jacquard-common/src/handle.rs
crates/jacquard-common/src/handle.rs
This is a binary file and will not be displayed.
+2
-9
crates/jacquard-common/src/lib.rs
+2
-9
crates/jacquard-common/src/lib.rs
crates/jacquard-common/src/link.rs
crates/jacquard-common/src/types/link.rs
crates/jacquard-common/src/link.rs
crates/jacquard-common/src/types/link.rs
crates/jacquard-common/src/nsid.rs
crates/jacquard-common/src/types/nsid.rs
crates/jacquard-common/src/nsid.rs
crates/jacquard-common/src/types/nsid.rs
+11
crates/jacquard-common/src/types.rs
+11
crates/jacquard-common/src/types.rs
+148
crates/jacquard-common/src/types/aturi.rs
+148
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
+
}
66
+
}
67
+
68
+
impl FromStr for AtUri<'_> {
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
+
142
+
impl Deref for AtUri<'_> {
143
+
type Target = str;
144
+
145
+
fn deref(&self) -> &Self::Target {
146
+
self.as_str()
147
+
}
148
+
}
+166
crates/jacquard-common/src/types/datetime.rs
+166
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;
11
+
12
+
pub static ISO8601_REGEX: LazyLock<Regex> = LazyLock::new(|| {
13
+
Regex::new(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]+)?(Z|(\+[0-9]{2}|\-[0-9][1-9]):[0-9]{2})$").unwrap()
14
+
});
15
+
16
+
/// A Lexicon timestamp.
17
+
#[derive(Clone, Debug, Eq)]
18
+
pub struct Datetime {
19
+
/// Serialized form. Preserved during parsing to ensure round-trip re-serialization.
20
+
serialized: CowStr<'static>,
21
+
/// Parsed form.
22
+
dt: chrono::DateTime<chrono::FixedOffset>,
23
+
}
24
+
25
+
impl PartialEq for Datetime {
26
+
fn eq(&self, other: &Self) -> bool {
27
+
self.dt == other.dt
28
+
}
29
+
}
30
+
31
+
impl Ord for Datetime {
32
+
fn cmp(&self, other: &Self) -> cmp::Ordering {
33
+
self.dt.cmp(&other.dt)
34
+
}
35
+
}
36
+
37
+
impl PartialOrd for Datetime {
38
+
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
39
+
Some(self.cmp(other))
40
+
}
41
+
}
42
+
43
+
impl Datetime {
44
+
/// Returns a `Datetime` which corresponds to the current date and time in UTC.
45
+
///
46
+
/// The timestamp uses microsecond precision.
47
+
pub fn now() -> Self {
48
+
Self::new(chrono::Utc::now().fixed_offset())
49
+
}
50
+
51
+
/// Constructs a new Lexicon timestamp.
52
+
///
53
+
/// The timestamp is rounded to microsecond precision.
54
+
pub fn new(dt: chrono::DateTime<chrono::FixedOffset>) -> Self {
55
+
let dt = dt
56
+
.duration_round(chrono::Duration::microseconds(1))
57
+
.expect("delta does not exceed limits");
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
+
}
65
+
66
+
/// Infallibly parses a new Lexicon timestamp from a compatible str reference
67
+
///
68
+
/// Panics if invalid. Use the fallible trait implementations or deserialize for input
69
+
/// you cannot reasonably trust to be properly formatted.
70
+
pub fn raw_str(s: impl AsRef<str>) -> Self {
71
+
let s = s.as_ref();
72
+
if ISO8601_REGEX.is_match(s) {
73
+
let dt = chrono::DateTime::parse_from_rfc3339(s).expect("valid ISO8601 time string");
74
+
Self {
75
+
serialized: CowStr::Borrowed(s).into_static(),
76
+
dt,
77
+
}
78
+
} else {
79
+
panic!("atproto datetime should be valid ISO8601")
80
+
}
81
+
}
82
+
83
+
/// Extracts a string slice containing the entire `Datetime`.
84
+
#[inline]
85
+
#[must_use]
86
+
pub fn as_str(&self) -> &str {
87
+
self.serialized.as_ref()
88
+
}
89
+
}
90
+
91
+
impl FromStr for Datetime {
92
+
type Err = chrono::ParseError;
93
+
94
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
95
+
// The `chrono` crate only supports RFC 3339 parsing, but Lexicon restricts
96
+
// datetimes to the subset that is also valid under ISO 8601. Apply a regex that
97
+
// validates enough of the relevant ISO 8601 format that the RFC 3339 parser can
98
+
// do the rest.
99
+
if ISO8601_REGEX.is_match(s) {
100
+
let dt = chrono::DateTime::parse_from_rfc3339(s)?;
101
+
Ok(Self {
102
+
serialized: CowStr::Borrowed(s).into_static(),
103
+
dt,
104
+
})
105
+
} else {
106
+
// Simulate an invalid `ParseError`.
107
+
Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid"))
108
+
}
109
+
}
110
+
}
111
+
112
+
impl<'de> Deserialize<'de> for Datetime {
113
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114
+
where
115
+
D: Deserializer<'de>,
116
+
{
117
+
let value: String = Deserialize::deserialize(deserializer)?;
118
+
Self::from_str(&value).map_err(D::Error::custom)
119
+
}
120
+
}
121
+
impl Serialize for Datetime {
122
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
123
+
where
124
+
S: Serializer,
125
+
{
126
+
serializer.serialize_str(&self.serialized)
127
+
}
128
+
}
129
+
130
+
impl AsRef<chrono::DateTime<chrono::FixedOffset>> for Datetime {
131
+
fn as_ref(&self) -> &chrono::DateTime<chrono::FixedOffset> {
132
+
&self.dt
133
+
}
134
+
}
135
+
136
+
impl TryFrom<String> for Datetime {
137
+
type Error = chrono::ParseError;
138
+
fn try_from(value: String) -> Result<Self, Self::Error> {
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 {
146
+
// Simulate an invalid `ParseError`.
147
+
Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid"))
148
+
}
149
+
}
150
+
}
151
+
152
+
impl TryFrom<CowStr<'_>> for Datetime {
153
+
type Error = chrono::ParseError;
154
+
fn try_from(value: CowStr<'_>) -> Result<Self, Self::Error> {
155
+
if ISO8601_REGEX.is_match(&value) {
156
+
let dt = chrono::DateTime::parse_from_rfc3339(&value)?;
157
+
Ok(Self {
158
+
serialized: value.into_static(),
159
+
dt,
160
+
})
161
+
} else {
162
+
// Simulate an invalid `ParseError`.
163
+
Err(chrono::DateTime::parse_from_rfc3339("invalid").expect_err("invalid"))
164
+
}
165
+
}
166
+
}
+160
crates/jacquard-common/src/types/handle.rs
+160
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")
29
+
} else {
30
+
Ok(Self(CowStr::Borrowed(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.
55
+
/// The From<String> and From<CowStr> impls use the same logic.
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")
64
+
} else {
65
+
Self(CowStr::Borrowed(handle))
66
+
}
67
+
}
68
+
69
+
/// Infallible constructor for when you *know* the string is a valid handle.
70
+
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
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
+
78
+
pub fn as_str(&self) -> &str {
79
+
{
80
+
let this = &self.0;
81
+
this
82
+
}
83
+
}
84
+
}
85
+
86
+
impl FromStr for Handle<'_> {
87
+
type Err = &'static str;
88
+
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
+
96
+
impl<'de> Deserialize<'de> for Handle<'de> {
97
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98
+
where
99
+
D: Deserializer<'de>,
100
+
{
101
+
let value = Deserialize::deserialize(deserializer)?;
102
+
Self::new(value).map_err(D::Error::custom)
103
+
}
104
+
}
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
+
112
+
impl<'h> From<Handle<'h>> for String {
113
+
fn from(value: Handle<'h>) -> Self {
114
+
value.0.to_string()
115
+
}
116
+
}
117
+
118
+
impl<'h> From<Handle<'h>> for CowStr<'h> {
119
+
fn from(value: Handle<'h>) -> Self {
120
+
value.0
121
+
}
122
+
}
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
+
}
147
+
148
+
impl AsRef<str> for Handle<'_> {
149
+
fn as_ref(&self) -> &str {
150
+
self.as_str()
151
+
}
152
+
}
153
+
154
+
impl Deref for Handle<'_> {
155
+
type Target = str;
156
+
157
+
fn deref(&self) -> &Self::Target {
158
+
self.as_str()
159
+
}
160
+
}
+148
crates/jacquard-common/src/types/ident.rs
+148
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
+
6
+
use serde::{Deserialize, Serialize};
7
+
8
+
use crate::CowStr;
9
+
10
+
/// An AT Protocol identifier.
11
+
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)]
12
+
#[serde(untagged)]
13
+
pub enum AtIdentifier<'i> {
14
+
#[serde(borrow)]
15
+
Did(Did<'i>),
16
+
Handle(Handle<'i>),
17
+
}
18
+
19
+
impl<'i> AtIdentifier<'i> {
20
+
/// Fallible constructor, validates, borrows from input
21
+
pub fn new(ident: &'i str) -> Result<Self, &'static str> {
22
+
if let Ok(did) = ident.parse() {
23
+
Ok(AtIdentifier::Did(did))
24
+
} else {
25
+
ident.parse().map(AtIdentifier::Handle)
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
+
38
+
/// Infallible constructor for when you *know* the string is a valid identifier.
39
+
/// Will panic on invalid identifiers. If you're manually decoding atproto records
40
+
/// or API values you know are valid (rather than using serde), this is the one to use.
41
+
/// The From<String> and From<CowStr> impls use the same logic.
42
+
pub fn raw(ident: &'i str) -> Self {
43
+
if let Ok(did) = ident.parse() {
44
+
AtIdentifier::Did(did)
45
+
} else {
46
+
ident
47
+
.parse()
48
+
.map(AtIdentifier::Handle)
49
+
.expect("valid handle")
50
+
}
51
+
}
52
+
53
+
/// Infallible constructor for when you *know* the string is a valid identifier.
54
+
/// Marked unsafe because responsibility for upholding the invariant is on the developer.
55
+
///
56
+
/// Will validate DIDs, but will treat anything else as a valid handle
57
+
pub unsafe fn unchecked(ident: &'i str) -> Self {
58
+
if let Ok(did) = ident.parse() {
59
+
AtIdentifier::Did(did)
60
+
} else {
61
+
unsafe { AtIdentifier::Handle(Handle::unchecked(ident)) }
62
+
}
63
+
}
64
+
65
+
pub fn as_str(&self) -> &str {
66
+
match self {
67
+
AtIdentifier::Did(did) => did.as_str(),
68
+
AtIdentifier::Handle(handle) => handle.as_str(),
69
+
}
70
+
}
71
+
}
72
+
73
+
impl<'i> From<Did<'i>> for AtIdentifier<'i> {
74
+
fn from(did: Did<'i>) -> Self {
75
+
AtIdentifier::Did(did)
76
+
}
77
+
}
78
+
79
+
impl<'i> From<Handle<'i>> for AtIdentifier<'i> {
80
+
fn from(handle: Handle<'i>) -> Self {
81
+
AtIdentifier::Handle(handle)
82
+
}
83
+
}
84
+
85
+
impl FromStr for AtIdentifier<'_> {
86
+
type Err = &'static str;
87
+
88
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
89
+
if let Ok(did) = s.parse() {
90
+
Ok(AtIdentifier::Did(did))
91
+
} else {
92
+
s.parse().map(AtIdentifier::Handle)
93
+
}
94
+
}
95
+
}
96
+
97
+
impl fmt::Display for AtIdentifier<'_> {
98
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99
+
match self {
100
+
AtIdentifier::Did(did) => did.fmt(f),
101
+
AtIdentifier::Handle(handle) => handle.fmt(f),
102
+
}
103
+
}
104
+
}
105
+
106
+
impl From<String> for AtIdentifier<'static> {
107
+
fn from(value: String) -> Self {
108
+
if let Ok(did) = value.parse() {
109
+
AtIdentifier::Did(did)
110
+
} else {
111
+
value
112
+
.parse()
113
+
.map(AtIdentifier::Handle)
114
+
.expect("valid handle")
115
+
}
116
+
}
117
+
}
118
+
119
+
impl<'i> From<CowStr<'i>> for AtIdentifier<'i> {
120
+
fn from(value: CowStr<'i>) -> Self {
121
+
if let Ok(did) = value.parse() {
122
+
AtIdentifier::Did(did)
123
+
} else {
124
+
value
125
+
.parse()
126
+
.map(AtIdentifier::Handle)
127
+
.expect("valid handle")
128
+
}
129
+
}
130
+
}
131
+
132
+
impl<'i> From<AtIdentifier<'i>> for String {
133
+
fn from(value: AtIdentifier) -> Self {
134
+
match value {
135
+
AtIdentifier::Did(did) => did.into(),
136
+
AtIdentifier::Handle(handle) => handle.into(),
137
+
}
138
+
}
139
+
}
140
+
141
+
impl AsRef<str> for AtIdentifier<'_> {
142
+
fn as_ref(&self) -> &str {
143
+
match self {
144
+
AtIdentifier::Did(did) => did.as_ref(),
145
+
AtIdentifier::Handle(handle) => handle.as_ref(),
146
+
}
147
+
}
148
+
}
+326
crates/jacquard-common/src/types/integer.rs
+326
crates/jacquard-common/src/types/integer.rs
···
···
1
+
//! Lexicon integer types with minimum or maximum acceptable values.
2
+
//! Copied from [atrium](https://github.com/atrium-rs/atrium/blob/main/atrium-api/src/types/integer.rs), because this they got right
3
+
4
+
use std::num::{NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64};
5
+
use std::str::FromStr;
6
+
7
+
use serde::{Deserialize, de::Error};
8
+
9
+
macro_rules! uint {
10
+
($primitive:ident, $nz:ident, $lim:ident, $lim_nz:ident, $bounded:ident) => {
11
+
/// An unsigned integer with a maximum value of `MAX`.
12
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)]
13
+
#[repr(transparent)]
14
+
#[serde(transparent)]
15
+
pub struct $lim<const MAX: $primitive>($primitive);
16
+
17
+
impl<const MAX: $primitive> $lim<MAX> {
18
+
/// The smallest value that can be represented by this limited integer type.
19
+
pub const MIN: Self = Self(<$primitive>::MIN);
20
+
21
+
/// The largest value that can be represented by this limited integer type.
22
+
pub const MAX: Self = Self(MAX);
23
+
24
+
fn new(value: $primitive) -> Result<Self, String> {
25
+
if value > MAX {
26
+
Err(format!("value is greater than {}", MAX))
27
+
} else {
28
+
Ok(Self(value))
29
+
}
30
+
}
31
+
}
32
+
33
+
impl<const MAX: $primitive> FromStr for $lim<MAX> {
34
+
type Err = String;
35
+
36
+
fn from_str(src: &str) -> Result<Self, Self::Err> {
37
+
Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?)
38
+
}
39
+
}
40
+
41
+
impl<const MAX: $primitive> TryFrom<$primitive> for $lim<MAX> {
42
+
type Error = String;
43
+
44
+
fn try_from(value: $primitive) -> Result<Self, Self::Error> {
45
+
Self::new(value)
46
+
}
47
+
}
48
+
49
+
impl<'de, const MAX: $primitive> Deserialize<'de> for $lim<MAX> {
50
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
51
+
where
52
+
D: serde::Deserializer<'de>,
53
+
{
54
+
Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom)
55
+
}
56
+
}
57
+
58
+
impl<const MAX: $primitive> From<$lim<MAX>> for $primitive {
59
+
fn from(value: $lim<MAX>) -> Self {
60
+
value.0
61
+
}
62
+
}
63
+
64
+
/// An unsigned integer with a minimum value of 1 and a maximum value of `MAX`.
65
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)]
66
+
#[repr(transparent)]
67
+
#[serde(transparent)]
68
+
pub struct $lim_nz<const MAX: $primitive>($nz);
69
+
70
+
impl<const MAX: $primitive> $lim_nz<MAX> {
71
+
/// The smallest value that can be represented by this limited non-zero
72
+
/// integer type.
73
+
pub const MIN: Self = Self($nz::MIN);
74
+
75
+
/// The largest value that can be represented by this limited non-zero integer
76
+
/// type.
77
+
pub const MAX: Self = Self(unsafe { $nz::new_unchecked(MAX) });
78
+
79
+
fn new(value: $primitive) -> Result<Self, String> {
80
+
if value > MAX {
81
+
Err(format!("value is greater than {}", MAX))
82
+
} else if let Some(value) = $nz::new(value) {
83
+
Ok(Self(value))
84
+
} else {
85
+
Err("value is zero".into())
86
+
}
87
+
}
88
+
}
89
+
90
+
impl<const MAX: $primitive> FromStr for $lim_nz<MAX> {
91
+
type Err = String;
92
+
93
+
fn from_str(src: &str) -> Result<Self, Self::Err> {
94
+
Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?)
95
+
}
96
+
}
97
+
98
+
impl<const MAX: $primitive> TryFrom<$primitive> for $lim_nz<MAX> {
99
+
type Error = String;
100
+
101
+
fn try_from(value: $primitive) -> Result<Self, Self::Error> {
102
+
Self::new(value)
103
+
}
104
+
}
105
+
106
+
impl<'de, const MAX: $primitive> Deserialize<'de> for $lim_nz<MAX> {
107
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
108
+
where
109
+
D: serde::Deserializer<'de>,
110
+
{
111
+
Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom)
112
+
}
113
+
}
114
+
115
+
impl<const MAX: $primitive> From<$lim_nz<MAX>> for $nz {
116
+
fn from(value: $lim_nz<MAX>) -> Self {
117
+
value.0
118
+
}
119
+
}
120
+
121
+
impl<const MAX: $primitive> From<$lim_nz<MAX>> for $primitive {
122
+
fn from(value: $lim_nz<MAX>) -> Self {
123
+
value.0.into()
124
+
}
125
+
}
126
+
127
+
/// An unsigned integer with a minimum value of `MIN` and a maximum value of `MAX`.
128
+
///
129
+
/// `MIN` must be non-zero.
130
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, Hash)]
131
+
#[repr(transparent)]
132
+
#[serde(transparent)]
133
+
pub struct $bounded<const MIN: $primitive, const MAX: $primitive>($nz);
134
+
135
+
impl<const MIN: $primitive, const MAX: $primitive> $bounded<MIN, MAX> {
136
+
/// The smallest value that can be represented by this bounded integer type.
137
+
pub const MIN: Self = Self(unsafe { $nz::new_unchecked(MIN) });
138
+
139
+
/// The largest value that can be represented by this bounded integer type.
140
+
pub const MAX: Self = Self(unsafe { $nz::new_unchecked(MAX) });
141
+
142
+
fn new(value: $primitive) -> Result<Self, String> {
143
+
if value < MIN {
144
+
Err(format!("value is less than {}", MIN))
145
+
} else if value > MAX {
146
+
Err(format!("value is greater than {}", MAX))
147
+
} else if let Some(value) = $nz::new(value) {
148
+
Ok(Self(value))
149
+
} else {
150
+
Err("value is zero".into())
151
+
}
152
+
}
153
+
}
154
+
155
+
impl<const MIN: $primitive, const MAX: $primitive> TryFrom<$primitive>
156
+
for $bounded<MIN, MAX>
157
+
{
158
+
type Error = String;
159
+
160
+
fn try_from(value: $primitive) -> Result<Self, Self::Error> {
161
+
Self::new(value)
162
+
}
163
+
}
164
+
165
+
impl<const MIN: $primitive, const MAX: $primitive> FromStr for $bounded<MIN, MAX> {
166
+
type Err = String;
167
+
168
+
fn from_str(src: &str) -> Result<Self, Self::Err> {
169
+
Self::new(src.parse::<$primitive>().map_err(|e| e.to_string())?)
170
+
}
171
+
}
172
+
173
+
impl<'de, const MIN: $primitive, const MAX: $primitive> Deserialize<'de>
174
+
for $bounded<MIN, MAX>
175
+
{
176
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177
+
where
178
+
D: serde::Deserializer<'de>,
179
+
{
180
+
Self::new(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom)
181
+
}
182
+
}
183
+
184
+
impl<const MIN: $primitive, const MAX: $primitive> From<$bounded<MIN, MAX>> for $nz {
185
+
fn from(value: $bounded<MIN, MAX>) -> Self {
186
+
value.0
187
+
}
188
+
}
189
+
190
+
impl<const MIN: $primitive, const MAX: $primitive> From<$bounded<MIN, MAX>> for $primitive {
191
+
fn from(value: $bounded<MIN, MAX>) -> Self {
192
+
value.0.into()
193
+
}
194
+
}
195
+
};
196
+
}
197
+
198
+
uint!(u8, NonZeroU8, LimitedU8, LimitedNonZeroU8, BoundedU8);
199
+
uint!(u16, NonZeroU16, LimitedU16, LimitedNonZeroU16, BoundedU16);
200
+
uint!(u32, NonZeroU32, LimitedU32, LimitedNonZeroU32, BoundedU32);
201
+
uint!(u64, NonZeroU64, LimitedU64, LimitedNonZeroU64, BoundedU64);
202
+
203
+
#[cfg(test)]
204
+
mod tests {
205
+
use super::*;
206
+
207
+
#[test]
208
+
fn u8_min_max() {
209
+
assert_eq!(Ok(LimitedU8::<10>::MIN), 0.try_into());
210
+
assert_eq!(Ok(LimitedU8::<10>::MAX), 10.try_into());
211
+
assert_eq!(Ok(LimitedNonZeroU8::<10>::MIN), 1.try_into());
212
+
assert_eq!(Ok(LimitedNonZeroU8::<10>::MAX), 10.try_into());
213
+
assert_eq!(Ok(BoundedU8::<7, 10>::MIN), 7.try_into());
214
+
assert_eq!(Ok(BoundedU8::<7, 10>::MAX), 10.try_into());
215
+
}
216
+
217
+
#[test]
218
+
fn u8_from_str() {
219
+
{
220
+
type LU8 = LimitedU8<10>;
221
+
assert_eq!(Ok(LU8::MIN), "0".parse());
222
+
assert_eq!(Ok(LU8::MAX), "10".parse());
223
+
assert_eq!(Err("value is greater than 10".into()), "11".parse::<LU8>());
224
+
}
225
+
{
226
+
type LU8 = LimitedNonZeroU8<10>;
227
+
assert_eq!(Ok(LU8::MIN), "1".parse());
228
+
assert_eq!(Ok(LU8::MAX), "10".parse());
229
+
assert_eq!(Err("value is greater than 10".into()), "11".parse::<LU8>());
230
+
}
231
+
{
232
+
type BU8 = BoundedU8<7, 10>;
233
+
assert_eq!(Err("value is less than 7".into()), "6".parse::<BU8>());
234
+
assert_eq!(Ok(BU8::MIN), "7".parse());
235
+
assert_eq!(Ok(BU8::MAX), "10".parse());
236
+
assert_eq!(Err("value is greater than 10".into()), "11".parse::<BU8>());
237
+
}
238
+
}
239
+
240
+
#[test]
241
+
fn deserialize_u8_from_str() {
242
+
{
243
+
#[derive(Deserialize, Debug)]
244
+
struct Foo {
245
+
bar: LimitedU8<10>,
246
+
}
247
+
248
+
match serde_json::from_str::<Foo>(r#"{"bar": 0}"#) {
249
+
Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MIN),
250
+
Err(e) => panic!("failed to deserialize: {e}"),
251
+
}
252
+
match serde_json::from_str::<Foo>(r#"{"bar": "0"}"#) {
253
+
Ok(_) => panic!("deserialization should fail"),
254
+
Err(e) => assert!(e.to_string().contains("invalid type: string")),
255
+
}
256
+
match serde_html_form::from_str::<Foo>(r#"bar=0"#) {
257
+
Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MIN),
258
+
Err(e) => panic!("failed to deserialize: {e}"),
259
+
}
260
+
match serde_html_form::from_str::<Foo>(r#"bar=10"#) {
261
+
Ok(foo) => assert_eq!(foo.bar, LimitedU8::<10>::MAX),
262
+
Err(e) => panic!("failed to deserialize: {e}"),
263
+
}
264
+
match serde_html_form::from_str::<Foo>(r#"bar=11"#) {
265
+
Ok(_) => panic!("deserialization should fail"),
266
+
Err(e) => assert_eq!(e.to_string(), "value is greater than 10"),
267
+
}
268
+
}
269
+
270
+
{
271
+
#[derive(Deserialize, Debug)]
272
+
struct Foo {
273
+
bar: LimitedNonZeroU8<10>,
274
+
}
275
+
276
+
match serde_json::from_str::<Foo>(r#"{"bar": 0}"#) {
277
+
Ok(_) => panic!("deserialization should fail"),
278
+
Err(e) => assert_eq!(e.to_string(), "value is zero at line 1 column 10"),
279
+
}
280
+
match serde_json::from_str::<Foo>(r#"{"bar": "0"}"#) {
281
+
Ok(_) => panic!("deserialization should fail"),
282
+
Err(e) => assert!(e.to_string().contains("invalid type: string")),
283
+
}
284
+
match serde_html_form::from_str::<Foo>(r#"bar=0"#) {
285
+
Ok(_) => panic!("deserialization should fail"),
286
+
Err(e) => assert_eq!(e.to_string(), "value is zero"),
287
+
}
288
+
match serde_html_form::from_str::<Foo>(r#"bar=10"#) {
289
+
Ok(foo) => assert_eq!(foo.bar, LimitedNonZeroU8::<10>::MAX),
290
+
Err(e) => panic!("failed to deserialize: {e}"),
291
+
}
292
+
match serde_html_form::from_str::<Foo>(r#"bar=11"#) {
293
+
Ok(_) => panic!("deserialization should fail"),
294
+
Err(e) => assert_eq!(e.to_string(), "value is greater than 10"),
295
+
}
296
+
}
297
+
298
+
{
299
+
#[derive(Deserialize, Debug)]
300
+
struct Foo {
301
+
bar: BoundedU8<1, 10>,
302
+
}
303
+
304
+
match serde_json::from_str::<Foo>(r#"{"bar": 0}"#) {
305
+
Ok(_) => panic!("deserialization should fail"),
306
+
Err(e) => assert_eq!(e.to_string(), "value is less than 1 at line 1 column 10"),
307
+
}
308
+
match serde_json::from_str::<Foo>(r#"{"bar": "0"}"#) {
309
+
Ok(_) => panic!("deserialization should fail"),
310
+
Err(e) => assert!(e.to_string().contains("invalid type: string")),
311
+
}
312
+
match serde_html_form::from_str::<Foo>(r#"bar=0"#) {
313
+
Ok(_) => panic!("deserialization should fail"),
314
+
Err(e) => assert_eq!(e.to_string(), "value is less than 1"),
315
+
}
316
+
match serde_html_form::from_str::<Foo>(r#"bar=10"#) {
317
+
Ok(foo) => assert_eq!(foo.bar, BoundedU8::<1, 10>::MAX),
318
+
Err(e) => panic!("failed to deserialize: {e}"),
319
+
}
320
+
match serde_html_form::from_str::<Foo>(r#"bar=11"#) {
321
+
Ok(_) => panic!("deserialization should fail"),
322
+
Err(e) => assert_eq!(e.to_string(), "value is greater than 10"),
323
+
}
324
+
}
325
+
}
326
+
}
+202
crates/jacquard-common/src/types/tid.rs
+202
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);
19
+
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(|| {
28
+
Regex::new(r"^[234567abcdefghij][234567abcdefghijklmnopqrstuvwxyz]{12}$").unwrap()
29
+
});
30
+
31
+
/// A [Timestamp Identifier].
32
+
///
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
+
61
+
/// Infallible constructor for when you *know* the string is a valid TID.
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.
82
+
///
83
+
/// If you have multiple clock sources, you can use `clkid` to distinguish between them
84
+
/// and hint to other implementations that the timestamp cannot be compared with other
85
+
/// timestamps from other sources.
86
+
/// If you are only using a single clock source, you can just specify `0` for `clkid`.
87
+
pub fn from_datetime(clkid: LimitedU32<1023>, time: chrono::DateTime<chrono::Utc>) -> Self {
88
+
let time = time.timestamp_micros() as u64;
89
+
90
+
// The TID is laid out as follows:
91
+
// 0TTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTTTTTTTTTTT TTTTTTCCCCCCCCCC
92
+
let tid = (time << 10) & 0x7FFF_FFFF_FFFF_FC00 | (Into::<u32>::into(clkid) as u64 & 0x3FF);
93
+
Self(s32_encode(tid))
94
+
}
95
+
96
+
/// Construct a new [Tid] that represents the current time.
97
+
///
98
+
/// If you have multiple clock sources, you can use `clkid` to distinguish between them
99
+
/// and hint to other implementations that the timestamp cannot be compared with other
100
+
/// timestamps from other sources.
101
+
/// If you are only using a single clock source, you can just specify `0` for `clkid`.
102
+
///
103
+
/// _Warning:_ It's possible that this function will return the same time more than once.
104
+
/// If it's important that these values be unique, you will want to repeatedly call this
105
+
/// function until a different time is returned.
106
+
pub fn now(clkid: LimitedU32<1023>) -> Self {
107
+
Self::from_datetime(clkid, chrono::Utc::now())
108
+
}
109
+
110
+
/// Construct a new [Tid] that represents the current time with clkid 0.
111
+
///
112
+
/// _Warning:_ It's possible that this function will return the same time more than once.
113
+
/// If it's important that these values be unique, you will want to repeatedly call this
114
+
/// function until a different time is returned.
115
+
pub fn now_0() -> Self {
116
+
Self::from_datetime(LimitedU32::from_str("0").unwrap(), chrono::Utc::now())
117
+
}
118
+
119
+
/// Returns the TID as a string slice.
120
+
pub fn as_str(&self) -> &str {
121
+
{
122
+
let this = &self.0;
123
+
this
124
+
}
125
+
}
126
+
}
127
+
128
+
impl FromStr for Tid<'_> {
129
+
type Err = &'static str;
130
+
131
+
/// Has to take ownership due to the lifetime constraints of the FromStr trait.
132
+
/// Prefer `Did::new()` or `Did::raw` if you want to borrow.
133
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
134
+
Self::from_cowstr(CowStr::Borrowed(s).into_static())
135
+
}
136
+
}
137
+
138
+
impl<'de> Deserialize<'de> for Tid<'de> {
139
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140
+
where
141
+
D: Deserializer<'de>,
142
+
{
143
+
let value = Deserialize::deserialize(deserializer)?;
144
+
Self::new(value).map_err(D::Error::custom)
145
+
}
146
+
}
147
+
148
+
impl fmt::Display for Tid<'_> {
149
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150
+
f.write_str(&self.0)
151
+
}
152
+
}
153
+
154
+
impl<'t> From<Tid<'t>> for String {
155
+
fn from(value: Tid<'t>) -> Self {
156
+
value.0.to_string()
157
+
}
158
+
}
159
+
160
+
impl<'t> From<Tid<'t>> for CowStr<'t> {
161
+
fn from(value: Tid<'t>) -> Self {
162
+
value.0
163
+
}
164
+
}
165
+
166
+
impl From<String> for Tid<'static> {
167
+
fn from(value: String) -> Self {
168
+
if value.len() != 13 {
169
+
panic!("TID must be 13 characters")
170
+
} else if !TID_REGEX.is_match(&value) {
171
+
panic!("Invalid TID")
172
+
} else {
173
+
Self(CowStr::Owned(value.to_compact_string()))
174
+
}
175
+
}
176
+
}
177
+
178
+
impl<'t> From<CowStr<'t>> for Tid<'t> {
179
+
fn from(value: CowStr<'t>) -> Self {
180
+
if value.len() != 13 {
181
+
panic!("TID must be 13 characters")
182
+
} else if !TID_REGEX.is_match(&value) {
183
+
panic!("Invalid TID")
184
+
} else {
185
+
Self(value)
186
+
}
187
+
}
188
+
}
189
+
190
+
impl AsRef<str> for Tid<'_> {
191
+
fn as_ref(&self) -> &str {
192
+
self.as_str()
193
+
}
194
+
}
195
+
196
+
impl Deref for Tid<'_> {
197
+
type Target = str;
198
+
199
+
fn deref(&self) -> &Self::Target {
200
+
self.as_str()
201
+
}
202
+
}