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}