Skip to main content

domain_key/
integrations.rs

1//! Framework/database integrations behind feature flags.
2//!
3//! This module wires `domain-key` types into common ecosystem crates:
4//! - `sqlx` for database encoding/decoding
5//! - `axum` for HTTP error responses
6//! - `actix-web` for HTTP error responses
7
8#[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    // UUID uses backend-specific mappings:
141    // - Postgres: native UUID type (`uuid`)
142    // - SQLite/MySQL: string representation
143    #[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    // ULID uses backend-specific mappings:
279    // - Postgres: native UUID type (`uuid`, 16 bytes)
280    // - SQLite/MySQL: prefixed string representation
281    #[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}