1#[cfg(feature = "sqlx")]
9mod sqlx_support {
10 use core::convert::TryFrom;
11 use std::io;
12
13 use sqlx::decode::Decode;
14 use sqlx::encode::{Encode, IsNull};
15 use sqlx::error::BoxDynError;
16 use sqlx::{Database, Type};
17
18 use crate::{CompositeKey, Id, IdDomain, IdParseError, Key, KeyDomain};
19 #[cfg(feature = "ulid")]
20 use crate::{Ulid, UlidDomain};
21 #[cfg(feature = "uuid")]
22 use crate::{Uuid, UuidDomain};
23
24 impl<DB, D> Type<DB> for Key<D>
25 where
26 DB: Database,
27 D: KeyDomain,
28 String: Type<DB>,
29 {
30 fn type_info() -> DB::TypeInfo {
31 <String as Type<DB>>::type_info()
32 }
33
34 fn compatible(ty: &DB::TypeInfo) -> bool {
35 <String as Type<DB>>::compatible(ty)
36 }
37 }
38
39 impl<'q, DB, D> Encode<'q, DB> for Key<D>
40 where
41 DB: Database,
42 D: KeyDomain,
43 String: Encode<'q, DB>,
44 {
45 fn encode_by_ref(
46 &self,
47 buf: &mut <DB as Database>::ArgumentBuffer<'q>,
48 ) -> Result<IsNull, BoxDynError> {
49 <String as Encode<'q, DB>>::encode(self.as_str().to_owned(), buf)
50 }
51
52 fn size_hint(&self) -> usize {
53 self.len()
54 }
55 }
56
57 impl<'r, DB, D> Decode<'r, DB> for Key<D>
58 where
59 DB: Database,
60 D: KeyDomain,
61 String: Decode<'r, DB>,
62 {
63 fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
64 let decoded = <String as Decode<'r, DB>>::decode(value)?;
65 Key::from_string(decoded).map_err(|error| Box::new(error) as BoxDynError)
66 }
67 }
68
69 impl<DB, D> Type<DB> for Id<D>
70 where
71 DB: Database,
72 D: IdDomain,
73 i64: Type<DB>,
74 {
75 fn type_info() -> DB::TypeInfo {
76 <i64 as Type<DB>>::type_info()
77 }
78
79 fn compatible(ty: &DB::TypeInfo) -> bool {
80 <i64 as Type<DB>>::compatible(ty)
81 }
82 }
83
84 impl<'q, DB, D> Encode<'q, DB> for Id<D>
85 where
86 DB: Database,
87 D: IdDomain,
88 i64: Encode<'q, DB>,
89 {
90 fn encode_by_ref(
91 &self,
92 buf: &mut <DB as Database>::ArgumentBuffer<'q>,
93 ) -> Result<IsNull, BoxDynError> {
94 let value = i64::try_from(self.get()).map_err(|_| {
95 Box::new(io::Error::new(
96 io::ErrorKind::InvalidData,
97 "Id value does not fit into signed BIGINT",
98 )) as BoxDynError
99 })?;
100
101 value.encode_by_ref(buf)
102 }
103
104 fn size_hint(&self) -> usize {
105 core::mem::size_of::<i64>()
106 }
107 }
108
109 impl<'r, DB, D> Decode<'r, DB> for Id<D>
110 where
111 DB: Database,
112 D: IdDomain,
113 i64: Decode<'r, DB>,
114 {
115 fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
116 let decoded = <i64 as Decode<'r, DB>>::decode(value)?;
117
118 if decoded == 0 {
119 return Err(Box::new(IdParseError::Zero));
120 }
121
122 if decoded < 0 {
123 return Err(Box::new(io::Error::new(
124 io::ErrorKind::InvalidData,
125 "Negative database ID cannot be converted to domain-key::Id",
126 )));
127 }
128
129 let as_u64 = u64::try_from(decoded).map_err(|_| {
130 Box::new(io::Error::new(
131 io::ErrorKind::InvalidData,
132 "Negative database ID cannot be converted to domain-key::Id",
133 )) as BoxDynError
134 })?;
135
136 Id::new(as_u64).ok_or_else(|| Box::new(IdParseError::Zero) as BoxDynError)
137 }
138 }
139
140 #[cfg(all(feature = "uuid", feature = "sqlx-postgres"))]
144 impl<D> Type<sqlx::Postgres> for Uuid<D>
145 where
146 D: UuidDomain,
147 ::uuid::Uuid: Type<sqlx::Postgres>,
148 {
149 fn type_info() -> <sqlx::Postgres as Database>::TypeInfo {
150 <::uuid::Uuid as Type<sqlx::Postgres>>::type_info()
151 }
152
153 fn compatible(ty: &<sqlx::Postgres as Database>::TypeInfo) -> bool {
154 <::uuid::Uuid as Type<sqlx::Postgres>>::compatible(ty)
155 }
156 }
157
158 #[cfg(all(feature = "uuid", feature = "sqlx-postgres"))]
159 impl<'q, D> Encode<'q, sqlx::Postgres> for Uuid<D>
160 where
161 D: UuidDomain,
162 ::uuid::Uuid: Encode<'q, sqlx::Postgres>,
163 {
164 fn encode_by_ref(
165 &self,
166 buf: &mut <sqlx::Postgres as Database>::ArgumentBuffer<'q>,
167 ) -> Result<IsNull, BoxDynError> {
168 self.get().encode_by_ref(buf)
169 }
170
171 fn size_hint(&self) -> usize {
172 core::mem::size_of::<u128>()
173 }
174 }
175
176 #[cfg(all(feature = "uuid", feature = "sqlx-postgres"))]
177 impl<'r, D> Decode<'r, sqlx::Postgres> for Uuid<D>
178 where
179 D: UuidDomain,
180 ::uuid::Uuid: Decode<'r, sqlx::Postgres>,
181 {
182 fn decode(value: <sqlx::Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
183 let decoded = <::uuid::Uuid as Decode<'r, sqlx::Postgres>>::decode(value)?;
184 Ok(Uuid::from(decoded))
185 }
186 }
187
188 #[cfg(all(feature = "uuid", feature = "sqlx-sqlite"))]
189 impl<D> Type<sqlx::Sqlite> for Uuid<D>
190 where
191 D: UuidDomain,
192 String: Type<sqlx::Sqlite>,
193 {
194 fn type_info() -> <sqlx::Sqlite as Database>::TypeInfo {
195 <String as Type<sqlx::Sqlite>>::type_info()
196 }
197
198 fn compatible(ty: &<sqlx::Sqlite as Database>::TypeInfo) -> bool {
199 <String as Type<sqlx::Sqlite>>::compatible(ty)
200 }
201 }
202
203 #[cfg(all(feature = "uuid", feature = "sqlx-sqlite"))]
204 impl<'q, D> Encode<'q, sqlx::Sqlite> for Uuid<D>
205 where
206 D: UuidDomain,
207 String: Encode<'q, sqlx::Sqlite>,
208 {
209 fn encode_by_ref(
210 &self,
211 buf: &mut <sqlx::Sqlite as Database>::ArgumentBuffer<'q>,
212 ) -> Result<IsNull, BoxDynError> {
213 <String as Encode<'q, sqlx::Sqlite>>::encode(self.to_string(), buf)
214 }
215
216 fn size_hint(&self) -> usize {
217 36
218 }
219 }
220
221 #[cfg(all(feature = "uuid", feature = "sqlx-sqlite"))]
222 impl<'r, D> Decode<'r, sqlx::Sqlite> for Uuid<D>
223 where
224 D: UuidDomain,
225 String: Decode<'r, sqlx::Sqlite>,
226 {
227 fn decode(value: <sqlx::Sqlite as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
228 let decoded = <String as Decode<'r, sqlx::Sqlite>>::decode(value)?;
229 Uuid::parse(&decoded).map_err(|error| Box::new(error) as BoxDynError)
230 }
231 }
232
233 #[cfg(all(feature = "uuid", feature = "sqlx-mysql"))]
234 impl<D> Type<sqlx::MySql> for Uuid<D>
235 where
236 D: UuidDomain,
237 String: Type<sqlx::MySql>,
238 {
239 fn type_info() -> <sqlx::MySql as Database>::TypeInfo {
240 <String as Type<sqlx::MySql>>::type_info()
241 }
242
243 fn compatible(ty: &<sqlx::MySql as Database>::TypeInfo) -> bool {
244 <String as Type<sqlx::MySql>>::compatible(ty)
245 }
246 }
247
248 #[cfg(all(feature = "uuid", feature = "sqlx-mysql"))]
249 impl<'q, D> Encode<'q, sqlx::MySql> for Uuid<D>
250 where
251 D: UuidDomain,
252 String: Encode<'q, sqlx::MySql>,
253 {
254 fn encode_by_ref(
255 &self,
256 buf: &mut <sqlx::MySql as Database>::ArgumentBuffer<'q>,
257 ) -> Result<IsNull, BoxDynError> {
258 <String as Encode<'q, sqlx::MySql>>::encode(self.to_string(), buf)
259 }
260
261 fn size_hint(&self) -> usize {
262 36
263 }
264 }
265
266 #[cfg(all(feature = "uuid", feature = "sqlx-mysql"))]
267 impl<'r, D> Decode<'r, sqlx::MySql> for Uuid<D>
268 where
269 D: UuidDomain,
270 String: Decode<'r, sqlx::MySql>,
271 {
272 fn decode(value: <sqlx::MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
273 let decoded = <String as Decode<'r, sqlx::MySql>>::decode(value)?;
274 Uuid::parse(&decoded).map_err(|error| Box::new(error) as BoxDynError)
275 }
276 }
277
278 #[cfg(all(feature = "ulid", feature = "sqlx-postgres"))]
282 impl<D> Type<sqlx::Postgres> for Ulid<D>
283 where
284 D: UlidDomain,
285 ::uuid::Uuid: Type<sqlx::Postgres>,
286 {
287 fn type_info() -> <sqlx::Postgres as Database>::TypeInfo {
288 <::uuid::Uuid as Type<sqlx::Postgres>>::type_info()
289 }
290
291 fn compatible(ty: &<sqlx::Postgres as Database>::TypeInfo) -> bool {
292 <::uuid::Uuid as Type<sqlx::Postgres>>::compatible(ty)
293 }
294 }
295
296 #[cfg(all(feature = "ulid", feature = "sqlx-postgres"))]
297 impl<'q, D> Encode<'q, sqlx::Postgres> for Ulid<D>
298 where
299 D: UlidDomain,
300 ::uuid::Uuid: Encode<'q, sqlx::Postgres>,
301 {
302 fn encode_by_ref(
303 &self,
304 buf: &mut <sqlx::Postgres as Database>::ArgumentBuffer<'q>,
305 ) -> Result<IsNull, BoxDynError> {
306 let uuid = ::uuid::Uuid::from_bytes(self.to_bytes());
307 uuid.encode_by_ref(buf)
308 }
309
310 fn size_hint(&self) -> usize {
311 core::mem::size_of::<u128>()
312 }
313 }
314
315 #[cfg(all(feature = "ulid", feature = "sqlx-postgres"))]
316 impl<'r, D> Decode<'r, sqlx::Postgres> for Ulid<D>
317 where
318 D: UlidDomain,
319 ::uuid::Uuid: Decode<'r, sqlx::Postgres>,
320 {
321 fn decode(value: <sqlx::Postgres as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
322 let decoded = <::uuid::Uuid as Decode<'r, sqlx::Postgres>>::decode(value)?;
323 Ok(Ulid::from_bytes(*decoded.as_bytes()))
324 }
325 }
326
327 #[cfg(all(feature = "ulid", feature = "sqlx-sqlite"))]
328 impl<D> Type<sqlx::Sqlite> for Ulid<D>
329 where
330 D: UlidDomain,
331 String: Type<sqlx::Sqlite>,
332 {
333 fn type_info() -> <sqlx::Sqlite as Database>::TypeInfo {
334 <String as Type<sqlx::Sqlite>>::type_info()
335 }
336
337 fn compatible(ty: &<sqlx::Sqlite as Database>::TypeInfo) -> bool {
338 <String as Type<sqlx::Sqlite>>::compatible(ty)
339 }
340 }
341
342 #[cfg(all(feature = "ulid", feature = "sqlx-sqlite"))]
343 impl<'q, D> Encode<'q, sqlx::Sqlite> for Ulid<D>
344 where
345 D: UlidDomain,
346 String: Encode<'q, sqlx::Sqlite>,
347 {
348 fn encode_by_ref(
349 &self,
350 buf: &mut <sqlx::Sqlite as Database>::ArgumentBuffer<'q>,
351 ) -> Result<IsNull, BoxDynError> {
352 <String as Encode<'q, sqlx::Sqlite>>::encode(self.to_string(), buf)
353 }
354
355 fn size_hint(&self) -> usize {
356 D::PREFIX.len() + 1 + 26
357 }
358 }
359
360 #[cfg(all(feature = "ulid", feature = "sqlx-sqlite"))]
361 impl<'r, D> Decode<'r, sqlx::Sqlite> for Ulid<D>
362 where
363 D: UlidDomain,
364 String: Decode<'r, sqlx::Sqlite>,
365 {
366 fn decode(value: <sqlx::Sqlite as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
367 let decoded = <String as Decode<'r, sqlx::Sqlite>>::decode(value)?;
368 Ulid::parse(&decoded).map_err(|error| Box::new(error) as BoxDynError)
369 }
370 }
371
372 #[cfg(all(feature = "ulid", feature = "sqlx-mysql"))]
373 impl<D> Type<sqlx::MySql> for Ulid<D>
374 where
375 D: UlidDomain,
376 String: Type<sqlx::MySql>,
377 {
378 fn type_info() -> <sqlx::MySql as Database>::TypeInfo {
379 <String as Type<sqlx::MySql>>::type_info()
380 }
381
382 fn compatible(ty: &<sqlx::MySql as Database>::TypeInfo) -> bool {
383 <String as Type<sqlx::MySql>>::compatible(ty)
384 }
385 }
386
387 #[cfg(all(feature = "ulid", feature = "sqlx-mysql"))]
388 impl<'q, D> Encode<'q, sqlx::MySql> for Ulid<D>
389 where
390 D: UlidDomain,
391 String: Encode<'q, sqlx::MySql>,
392 {
393 fn encode_by_ref(
394 &self,
395 buf: &mut <sqlx::MySql as Database>::ArgumentBuffer<'q>,
396 ) -> Result<IsNull, BoxDynError> {
397 <String as Encode<'q, sqlx::MySql>>::encode(self.to_string(), buf)
398 }
399
400 fn size_hint(&self) -> usize {
401 D::PREFIX.len() + 1 + 26
402 }
403 }
404
405 #[cfg(all(feature = "ulid", feature = "sqlx-mysql"))]
406 impl<'r, D> Decode<'r, sqlx::MySql> for Ulid<D>
407 where
408 D: UlidDomain,
409 String: Decode<'r, sqlx::MySql>,
410 {
411 fn decode(value: <sqlx::MySql as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
412 let decoded = <String as Decode<'r, sqlx::MySql>>::decode(value)?;
413 Ulid::parse(&decoded).map_err(|error| Box::new(error) as BoxDynError)
414 }
415 }
416
417 impl<DB, A, B, const SEP: char> Type<DB> for CompositeKey<A, B, SEP>
418 where
419 DB: Database,
420 A: KeyDomain,
421 B: KeyDomain,
422 String: Type<DB>,
423 {
424 fn type_info() -> DB::TypeInfo {
425 <String as Type<DB>>::type_info()
426 }
427
428 fn compatible(ty: &DB::TypeInfo) -> bool {
429 <String as Type<DB>>::compatible(ty)
430 }
431 }
432
433 impl<'q, DB, A, B, const SEP: char> Encode<'q, DB> for CompositeKey<A, B, SEP>
434 where
435 DB: Database,
436 A: KeyDomain,
437 B: KeyDomain,
438 String: Encode<'q, DB>,
439 {
440 fn encode_by_ref(
441 &self,
442 buf: &mut <DB as Database>::ArgumentBuffer<'q>,
443 ) -> Result<IsNull, BoxDynError> {
444 <String as Encode<'q, DB>>::encode(self.to_string(), buf)
445 }
446
447 fn size_hint(&self) -> usize {
448 self.first().as_str().len() + SEP.len_utf8() + self.second().as_str().len()
449 }
450 }
451
452 impl<'r, DB, A, B, const SEP: char> Decode<'r, DB> for CompositeKey<A, B, SEP>
453 where
454 DB: Database,
455 A: KeyDomain,
456 B: KeyDomain,
457 String: Decode<'r, DB>,
458 {
459 fn decode(value: <DB as Database>::ValueRef<'r>) -> Result<Self, BoxDynError> {
460 let decoded = <String as Decode<'r, DB>>::decode(value)?;
461 decoded
462 .parse::<CompositeKey<A, B, SEP>>()
463 .map_err(|error| Box::new(error) as BoxDynError)
464 }
465 }
466}
467
468#[cfg(feature = "axum")]
469mod axum_support {
470 use axum::http::StatusCode;
471 use axum::response::{IntoResponse, Response};
472
473 #[cfg(feature = "ulid")]
474 use crate::UlidParseError;
475 #[cfg(feature = "uuid")]
476 use crate::UuidParseError;
477 use crate::{CompositeKeyParseError, IdParseError, KeyParseError};
478
479 impl IntoResponse for KeyParseError {
480 fn into_response(self) -> Response {
481 (StatusCode::BAD_REQUEST, self.to_string()).into_response()
482 }
483 }
484
485 impl IntoResponse for IdParseError {
486 fn into_response(self) -> Response {
487 (StatusCode::BAD_REQUEST, self.to_string()).into_response()
488 }
489 }
490
491 #[cfg(feature = "uuid")]
492 impl IntoResponse for UuidParseError {
493 fn into_response(self) -> Response {
494 (StatusCode::BAD_REQUEST, self.to_string()).into_response()
495 }
496 }
497
498 #[cfg(feature = "ulid")]
499 impl IntoResponse for UlidParseError {
500 fn into_response(self) -> Response {
501 (StatusCode::BAD_REQUEST, self.to_string()).into_response()
502 }
503 }
504
505 impl IntoResponse for CompositeKeyParseError {
506 fn into_response(self) -> Response {
507 (StatusCode::UNPROCESSABLE_ENTITY, self.to_string()).into_response()
508 }
509 }
510}
511
512#[cfg(feature = "actix-web")]
513mod actix_web_support {
514 use actix_web::http::StatusCode;
515 use actix_web::{HttpResponse, ResponseError};
516
517 #[cfg(feature = "ulid")]
518 use crate::UlidParseError;
519 #[cfg(feature = "uuid")]
520 use crate::UuidParseError;
521 use crate::{CompositeKeyParseError, IdParseError, KeyParseError};
522
523 impl ResponseError for KeyParseError {
524 fn status_code(&self) -> StatusCode {
525 StatusCode::BAD_REQUEST
526 }
527
528 fn error_response(&self) -> HttpResponse {
529 HttpResponse::build(self.status_code()).body(self.to_string())
530 }
531 }
532
533 impl ResponseError for IdParseError {
534 fn status_code(&self) -> StatusCode {
535 StatusCode::BAD_REQUEST
536 }
537
538 fn error_response(&self) -> HttpResponse {
539 HttpResponse::build(self.status_code()).body(self.to_string())
540 }
541 }
542
543 #[cfg(feature = "uuid")]
544 impl ResponseError for UuidParseError {
545 fn status_code(&self) -> StatusCode {
546 StatusCode::BAD_REQUEST
547 }
548
549 fn error_response(&self) -> HttpResponse {
550 HttpResponse::build(self.status_code()).body(self.to_string())
551 }
552 }
553
554 #[cfg(feature = "ulid")]
555 impl ResponseError for UlidParseError {
556 fn status_code(&self) -> StatusCode {
557 StatusCode::BAD_REQUEST
558 }
559
560 fn error_response(&self) -> HttpResponse {
561 HttpResponse::build(self.status_code()).body(self.to_string())
562 }
563 }
564
565 impl ResponseError for CompositeKeyParseError {
566 fn status_code(&self) -> StatusCode {
567 StatusCode::UNPROCESSABLE_ENTITY
568 }
569
570 fn error_response(&self) -> HttpResponse {
571 HttpResponse::build(self.status_code()).body(self.to_string())
572 }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use crate::{Domain, IdDomain, Key, KeyDomain};
579
580 #[derive(Debug)]
581 #[allow(dead_code)]
582 struct TestKeyDomain;
583 impl Domain for TestKeyDomain {
584 const DOMAIN_NAME: &'static str = "test_key";
585 }
586 impl KeyDomain for TestKeyDomain {}
587 #[allow(dead_code)]
588 type TestKey = Key<TestKeyDomain>;
589
590 #[derive(Debug)]
591 #[allow(dead_code)]
592 struct TestIdDomain;
593 impl Domain for TestIdDomain {
594 const DOMAIN_NAME: &'static str = "test_id";
595 }
596 impl IdDomain for TestIdDomain {}
597
598 #[cfg(feature = "uuid")]
599 #[derive(Debug)]
600 struct TestUuidDomain;
601 #[cfg(feature = "uuid")]
602 impl Domain for TestUuidDomain {
603 const DOMAIN_NAME: &'static str = "test_uuid";
604 }
605 #[cfg(feature = "uuid")]
606 impl crate::UuidDomain for TestUuidDomain {}
607
608 #[cfg(feature = "ulid")]
609 #[derive(Debug)]
610 struct TestUlidDomain;
611 #[cfg(feature = "ulid")]
612 impl Domain for TestUlidDomain {
613 const DOMAIN_NAME: &'static str = "test_ulid";
614 }
615 #[cfg(feature = "ulid")]
616 impl crate::UlidDomain for TestUlidDomain {
617 const PREFIX: &'static str = "tst";
618 }
619
620 #[cfg(feature = "sqlx-postgres")]
621 mod sqlx_postgres_traits {
622 use super::*;
623 use sqlx::decode::Decode;
624 use sqlx::encode::Encode;
625 use sqlx::{Postgres, Type};
626
627 fn assert_sqlx_traits<T>()
628 where
629 T: Type<Postgres> + for<'q> Encode<'q, Postgres> + for<'r> Decode<'r, Postgres>,
630 {
631 }
632
633 #[test]
634 fn key_id_and_ulid_implement_sqlx_traits_for_postgres() {
635 assert_sqlx_traits::<TestKey>();
636 assert_sqlx_traits::<crate::Id<TestIdDomain>>();
637 #[cfg(feature = "ulid")]
638 assert_sqlx_traits::<crate::Ulid<TestUlidDomain>>();
639 }
640
641 #[test]
642 fn key_and_id_are_compatible_with_expected_postgres_carriers() {
643 assert!(<TestKey as Type<Postgres>>::compatible(&<String as Type<
644 Postgres,
645 >>::type_info(
646 )));
647 assert!(<crate::Id<TestIdDomain> as Type<Postgres>>::compatible(
648 &<i64 as Type<Postgres>>::type_info()
649 ));
650 }
651
652 #[cfg(feature = "uuid")]
653 #[test]
654 fn uuid_implements_sqlx_traits_for_postgres() {
655 assert_sqlx_traits::<crate::Uuid<TestUuidDomain>>();
656 }
657
658 #[cfg(feature = "uuid")]
659 #[test]
660 fn uuid_is_compatible_with_native_postgres_uuid() {
661 assert!(<crate::Uuid<TestUuidDomain> as Type<Postgres>>::compatible(
662 &<::uuid::Uuid as Type<Postgres>>::type_info()
663 ));
664 }
665
666 #[cfg(feature = "ulid")]
667 #[test]
668 fn ulid_postgres_uses_native_uuid_mapping() {
669 assert!(<crate::Ulid<TestUlidDomain> as Type<Postgres>>::compatible(
670 &<::uuid::Uuid as Type<Postgres>>::type_info()
671 ));
672 }
673
674 #[cfg(feature = "ulid")]
675 #[test]
676 fn ulid_postgres_size_hint_is_binary_uuid_size() {
677 let id = crate::Ulid::<TestUlidDomain>::new();
678 let expected = core::mem::size_of::<u128>();
679 let actual = <crate::Ulid<TestUlidDomain> as Encode<'_, Postgres>>::size_hint(&id);
680 assert_eq!(actual, expected);
681 }
682
683 #[cfg(feature = "ulid")]
684 #[test]
685 fn ulid_to_bytes_matches_inner_and_uuid_conversion() {
686 let id = crate::Ulid::<TestUlidDomain>::new();
687 let outer_bytes = id.to_bytes();
688 let inner_bytes = id.get().to_bytes();
689
690 assert_eq!(outer_bytes, inner_bytes);
691
692 let uuid = ::uuid::Uuid::from_bytes(outer_bytes);
693 assert_eq!(*uuid.as_bytes(), outer_bytes);
694 }
695 }
696
697 #[cfg(feature = "sqlx-sqlite")]
698 mod sqlx_sqlite_traits {
699 use super::*;
700 use sqlx::decode::Decode;
701 use sqlx::encode::Encode;
702 use sqlx::sqlite::SqlitePoolOptions;
703 use sqlx::{Sqlite, Type};
704
705 fn assert_sqlx_traits<T>()
706 where
707 T: Type<Sqlite> + for<'q> Encode<'q, Sqlite> + for<'r> Decode<'r, Sqlite>,
708 {
709 }
710
711 #[test]
712 fn key_id_and_ulid_implement_sqlx_traits_for_sqlite() {
713 assert_sqlx_traits::<TestKey>();
714 assert_sqlx_traits::<crate::Id<TestIdDomain>>();
715 #[cfg(feature = "ulid")]
716 assert_sqlx_traits::<crate::Ulid<TestUlidDomain>>();
717 }
718
719 #[test]
720 fn key_id_and_ulid_sqlite_compatibility_matches_carriers() {
721 assert!(<TestKey as Type<Sqlite>>::compatible(&<String as Type<
722 Sqlite,
723 >>::type_info(
724 )));
725 assert!(<crate::Id<TestIdDomain> as Type<Sqlite>>::compatible(
726 &<i64 as Type<Sqlite>>::type_info()
727 ));
728 #[cfg(feature = "ulid")]
729 assert!(<crate::Ulid<TestUlidDomain> as Type<Sqlite>>::compatible(
730 &<String as Type<Sqlite>>::type_info()
731 ));
732 }
733
734 #[cfg(feature = "uuid")]
735 #[test]
736 fn uuid_implements_sqlx_traits_for_sqlite() {
737 assert_sqlx_traits::<crate::Uuid<TestUuidDomain>>();
738 }
739
740 #[cfg(feature = "uuid")]
741 #[test]
742 fn uuid_sqlite_uses_string_compatibility() {
743 assert!(<crate::Uuid<TestUuidDomain> as Type<Sqlite>>::compatible(
744 &<String as Type<Sqlite>>::type_info()
745 ));
746 let value = crate::Uuid::<TestUuidDomain>::nil();
747 let hint = <crate::Uuid<TestUuidDomain> as Encode<'_, Sqlite>>::size_hint(&value);
748 assert_eq!(hint, 36);
749 }
750
751 #[tokio::test]
752 async fn id_roundtrip_bind_and_query_scalar_sqlite() {
753 let pool = SqlitePoolOptions::new()
754 .max_connections(1)
755 .connect("sqlite::memory:")
756 .await
757 .expect("create in-memory sqlite pool");
758
759 sqlx::query("CREATE TABLE test_ids (id INTEGER NOT NULL PRIMARY KEY)")
760 .execute(&pool)
761 .await
762 .expect("create test_ids table");
763
764 let id = crate::Id::<TestIdDomain>::new(42).expect("non-zero id");
765
766 sqlx::query("INSERT INTO test_ids (id) VALUES (?)")
767 .bind(id)
768 .execute(&pool)
769 .await
770 .expect("insert typed id");
771
772 let row: crate::Id<TestIdDomain> = sqlx::query_scalar("SELECT id FROM test_ids")
773 .fetch_one(&pool)
774 .await
775 .expect("fetch typed id");
776
777 assert_eq!(id, row);
778 }
779
780 #[tokio::test]
781 async fn key_roundtrip_bind_and_query_scalar_sqlite() {
782 let pool = SqlitePoolOptions::new()
783 .max_connections(1)
784 .connect("sqlite::memory:")
785 .await
786 .expect("create in-memory sqlite pool");
787
788 sqlx::query("CREATE TABLE test_keys (id TEXT NOT NULL PRIMARY KEY)")
789 .execute(&pool)
790 .await
791 .expect("create test_keys table");
792
793 let id = TestKey::new("exec_123").expect("valid key");
794
795 sqlx::query("INSERT INTO test_keys (id) VALUES (?)")
796 .bind(id.clone())
797 .execute(&pool)
798 .await
799 .expect("insert typed key");
800
801 let row: TestKey = sqlx::query_scalar("SELECT id FROM test_keys")
802 .fetch_one(&pool)
803 .await
804 .expect("fetch typed key");
805
806 assert_eq!(id, row);
807 }
808
809 #[cfg(feature = "uuid")]
810 #[tokio::test]
811 async fn uuid_roundtrip_bind_and_query_scalar_sqlite() {
812 let pool = SqlitePoolOptions::new()
813 .max_connections(1)
814 .connect("sqlite::memory:")
815 .await
816 .expect("create in-memory sqlite pool");
817
818 sqlx::query("CREATE TABLE test_uuids (id TEXT NOT NULL PRIMARY KEY)")
819 .execute(&pool)
820 .await
821 .expect("create test_uuids table");
822
823 let id = crate::Uuid::<TestUuidDomain>::parse("550e8400-e29b-41d4-a716-446655440000")
824 .expect("valid uuid");
825
826 sqlx::query("INSERT INTO test_uuids (id) VALUES (?)")
827 .bind(id)
828 .execute(&pool)
829 .await
830 .expect("insert typed uuid");
831
832 let row: crate::Uuid<TestUuidDomain> = sqlx::query_scalar("SELECT id FROM test_uuids")
833 .fetch_one(&pool)
834 .await
835 .expect("fetch typed uuid");
836
837 assert_eq!(id, row);
838 }
839
840 #[cfg(feature = "ulid")]
841 #[tokio::test]
842 async fn ulid_roundtrip_bind_and_query_scalar_sqlite() {
843 let pool = SqlitePoolOptions::new()
844 .max_connections(1)
845 .connect("sqlite::memory:")
846 .await
847 .expect("create in-memory sqlite pool");
848
849 sqlx::query("CREATE TABLE test_ulids (id TEXT NOT NULL PRIMARY KEY)")
850 .execute(&pool)
851 .await
852 .expect("create test_ulids table");
853
854 let id = crate::Ulid::<TestUlidDomain>::new();
855
856 sqlx::query("INSERT INTO test_ulids (id) VALUES (?)")
857 .bind(id)
858 .execute(&pool)
859 .await
860 .expect("insert typed ulid");
861
862 let row: crate::Ulid<TestUlidDomain> = sqlx::query_scalar("SELECT id FROM test_ulids")
863 .fetch_one(&pool)
864 .await
865 .expect("fetch typed ulid");
866
867 assert_eq!(id, row);
868 }
869
870 #[cfg(feature = "ulid")]
871 #[tokio::test]
872 async fn ulid_sqlite_decode_rejects_wrong_prefix() {
873 let pool = SqlitePoolOptions::new()
874 .max_connections(1)
875 .connect("sqlite::memory:")
876 .await
877 .expect("create in-memory sqlite pool");
878
879 sqlx::query("CREATE TABLE bad_ulids (id TEXT NOT NULL PRIMARY KEY)")
880 .execute(&pool)
881 .await
882 .expect("create bad_ulids table");
883
884 sqlx::query("INSERT INTO bad_ulids (id) VALUES (?)")
885 .bind("bad_01D39ZY06FGSCTVN4T2V9PKHFZ")
886 .execute(&pool)
887 .await
888 .expect("insert invalid-prefix ulid");
889
890 let result: Result<crate::Ulid<TestUlidDomain>, sqlx::Error> =
891 sqlx::query_scalar("SELECT id FROM bad_ulids")
892 .fetch_one(&pool)
893 .await;
894
895 assert!(result.is_err(), "decode should fail for wrong ULID prefix");
896 }
897 }
898
899 #[cfg(feature = "sqlx-mysql")]
900 mod sqlx_mysql_traits {
901 use super::*;
902 use sqlx::decode::Decode;
903 use sqlx::encode::Encode;
904 use sqlx::{MySql, Type};
905
906 fn assert_sqlx_traits<T>()
907 where
908 T: Type<MySql> + for<'q> Encode<'q, MySql> + for<'r> Decode<'r, MySql>,
909 {
910 }
911
912 #[test]
913 fn key_id_and_ulid_implement_sqlx_traits_for_mysql() {
914 assert_sqlx_traits::<TestKey>();
915 assert_sqlx_traits::<crate::Id<TestIdDomain>>();
916 #[cfg(feature = "ulid")]
917 assert_sqlx_traits::<crate::Ulid<TestUlidDomain>>();
918 }
919
920 #[test]
921 fn key_id_and_ulid_mysql_compatibility_matches_carriers() {
922 assert!(<TestKey as Type<MySql>>::compatible(&<String as Type<
923 MySql,
924 >>::type_info(
925 )));
926 assert!(<crate::Id<TestIdDomain> as Type<MySql>>::compatible(
927 &<i64 as Type<MySql>>::type_info()
928 ));
929 #[cfg(feature = "ulid")]
930 assert!(<crate::Ulid<TestUlidDomain> as Type<MySql>>::compatible(
931 &<String as Type<MySql>>::type_info()
932 ));
933 }
934
935 #[cfg(feature = "uuid")]
936 #[test]
937 fn uuid_implements_sqlx_traits_for_mysql() {
938 assert_sqlx_traits::<crate::Uuid<TestUuidDomain>>();
939 }
940
941 #[cfg(feature = "uuid")]
942 #[test]
943 fn uuid_mysql_uses_string_compatibility() {
944 assert!(<crate::Uuid<TestUuidDomain> as Type<MySql>>::compatible(
945 &<String as Type<MySql>>::type_info()
946 ));
947 let value = crate::Uuid::<TestUuidDomain>::nil();
948 let hint = <crate::Uuid<TestUuidDomain> as Encode<'_, MySql>>::size_hint(&value);
949 assert_eq!(hint, 36);
950 }
951 }
952
953 #[cfg(all(feature = "axum", feature = "serde"))]
954 mod axum_responses {
955 use super::*;
956 use crate::{IdParseError, KeyParseError};
957 use axum::body::{to_bytes, Body};
958 use axum::extract::{Form, Json};
959 use axum::http::{header, Request, StatusCode};
960 use axum::response::IntoResponse;
961 use axum::routing::get;
962 use axum::Router;
963 use serde::Deserialize;
964 use tower::util::ServiceExt;
965
966 #[derive(Debug, Deserialize)]
967 struct KeyPayload {
968 id: TestKey,
969 title: String,
970 }
971
972 #[derive(Debug, Deserialize)]
973 struct IdPayload {
974 id: crate::Id<TestIdDomain>,
975 title: String,
976 }
977
978 #[cfg(feature = "uuid")]
979 #[derive(Debug, Deserialize)]
980 struct UuidPayload {
981 id: crate::Uuid<TestUuidDomain>,
982 title: String,
983 }
984
985 #[cfg(feature = "ulid")]
986 #[derive(Debug, Deserialize)]
987 struct UlidPayload {
988 id: crate::Ulid<TestUlidDomain>,
989 title: String,
990 }
991
992 #[test]
993 fn key_error_maps_to_bad_request() {
994 let response = KeyParseError::Empty.into_response();
995 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
996 }
997
998 #[test]
999 fn id_error_maps_to_bad_request() {
1000 let response = IdParseError::Zero.into_response();
1001 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1002 }
1003
1004 #[cfg(feature = "uuid")]
1005 #[test]
1006 fn uuid_error_maps_to_bad_request() {
1007 let err = crate::Uuid::<TestUuidDomain>::parse("not-a-uuid").unwrap_err();
1008 let response = err.into_response();
1009 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1010 }
1011
1012 #[cfg(feature = "ulid")]
1013 #[test]
1014 fn ulid_error_maps_to_bad_request() {
1015 let err = crate::Ulid::<TestUlidDomain>::parse("bad").unwrap_err();
1016 let response = err.into_response();
1017 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1018 }
1019
1020 #[tokio::test]
1021 async fn e2e_handler_error_returns_bad_request_for_key() {
1022 async fn handler() -> Result<&'static str, KeyParseError> {
1023 Err(KeyParseError::Empty)
1024 }
1025
1026 let app = Router::new().route("/key", get(handler));
1027 let response = app
1028 .oneshot(
1029 Request::builder()
1030 .uri("/key")
1031 .method("GET")
1032 .body(Body::empty())
1033 .expect("build request"),
1034 )
1035 .await
1036 .expect("execute request");
1037
1038 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1039 let body = to_bytes(response.into_body(), usize::MAX)
1040 .await
1041 .expect("read response body");
1042 let text = String::from_utf8(body.to_vec()).expect("utf8 body");
1043 assert!(text.contains("Key cannot be empty"));
1044 }
1045
1046 #[tokio::test]
1047 async fn e2e_handler_error_returns_bad_request_for_id() {
1048 async fn handler() -> Result<&'static str, IdParseError> {
1049 Err(IdParseError::Zero)
1050 }
1051
1052 let app = Router::new().route("/id", get(handler));
1053 let response = app
1054 .oneshot(
1055 Request::builder()
1056 .uri("/id")
1057 .method("GET")
1058 .body(Body::empty())
1059 .expect("build request"),
1060 )
1061 .await
1062 .expect("execute request");
1063
1064 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1065 let body = to_bytes(response.into_body(), usize::MAX)
1066 .await
1067 .expect("read response body");
1068 let text = String::from_utf8(body.to_vec()).expect("utf8 body");
1069 assert!(text.contains("ID cannot be zero"));
1070 }
1071
1072 #[cfg(feature = "uuid")]
1073 #[tokio::test]
1074 async fn e2e_handler_error_returns_bad_request_for_uuid() {
1075 async fn handler() -> Result<&'static str, crate::UuidParseError> {
1076 crate::Uuid::<TestUuidDomain>::parse("not-a-uuid")?;
1077 Ok("ok")
1078 }
1079
1080 let app = Router::new().route("/uuid", get(handler));
1081 let response = app
1082 .oneshot(
1083 Request::builder()
1084 .uri("/uuid")
1085 .method("GET")
1086 .body(Body::empty())
1087 .expect("build request"),
1088 )
1089 .await
1090 .expect("execute request");
1091
1092 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1093 }
1094
1095 #[cfg(feature = "ulid")]
1096 #[tokio::test]
1097 async fn e2e_handler_error_returns_bad_request_for_ulid() {
1098 async fn handler() -> Result<&'static str, crate::UlidParseError> {
1099 crate::Ulid::<TestUlidDomain>::parse("bad")?;
1100 Ok("ok")
1101 }
1102
1103 let app = Router::new().route("/ulid", get(handler));
1104 let response = app
1105 .oneshot(
1106 Request::builder()
1107 .uri("/ulid")
1108 .method("GET")
1109 .body(Body::empty())
1110 .expect("build request"),
1111 )
1112 .await
1113 .expect("execute request");
1114
1115 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
1116 }
1117
1118 #[tokio::test]
1119 async fn e2e_json_payload_roundtrip_for_key_and_id() {
1120 async fn key_handler(Json(payload): Json<KeyPayload>) -> &'static str {
1121 assert_eq!(payload.id.as_str(), "valid_key");
1122 assert_eq!(payload.title, "hello");
1123 "ok"
1124 }
1125 async fn id_handler(Json(payload): Json<IdPayload>) -> &'static str {
1126 assert_eq!(payload.id.get(), 42);
1127 assert_eq!(payload.title, "hello");
1128 "ok"
1129 }
1130
1131 let app = Router::new()
1132 .route("/json-key", axum::routing::post(key_handler))
1133 .route("/json-id", axum::routing::post(id_handler));
1134
1135 let key_ok = app
1136 .clone()
1137 .oneshot(
1138 Request::builder()
1139 .uri("/json-key")
1140 .method("POST")
1141 .header(header::CONTENT_TYPE, "application/json")
1142 .body(Body::from(r#"{"id":"valid_key","title":"hello"}"#))
1143 .expect("build key request"),
1144 )
1145 .await
1146 .expect("execute key request");
1147 assert_eq!(key_ok.status(), StatusCode::OK);
1148
1149 let id_ok = app
1150 .clone()
1151 .oneshot(
1152 Request::builder()
1153 .uri("/json-id")
1154 .method("POST")
1155 .header(header::CONTENT_TYPE, "application/json")
1156 .body(Body::from(r#"{"id":42,"title":"hello"}"#))
1157 .expect("build id request"),
1158 )
1159 .await
1160 .expect("execute id request");
1161 assert_eq!(id_ok.status(), StatusCode::OK);
1162
1163 let key_bad = app
1164 .clone()
1165 .oneshot(
1166 Request::builder()
1167 .uri("/json-key")
1168 .method("POST")
1169 .header(header::CONTENT_TYPE, "application/json")
1170 .body(Body::from(r#"{"id":"bad key","title":"hello"}"#))
1171 .expect("build bad key request"),
1172 )
1173 .await
1174 .expect("execute bad key request");
1175 assert!(key_bad.status().is_client_error());
1176
1177 let id_bad = app
1178 .oneshot(
1179 Request::builder()
1180 .uri("/json-id")
1181 .method("POST")
1182 .header(header::CONTENT_TYPE, "application/json")
1183 .body(Body::from(r#"{"id":0,"title":"hello"}"#))
1184 .expect("build bad id request"),
1185 )
1186 .await
1187 .expect("execute bad id request");
1188 assert!(id_bad.status().is_client_error());
1189 }
1190
1191 #[tokio::test]
1192 async fn e2e_form_payload_roundtrip_for_key_and_id() {
1193 async fn key_handler(Form(payload): Form<KeyPayload>) -> &'static str {
1194 assert_eq!(payload.id.as_str(), "valid_key");
1195 assert_eq!(payload.title, "hello");
1196 "ok"
1197 }
1198 async fn id_handler(Form(payload): Form<IdPayload>) -> &'static str {
1199 assert_eq!(payload.id.get(), 42);
1200 assert_eq!(payload.title, "hello");
1201 "ok"
1202 }
1203
1204 let app = Router::new()
1205 .route("/form-key", axum::routing::post(key_handler))
1206 .route("/form-id", axum::routing::post(id_handler));
1207
1208 let key_ok = app
1209 .clone()
1210 .oneshot(
1211 Request::builder()
1212 .uri("/form-key")
1213 .method("POST")
1214 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1215 .body(Body::from("id=valid_key&title=hello"))
1216 .expect("build key form request"),
1217 )
1218 .await
1219 .expect("execute key form request");
1220 assert_eq!(key_ok.status(), StatusCode::OK);
1221
1222 let id_ok = app
1223 .clone()
1224 .oneshot(
1225 Request::builder()
1226 .uri("/form-id")
1227 .method("POST")
1228 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1229 .body(Body::from("id=42&title=hello"))
1230 .expect("build id form request"),
1231 )
1232 .await
1233 .expect("execute id form request");
1234 assert_eq!(id_ok.status(), StatusCode::OK);
1235
1236 let key_bad = app
1237 .clone()
1238 .oneshot(
1239 Request::builder()
1240 .uri("/form-key")
1241 .method("POST")
1242 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1243 .body(Body::from("id=bad%20key&title=hello"))
1244 .expect("build bad key form request"),
1245 )
1246 .await
1247 .expect("execute bad key form request");
1248 assert!(key_bad.status().is_client_error());
1249
1250 let id_bad = app
1251 .oneshot(
1252 Request::builder()
1253 .uri("/form-id")
1254 .method("POST")
1255 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1256 .body(Body::from("id=0&title=hello"))
1257 .expect("build bad id form request"),
1258 )
1259 .await
1260 .expect("execute bad id form request");
1261 assert!(id_bad.status().is_client_error());
1262 }
1263
1264 #[cfg(feature = "uuid")]
1265 #[tokio::test]
1266 async fn e2e_json_and_form_payload_roundtrip_for_uuid() {
1267 async fn json_handler(Json(payload): Json<UuidPayload>) -> &'static str {
1268 assert_eq!(
1269 payload.id.to_string(),
1270 "550e8400-e29b-41d4-a716-446655440000"
1271 );
1272 assert_eq!(payload.title, "hello");
1273 "ok"
1274 }
1275 async fn form_handler(Form(payload): Form<UuidPayload>) -> &'static str {
1276 assert_eq!(
1277 payload.id.to_string(),
1278 "550e8400-e29b-41d4-a716-446655440000"
1279 );
1280 assert_eq!(payload.title, "hello");
1281 "ok"
1282 }
1283
1284 let app = Router::new()
1285 .route("/json-uuid", axum::routing::post(json_handler))
1286 .route("/form-uuid", axum::routing::post(form_handler));
1287
1288 let json_ok = app
1289 .clone()
1290 .oneshot(
1291 Request::builder()
1292 .uri("/json-uuid")
1293 .method("POST")
1294 .header(header::CONTENT_TYPE, "application/json")
1295 .body(Body::from(
1296 r#"{"id":"550e8400-e29b-41d4-a716-446655440000","title":"hello"}"#,
1297 ))
1298 .expect("build uuid json request"),
1299 )
1300 .await
1301 .expect("execute uuid json request");
1302 assert_eq!(json_ok.status(), StatusCode::OK);
1303
1304 let form_ok = app
1305 .clone()
1306 .oneshot(
1307 Request::builder()
1308 .uri("/form-uuid")
1309 .method("POST")
1310 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1311 .body(Body::from(
1312 "id=550e8400-e29b-41d4-a716-446655440000&title=hello",
1313 ))
1314 .expect("build uuid form request"),
1315 )
1316 .await
1317 .expect("execute uuid form request");
1318 assert_eq!(form_ok.status(), StatusCode::OK);
1319
1320 let json_bad = app
1321 .clone()
1322 .oneshot(
1323 Request::builder()
1324 .uri("/json-uuid")
1325 .method("POST")
1326 .header(header::CONTENT_TYPE, "application/json")
1327 .body(Body::from(r#"{"id":"not-a-uuid","title":"hello"}"#))
1328 .expect("build bad uuid json request"),
1329 )
1330 .await
1331 .expect("execute bad uuid json request");
1332 assert!(json_bad.status().is_client_error());
1333
1334 let form_bad = app
1335 .oneshot(
1336 Request::builder()
1337 .uri("/form-uuid")
1338 .method("POST")
1339 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1340 .body(Body::from("id=not-a-uuid&title=hello"))
1341 .expect("build bad uuid form request"),
1342 )
1343 .await
1344 .expect("execute bad uuid form request");
1345 assert!(form_bad.status().is_client_error());
1346 }
1347
1348 #[cfg(feature = "ulid")]
1349 #[tokio::test]
1350 async fn e2e_json_and_form_payload_roundtrip_for_ulid() {
1351 async fn json_handler(Json(payload): Json<UlidPayload>) -> &'static str {
1352 assert!(payload.id.to_string().starts_with("tst_"));
1353 assert_eq!(payload.title, "hello");
1354 "ok"
1355 }
1356 async fn form_handler(Form(payload): Form<UlidPayload>) -> &'static str {
1357 assert!(payload.id.to_string().starts_with("tst_"));
1358 assert_eq!(payload.title, "hello");
1359 "ok"
1360 }
1361
1362 let app = Router::new()
1363 .route("/json-ulid", axum::routing::post(json_handler))
1364 .route("/form-ulid", axum::routing::post(form_handler));
1365
1366 let json_ok = app
1367 .clone()
1368 .oneshot(
1369 Request::builder()
1370 .uri("/json-ulid")
1371 .method("POST")
1372 .header(header::CONTENT_TYPE, "application/json")
1373 .body(Body::from(
1374 r#"{"id":"tst_01D39ZY06FGSCTVN4T2V9PKHFZ","title":"hello"}"#,
1375 ))
1376 .expect("build ulid json request"),
1377 )
1378 .await
1379 .expect("execute ulid json request");
1380 assert_eq!(json_ok.status(), StatusCode::OK);
1381
1382 let form_ok = app
1383 .clone()
1384 .oneshot(
1385 Request::builder()
1386 .uri("/form-ulid")
1387 .method("POST")
1388 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1389 .body(Body::from("id=tst_01D39ZY06FGSCTVN4T2V9PKHFZ&title=hello"))
1390 .expect("build ulid form request"),
1391 )
1392 .await
1393 .expect("execute ulid form request");
1394 assert_eq!(form_ok.status(), StatusCode::OK);
1395
1396 let json_bad = app
1397 .clone()
1398 .oneshot(
1399 Request::builder()
1400 .uri("/json-ulid")
1401 .method("POST")
1402 .header(header::CONTENT_TYPE, "application/json")
1403 .body(Body::from(r#"{"id":"bad","title":"hello"}"#))
1404 .expect("build bad ulid json request"),
1405 )
1406 .await
1407 .expect("execute bad ulid json request");
1408 assert!(json_bad.status().is_client_error());
1409
1410 let form_bad = app
1411 .oneshot(
1412 Request::builder()
1413 .uri("/form-ulid")
1414 .method("POST")
1415 .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
1416 .body(Body::from("id=bad&title=hello"))
1417 .expect("build bad ulid form request"),
1418 )
1419 .await
1420 .expect("execute bad ulid form request");
1421 assert!(form_bad.status().is_client_error());
1422 }
1423 }
1424
1425 #[cfg(all(feature = "actix-web", feature = "serde"))]
1426 mod actix_responses {
1427 use super::*;
1428 use crate::{IdParseError, KeyParseError};
1429 use actix_web::http::StatusCode;
1430 use actix_web::{test, web, App, HttpResponse, ResponseError};
1431 use serde::{Deserialize, Serialize};
1432
1433 #[derive(Debug, Deserialize, Serialize)]
1434 struct KeyPayload {
1435 id: TestKey,
1436 title: String,
1437 }
1438
1439 #[derive(Debug, Deserialize, Serialize)]
1440 struct IdPayload {
1441 id: crate::Id<TestIdDomain>,
1442 title: String,
1443 }
1444
1445 #[cfg(feature = "uuid")]
1446 #[derive(Debug, Deserialize, Serialize)]
1447 struct UuidPayload {
1448 id: crate::Uuid<TestUuidDomain>,
1449 title: String,
1450 }
1451
1452 #[cfg(feature = "ulid")]
1453 #[derive(Debug, Deserialize, Serialize)]
1454 struct UlidPayload {
1455 id: crate::Ulid<TestUlidDomain>,
1456 title: String,
1457 }
1458
1459 #[test]
1460 fn key_error_maps_to_bad_request() {
1461 assert_eq!(KeyParseError::Empty.status_code(), StatusCode::BAD_REQUEST);
1462 }
1463
1464 #[test]
1465 fn id_error_maps_to_bad_request() {
1466 assert_eq!(IdParseError::Zero.status_code(), StatusCode::BAD_REQUEST);
1467 }
1468
1469 #[cfg(feature = "uuid")]
1470 #[test]
1471 fn uuid_error_maps_to_bad_request() {
1472 let err = crate::Uuid::<TestUuidDomain>::parse("not-a-uuid").unwrap_err();
1473 assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
1474 }
1475
1476 #[cfg(feature = "ulid")]
1477 #[test]
1478 fn ulid_error_maps_to_bad_request() {
1479 let err = crate::Ulid::<TestUlidDomain>::parse("bad").unwrap_err();
1480 assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
1481 }
1482
1483 #[tokio::test]
1484 async fn e2e_handler_error_returns_bad_request_for_key() {
1485 async fn handler() -> Result<HttpResponse, KeyParseError> {
1486 Err(KeyParseError::Empty)
1487 }
1488
1489 let app = test::init_service(App::new().route("/key", web::get().to(handler))).await;
1490 let req = test::TestRequest::get().uri("/key").to_request();
1491 let resp = test::call_service(&app, req).await;
1492
1493 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1494 let body = test::read_body(resp).await;
1495 let text = String::from_utf8(body.to_vec()).expect("utf8 body");
1496 assert!(text.contains("Key cannot be empty"));
1497 }
1498
1499 #[tokio::test]
1500 async fn e2e_handler_error_returns_bad_request_for_id() {
1501 async fn handler() -> Result<HttpResponse, IdParseError> {
1502 Err(IdParseError::Zero)
1503 }
1504
1505 let app = test::init_service(App::new().route("/id", web::get().to(handler))).await;
1506 let req = test::TestRequest::get().uri("/id").to_request();
1507 let resp = test::call_service(&app, req).await;
1508
1509 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1510 let body = test::read_body(resp).await;
1511 let text = String::from_utf8(body.to_vec()).expect("utf8 body");
1512 assert!(text.contains("ID cannot be zero"));
1513 }
1514
1515 #[cfg(feature = "uuid")]
1516 #[tokio::test]
1517 async fn e2e_handler_error_returns_bad_request_for_uuid() {
1518 async fn handler() -> Result<HttpResponse, crate::UuidParseError> {
1519 crate::Uuid::<TestUuidDomain>::parse("not-a-uuid")?;
1520 Ok(HttpResponse::Ok().finish())
1521 }
1522
1523 let app = test::init_service(App::new().route("/uuid", web::get().to(handler))).await;
1524 let req = test::TestRequest::get().uri("/uuid").to_request();
1525 let resp = test::call_service(&app, req).await;
1526
1527 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1528 }
1529
1530 #[cfg(feature = "ulid")]
1531 #[tokio::test]
1532 async fn e2e_handler_error_returns_bad_request_for_ulid() {
1533 async fn handler() -> Result<HttpResponse, crate::UlidParseError> {
1534 crate::Ulid::<TestUlidDomain>::parse("bad")?;
1535 Ok(HttpResponse::Ok().finish())
1536 }
1537
1538 let app = test::init_service(App::new().route("/ulid", web::get().to(handler))).await;
1539 let req = test::TestRequest::get().uri("/ulid").to_request();
1540 let resp = test::call_service(&app, req).await;
1541
1542 assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
1543 }
1544
1545 #[tokio::test]
1546 async fn e2e_json_payload_roundtrip_for_key_and_id() {
1547 async fn key_handler(payload: web::Json<KeyPayload>) -> HttpResponse {
1548 assert_eq!(payload.id.as_str(), "valid_key");
1549 assert_eq!(payload.title, "hello");
1550 HttpResponse::Ok().finish()
1551 }
1552 async fn id_handler(payload: web::Json<IdPayload>) -> HttpResponse {
1553 assert_eq!(payload.id.get(), 42);
1554 assert_eq!(payload.title, "hello");
1555 HttpResponse::Ok().finish()
1556 }
1557
1558 let app = test::init_service(
1559 App::new()
1560 .route("/json-key", web::post().to(key_handler))
1561 .route("/json-id", web::post().to(id_handler)),
1562 )
1563 .await;
1564
1565 let key_ok = test::TestRequest::post()
1566 .uri("/json-key")
1567 .set_json(&serde_json::json!({"id": "valid_key", "title": "hello"}))
1568 .to_request();
1569 let key_resp = test::call_service(&app, key_ok).await;
1570 assert_eq!(key_resp.status(), StatusCode::OK);
1571
1572 let id_ok = test::TestRequest::post()
1573 .uri("/json-id")
1574 .set_json(&serde_json::json!({"id": 42, "title": "hello"}))
1575 .to_request();
1576 let id_resp = test::call_service(&app, id_ok).await;
1577 assert_eq!(id_resp.status(), StatusCode::OK);
1578
1579 let key_bad = test::TestRequest::post()
1580 .uri("/json-key")
1581 .set_json(&serde_json::json!({"id": "bad key", "title": "hello"}))
1582 .to_request();
1583 let key_bad_resp = test::call_service(&app, key_bad).await;
1584 assert!(key_bad_resp.status().is_client_error());
1585
1586 let id_bad = test::TestRequest::post()
1587 .uri("/json-id")
1588 .set_json(&serde_json::json!({"id": 0, "title": "hello"}))
1589 .to_request();
1590 let id_bad_resp = test::call_service(&app, id_bad).await;
1591 assert!(id_bad_resp.status().is_client_error());
1592 }
1593
1594 #[tokio::test]
1595 async fn e2e_form_payload_roundtrip_for_key_and_id() {
1596 async fn key_handler(payload: web::Form<KeyPayload>) -> HttpResponse {
1597 assert_eq!(payload.id.as_str(), "valid_key");
1598 assert_eq!(payload.title, "hello");
1599 HttpResponse::Ok().finish()
1600 }
1601 async fn id_handler(payload: web::Form<IdPayload>) -> HttpResponse {
1602 assert_eq!(payload.id.get(), 42);
1603 assert_eq!(payload.title, "hello");
1604 HttpResponse::Ok().finish()
1605 }
1606
1607 let app = test::init_service(
1608 App::new()
1609 .route("/form-key", web::post().to(key_handler))
1610 .route("/form-id", web::post().to(id_handler)),
1611 )
1612 .await;
1613
1614 let key_ok = test::TestRequest::post()
1615 .uri("/form-key")
1616 .set_form(&KeyPayload {
1617 id: TestKey::new("valid_key").expect("valid key"),
1618 title: "hello".to_string(),
1619 })
1620 .to_request();
1621 let key_resp = test::call_service(&app, key_ok).await;
1622 assert_eq!(key_resp.status(), StatusCode::OK);
1623
1624 let id_ok = test::TestRequest::post()
1625 .uri("/form-id")
1626 .set_form(&IdPayload {
1627 id: crate::Id::<TestIdDomain>::new(42).expect("non-zero id"),
1628 title: "hello".to_string(),
1629 })
1630 .to_request();
1631 let id_resp = test::call_service(&app, id_ok).await;
1632 assert_eq!(id_resp.status(), StatusCode::OK);
1633
1634 let key_bad = test::TestRequest::post()
1635 .uri("/form-key")
1636 .insert_header(("content-type", "application/x-www-form-urlencoded"))
1637 .set_payload("id=bad%20key&title=hello")
1638 .to_request();
1639 let key_bad_resp = test::call_service(&app, key_bad).await;
1640 assert!(key_bad_resp.status().is_client_error());
1641
1642 let id_bad = test::TestRequest::post()
1643 .uri("/form-id")
1644 .insert_header(("content-type", "application/x-www-form-urlencoded"))
1645 .set_payload("id=0&title=hello")
1646 .to_request();
1647 let id_bad_resp = test::call_service(&app, id_bad).await;
1648 assert!(id_bad_resp.status().is_client_error());
1649 }
1650
1651 #[cfg(feature = "uuid")]
1652 #[tokio::test]
1653 async fn e2e_json_and_form_payload_roundtrip_for_uuid() {
1654 async fn json_handler(payload: web::Json<UuidPayload>) -> HttpResponse {
1655 assert_eq!(
1656 payload.id.to_string(),
1657 "550e8400-e29b-41d4-a716-446655440000"
1658 );
1659 assert_eq!(payload.title, "hello");
1660 HttpResponse::Ok().finish()
1661 }
1662 async fn form_handler(payload: web::Form<UuidPayload>) -> HttpResponse {
1663 assert_eq!(
1664 payload.id.to_string(),
1665 "550e8400-e29b-41d4-a716-446655440000"
1666 );
1667 assert_eq!(payload.title, "hello");
1668 HttpResponse::Ok().finish()
1669 }
1670
1671 let app = test::init_service(
1672 App::new()
1673 .route("/json-uuid", web::post().to(json_handler))
1674 .route("/form-uuid", web::post().to(form_handler)),
1675 )
1676 .await;
1677
1678 let json_ok = test::TestRequest::post()
1679 .uri("/json-uuid")
1680 .set_json(
1681 &serde_json::json!({"id":"550e8400-e29b-41d4-a716-446655440000","title":"hello"}),
1682 )
1683 .to_request();
1684 let json_resp = test::call_service(&app, json_ok).await;
1685 assert_eq!(json_resp.status(), StatusCode::OK);
1686
1687 let form_ok = test::TestRequest::post()
1688 .uri("/form-uuid")
1689 .set_form(&UuidPayload {
1690 id: crate::Uuid::<TestUuidDomain>::parse(
1691 "550e8400-e29b-41d4-a716-446655440000",
1692 )
1693 .expect("valid uuid"),
1694 title: "hello".to_string(),
1695 })
1696 .to_request();
1697 let form_resp = test::call_service(&app, form_ok).await;
1698 assert_eq!(form_resp.status(), StatusCode::OK);
1699
1700 let json_bad = test::TestRequest::post()
1701 .uri("/json-uuid")
1702 .set_json(&serde_json::json!({"id":"not-a-uuid","title":"hello"}))
1703 .to_request();
1704 let json_bad_resp = test::call_service(&app, json_bad).await;
1705 assert!(json_bad_resp.status().is_client_error());
1706
1707 let form_bad = test::TestRequest::post()
1708 .uri("/form-uuid")
1709 .insert_header(("content-type", "application/x-www-form-urlencoded"))
1710 .set_payload("id=not-a-uuid&title=hello")
1711 .to_request();
1712 let form_bad_resp = test::call_service(&app, form_bad).await;
1713 assert!(form_bad_resp.status().is_client_error());
1714 }
1715
1716 #[cfg(feature = "ulid")]
1717 #[tokio::test]
1718 async fn e2e_json_and_form_payload_roundtrip_for_ulid() {
1719 async fn json_handler(payload: web::Json<UlidPayload>) -> HttpResponse {
1720 assert!(payload.id.to_string().starts_with("tst_"));
1721 assert_eq!(payload.title, "hello");
1722 HttpResponse::Ok().finish()
1723 }
1724 async fn form_handler(payload: web::Form<UlidPayload>) -> HttpResponse {
1725 assert!(payload.id.to_string().starts_with("tst_"));
1726 assert_eq!(payload.title, "hello");
1727 HttpResponse::Ok().finish()
1728 }
1729
1730 let app = test::init_service(
1731 App::new()
1732 .route("/json-ulid", web::post().to(json_handler))
1733 .route("/form-ulid", web::post().to(form_handler)),
1734 )
1735 .await;
1736
1737 let json_ok = test::TestRequest::post()
1738 .uri("/json-ulid")
1739 .set_json(
1740 &serde_json::json!({"id":"tst_01D39ZY06FGSCTVN4T2V9PKHFZ","title":"hello"}),
1741 )
1742 .to_request();
1743 let json_resp = test::call_service(&app, json_ok).await;
1744 assert_eq!(json_resp.status(), StatusCode::OK);
1745
1746 let form_ok = test::TestRequest::post()
1747 .uri("/form-ulid")
1748 .set_form(&UlidPayload {
1749 id: crate::Ulid::<TestUlidDomain>::parse("tst_01D39ZY06FGSCTVN4T2V9PKHFZ")
1750 .expect("valid ulid"),
1751 title: "hello".to_string(),
1752 })
1753 .to_request();
1754 let form_resp = test::call_service(&app, form_ok).await;
1755 assert_eq!(form_resp.status(), StatusCode::OK);
1756
1757 let json_bad = test::TestRequest::post()
1758 .uri("/json-ulid")
1759 .set_json(&serde_json::json!({"id":"bad","title":"hello"}))
1760 .to_request();
1761 let json_bad_resp = test::call_service(&app, json_bad).await;
1762 assert!(json_bad_resp.status().is_client_error());
1763
1764 let form_bad = test::TestRequest::post()
1765 .uri("/form-ulid")
1766 .insert_header(("content-type", "application/x-www-form-urlencoded"))
1767 .set_payload("id=bad&title=hello")
1768 .to_request();
1769 let form_bad_resp = test::call_service(&app, form_bad).await;
1770 assert!(form_bad_resp.status().is_client_error());
1771 }
1772 }
1773}