1use std::{
4 any::{TypeId, type_name},
5 marker::PhantomData,
6};
7
8use crate::Conn;
9
10#[must_use = "dropping an erased connection abandons the PostgreSQL session"]
13#[derive(Debug)]
14pub struct ErasedConn<S> {
15 transport: Option<S>,
16 phase: TypeId,
17 phase_name: &'static str,
18 cleanliness: TypeId,
19 cleanliness_name: &'static str,
20}
21
22impl<S> ErasedConn<S> {
23 #[must_use]
25 pub const fn phase_name(&self) -> &'static str {
26 self.phase_name
27 }
28
29 #[must_use]
31 pub const fn cleanliness_name(&self) -> &'static str {
32 self.cleanliness_name
33 }
34
35 #[must_use]
37 pub fn phase_is<P: 'static>(&self) -> bool {
38 self.phase == TypeId::of::<P>()
39 }
40
41 #[must_use]
43 pub fn cleanliness_is<C: 'static>(&self) -> bool {
44 self.cleanliness == TypeId::of::<C>()
45 }
46
47 pub fn try_reenter<P: 'static, C: 'static>(mut self) -> Result<Conn<S, P, C>, Self> {
55 if !self.phase_is::<P>() || !self.cleanliness_is::<C>() {
56 return Err(self);
57 }
58 Ok(Conn {
59 transport: self.transport.take(),
60 _state: PhantomData,
61 })
62 }
63
64 pub fn map_transport<T>(mut self, map: impl FnOnce(S) -> T) -> ErasedConn<T> {
71 ErasedConn {
72 transport: Some(map(self
73 .transport
74 .take()
75 .expect("live erased connection has a transport"))),
76 phase: self.phase,
77 phase_name: self.phase_name,
78 cleanliness: self.cleanliness,
79 cleanliness_name: self.cleanliness_name,
80 }
81 }
82
83 pub fn into_transport(mut self) -> S {
89 self.transport
90 .take()
91 .expect("live erased connection has a transport")
92 }
93}
94
95impl<S, P: 'static, C: 'static> Conn<S, P, C> {
96 pub fn erase(mut self) -> ErasedConn<S> {
99 ErasedConn {
100 transport: self.transport.take(),
101 phase: TypeId::of::<P>(),
102 phase_name: type_name::<P>(),
103 cleanliness: TypeId::of::<C>(),
104 cleanliness_name: type_name::<C>(),
105 }
106 }
107}
108
109#[cfg(debug_assertions)]
110impl<S> Drop for ErasedConn<S> {
111 fn drop(&mut self) {
112 assert!(
113 self.transport.is_none() || std::thread::panicking(),
114 "live erased PostgreSQL connection dropped; re-enter or extract its transport"
115 );
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use crate::{Dirty, Pristine, auth::Ready, session::Building};
122
123 use super::*;
124
125 #[test]
126 fn exact_state_can_be_erased_and_reentered() {
127 let ready: Conn<_, Ready, Pristine> = Conn::new(42_u8).transition();
128 let erased = ready.erase();
129 assert!(erased.phase_is::<Ready>());
130 assert!(erased.cleanliness_is::<Pristine>());
131
132 let ready = erased
133 .try_reenter::<Ready, Pristine>()
134 .expect("exact state identities match");
135 assert_eq!(ready.into_transport(), 42);
136 }
137
138 #[test]
139 fn failed_reentry_preserves_the_erased_connection() {
140 let building: Conn<_, Building, Dirty> = Conn::new(42_u8).transition();
141 let erased = building.erase();
142
143 let erased = erased
144 .try_reenter::<Ready, Dirty>()
145 .expect_err("wrong phase must not re-enter");
146 let erased = erased
147 .try_reenter::<Building, Pristine>()
148 .expect_err("wrong cleanliness must not re-enter");
149 let building = erased
150 .try_reenter::<Building, Dirty>()
151 .expect("both exact identities match");
152 assert_eq!(building.into_transport(), 42);
153 }
154
155 #[test]
156 fn transport_mapping_does_not_change_erased_state() {
157 let ready: Conn<_, Ready, Pristine> = Conn::new(42_u8).transition();
158 let erased = ready.erase().map_transport(u16::from);
159
160 let ready = erased
161 .try_reenter::<Ready, Pristine>()
162 .expect("mapping retained exact identities");
163 assert_eq!(ready.into_transport(), 42_u16);
164 }
165}