vcr_cassette/
lib.rs

1//! Serializer and deserializer for the [VCR Cassette
2//! format](https://relishapp.com/vcr/vcr/v/6-0-0/docs/cassettes/cassette-format).
3//!
4//! # Examples
5//!
6//! Given the following `.json` VCR Cassette recording:
7//! ```json
8//! {
9//!     "http_interactions": [
10//!         {
11//!             "request": {
12//!                 "uri": "http://localhost:7777/foo",
13//!                 "body": "",
14//!                 "method": "get",
15//!                 "headers": { "Accept-Encoding": [ "identity" ] }
16//!             },
17//!             "response": {
18//!                 "body": "Hello foo",
19//!                 "http_version": "1.1",
20//!                 "status": { "code": 200, "message": "OK" },
21//!                 "headers": {
22//!                     "Date": [ "Thu, 27 Oct 2011 06:16:31 GMT" ],
23//!                     "Content-Type": [ "text/html;charset=utf-8" ],
24//!                     "Content-Length": [ "9" ],
25//!                 }
26//!             },
27//!             "recorded_at": "Tue, 01 Nov 2011 04:58:44 GMT"
28//!         },
29//!     ],
30//!     "recorded_with": "VCR 2.0.0"
31//! }
32//! ```
33//!
34//! We can deserialize it using [`serde_json`](https://docs.rs/serde-json):
35//!
36//! ```rust
37//! # #![allow(unused)]
38//! use std::fs;
39//! use vcr_cassette::Cassette;
40//!
41//! let example = fs::read_to_string("tests/fixtures/example.json").unwrap();
42//! let cassette: Cassette = serde_json::from_str(&example).unwrap();
43//! ```
44//!
45//! To deserialize `.yaml` Cassette files use
46//! [`serde_yaml`](https://docs.rs/serde-yaml) instead.
47
48#![forbid(unsafe_code, future_incompatible)]
49#![deny(missing_debug_implementations, nonstandard_style)]
50#![warn(missing_docs, unreachable_pub)]
51
52use std::fmt;
53use std::marker::PhantomData;
54use std::{collections::HashMap, str::FromStr};
55
56use chrono::{offset::FixedOffset, DateTime};
57use serde::de::{self, MapAccess, Visitor};
58use serde::{Deserialize, Deserializer, Serialize};
59use url::Url;
60use void::Void;
61
62pub use chrono;
63pub use url;
64
65mod datetime;
66
67/// An HTTP Headers type.
68pub type Headers = HashMap<String, Vec<String>>;
69
70/// An identifier of the library which created the recording.
71///
72/// # Examples
73///
74/// ```
75/// # #![allow(unused)]
76/// use vcr_cassette::RecorderId;
77///
78/// let id: RecorderId = String::from("VCR 2.0.0");
79/// ```
80pub type RecorderId = String;
81
82/// A sequence of recorded HTTP Request/Response pairs.
83#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
84pub struct Cassette {
85    /// A sequence of recorded HTTP Request/Response pairs.
86    pub http_interactions: Vec<HttpInteraction>,
87
88    /// An identifier of the library which created the recording.
89    pub recorded_with: RecorderId,
90}
91
92/// A single HTTP Request/Response pair.
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
94pub struct HttpInteraction {
95    /// An HTTP response.
96    pub response: Response,
97    /// An HTTP request.
98    pub request: Request,
99
100    /// An [RFC
101    /// 2822](https://docs.rs/chrono/0.4.19/chrono/struct.DateTime.html#method.parse_from_rfc2822)
102    /// formatted timestamp.
103    ///
104    /// # Examples
105    ///
106    /// ```json
107    /// { "recorded_at": "Tue, 01 Nov 2011 04:58:44 GMT" }
108    /// ```
109    #[serde(with = "datetime")]
110    pub recorded_at: DateTime<FixedOffset>,
111}
112
113/// A recorded HTTP Response.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115pub struct Response {
116    /// An HTTP Body.
117    #[serde(deserialize_with = "string_or_struct")]
118    pub body: Body,
119    /// The version of the HTTP Response.
120    pub http_version: Option<Version>,
121    /// The Response status
122    pub status: Status,
123    /// The Response headers
124    pub headers: Headers,
125}
126
127/// A recorded HTTP Body.
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129pub struct Body {
130    /// The encoding of the HTTP body.
131    pub encoding: Option<String>,
132    /// The HTTP body encoded as a string.
133    pub string: String,
134}
135
136impl FromStr for Body {
137    // This implementation of `from_str` can never fail, so use the impossible
138    // `Void` type as the error type.
139    type Err = Void;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        Ok(Body {
143            encoding: None,
144            string: s.to_string(),
145        })
146    }
147}
148
149/// A recorded HTTP Status Code.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct Status {
152    /// The HTTP status code.
153    pub code: u16,
154    /// The HTTP status message.
155    pub message: String,
156}
157
158/// A recorded HTTP Request.
159#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct Request {
161    /// The Request URI.
162    pub uri: Url,
163    /// The Request body.
164    #[serde(deserialize_with = "string_or_struct")]
165    pub body: Body,
166    /// The Request method.
167    pub method: Method,
168    /// The Request headers.
169    pub headers: Headers,
170}
171
172/// An HTTP method.
173///
174/// WebDAV and custom methods can be created by passing a static string to the
175/// `Other` member.
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(rename_all = "lowercase")]
178pub enum Method {
179    /// An HTTP `CONNECT` method.
180    Connect,
181    /// An HTTP `DELETE` method.
182    Delete,
183    /// An HTTP `GET` method.
184    Get,
185    /// An HTTP `HEAD` method.
186    Head,
187    /// An HTTP `OPTIONS` method.
188    Options,
189    /// An HTTP `PATCH` method.
190    Patch,
191    /// An HTTP `POST` method.
192    Post,
193    /// An HTTP `PUT` method.
194    Put,
195    /// An HTTP `TRACE` method.
196    Trace,
197    /// Any other HTTP method.
198    Other(String),
199}
200
201impl Method {
202    /// Convert the HTTP method to its string representation.
203    pub fn as_str(&self) -> &str {
204        match self {
205            Method::Connect => "CONNECT",
206            Method::Delete => "DELETE",
207            Method::Get => "GET",
208            Method::Head => "HEAD",
209            Method::Options => "OPTIONS",
210            Method::Patch => "PATCH",
211            Method::Post => "POST",
212            Method::Put => "PUT",
213            Method::Trace => "TRACE",
214            Method::Other(s) => &s,
215        }
216    }
217}
218
219/// The version of the HTTP protocol in use.
220#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
221#[non_exhaustive]
222pub enum Version {
223    /// HTTP/0.9
224    #[serde(rename = "0.9")]
225    Http0_9,
226
227    /// HTTP/1.0
228    #[serde(rename = "1.0")]
229    Http1_0,
230
231    /// HTTP/1.1
232    #[serde(rename = "1.1")]
233    Http1_1,
234
235    /// HTTP/2.0
236    #[serde(rename = "2")]
237    Http2_0,
238
239    /// HTTP/3.0
240    #[serde(rename = "3")]
241    Http3_0,
242}
243
244// Copied from: https://serde.rs/string-or-struct.html
245fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
246where
247    T: Deserialize<'de> + FromStr<Err = Void>,
248    D: Deserializer<'de>,
249{
250    struct StringOrStruct<T>(PhantomData<fn() -> T>);
251
252    impl<'de, T> Visitor<'de> for StringOrStruct<T>
253    where
254        T: Deserialize<'de> + FromStr<Err = Void>,
255    {
256        type Value = T;
257
258        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
259            formatter.write_str("string or map")
260        }
261
262        fn visit_str<E>(self, value: &str) -> Result<T, E>
263        where
264            E: de::Error,
265        {
266            Ok(FromStr::from_str(value).unwrap())
267        }
268
269        fn visit_map<M>(self, map: M) -> Result<T, M::Error>
270        where
271            M: MapAccess<'de>,
272        {
273            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
274        }
275    }
276
277    deserializer.deserialize_any(StringOrStruct(PhantomData))
278}