1use std::{borrow::Cow, marker::PhantomData};
2
3use serde::{Serialize, de::DeserializeOwned};
4
5use crate::{Cookie, Cookies};
6
7pub struct CookieStore<T, J> {
46 jar: J,
47 key: Cow<'static, str>,
48 value: T,
49}
50
51impl<T, J> CookieStore<T, J>
52where
53 T: Serialize + DeserializeOwned,
54 J: Cookies,
55{
56 pub fn new(jar: J, key: impl Into<Cow<'static, str>>, value: T) -> Self {
61 Self {
62 jar,
63 key: key.into(),
64 value,
65 }
66 }
67
68 pub fn read(&self) -> &T {
70 &self.value
71 }
72
73 pub fn get(&self) -> T
75 where
76 T: Clone,
77 {
78 self.value.clone()
79 }
80
81 #[must_use]
85 pub fn set(mut self, value: T) -> Self {
86 self.value = value;
87 self
88 }
89
90 #[must_use]
115 pub fn update<F>(mut self, f: F) -> Self
116 where
117 F: FnOnce(&mut T),
118 {
119 f(&mut self.value);
120 self
121 }
122
123 pub fn commit(self) -> Result<T, serde_json::Error> {
132 let serialized = serde_json::to_string(&self.value)?;
133 self.jar.add(Cookie::new(self.key, serialized));
134 Ok(self.value)
135 }
136
137 pub fn rollback(self) {}
142
143 pub fn remove(self) {
149 self.jar.remove(Cookie::new(self.key, ""));
150 }
151}
152
153pub struct UnparsedCookieStore<T, J> {
158 jar: J,
159 key: Cow<'static, str>,
160 _marker: PhantomData<fn() -> T>,
161}
162
163impl<T, J> UnparsedCookieStore<T, J>
164where
165 T: Serialize + DeserializeOwned,
166 J: Cookies,
167{
168 pub fn new(jar: J, key: impl Into<Cow<'static, str>>) -> Self {
172 Self {
173 jar,
174 key: key.into(),
175 _marker: PhantomData,
176 }
177 }
178
179 pub fn set(self, value: T) -> CookieStore<T, J> {
203 CookieStore::new(self.jar, self.key, value)
204 }
205
206 pub fn remove(self) {
210 self.jar.remove(Cookie::new(self.key, ""));
211 }
212
213 pub fn parse(self) -> Result<Option<CookieStore<T, J>>, serde_json::Error> {
223 match self.jar.get(&self.key) {
224 Some(cookie) => {
225 let value = serde_json::from_str(cookie.value())?;
226 Ok(Some(CookieStore::new(self.jar, self.key, value)))
227 }
228 None => Ok(None),
229 }
230 }
231
232 pub fn parse_or(self, default: T) -> CookieStore<T, J> {
235 self.parse_or_else(move || default)
236 }
237
238 pub fn parse_or_else<F>(self, f: F) -> CookieStore<T, J>
241 where
242 F: FnOnce() -> T,
243 {
244 let value = match self.jar.get(&self.key) {
245 Some(cookie) => serde_json::from_str(cookie.value()).unwrap_or_else(|_| f()),
246 None => f(),
247 };
248 CookieStore::new(self.jar, self.key, value)
249 }
250
251 pub fn parse_or_default(self) -> CookieStore<T, J>
254 where
255 T: Default,
256 {
257 self.parse_or_else(T::default)
258 }
259}
260
261pub fn cookie_store<T, J>(jar: J, key: impl Into<Cow<'static, str>>) -> UnparsedCookieStore<T, J>
283where
284 T: Serialize + DeserializeOwned,
285 J: Cookies,
286{
287 UnparsedCookieStore::new(jar, key)
288}
289
290#[cfg(test)]
291mod tests {
292 use std::{cell::RefCell, collections::HashMap};
293
294 use http::{HeaderMap, Request, header};
295 use serde::{Deserialize, Serialize};
296 use topcoat_core::context::{Cx, CxTestBuilder};
297
298 use super::*;
299 use crate::{CookieJarCell, Key, cookies, write_cookies};
300
301 #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
302 struct Cart {
303 items: Vec<String>,
304 }
305
306 #[derive(Default)]
309 struct MockJar {
310 incoming: HashMap<String, String>,
311 added: RefCell<Vec<Cookie<'static>>>,
312 removed: RefCell<Vec<Cookie<'static>>>,
313 }
314
315 impl MockJar {
316 fn with(pairs: &[(&str, &str)]) -> Self {
317 Self {
318 incoming: pairs
319 .iter()
320 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
321 .collect(),
322 ..Self::default()
323 }
324 }
325
326 fn added(&self) -> Vec<Cookie<'static>> {
327 self.added.borrow().clone()
328 }
329
330 fn removed(&self) -> Vec<Cookie<'static>> {
331 self.removed.borrow().clone()
332 }
333 }
334
335 impl Cookies for &MockJar {
336 fn get(&self, name: &str) -> Option<Cookie<'static>> {
337 self.incoming
338 .get(name)
339 .map(|value| Cookie::new(name.to_owned(), value.clone()))
340 }
341
342 fn add<C: Into<Cookie<'static>>>(&self, cookie: C) {
343 self.added.borrow_mut().push(cookie.into());
344 }
345
346 fn remove<C: Into<Cookie<'static>>>(&self, cookie: C) {
347 self.removed.borrow_mut().push(cookie.into());
348 }
349 }
350
351 #[test]
352 fn parse_reads_existing_value() {
353 let jar = MockJar::with(&[("cart", r#"{"items":["a","b"]}"#)]);
354 let store = cookie_store::<Cart, _>(&jar, "cart")
355 .parse()
356 .unwrap()
357 .expect("cookie is present");
358
359 assert_eq!(store.read().items, ["a", "b"]);
360 }
361
362 #[test]
363 fn parse_returns_none_when_absent() {
364 let jar = MockJar::with(&[]);
365 assert!(
366 cookie_store::<Cart, _>(&jar, "cart")
367 .parse()
368 .unwrap()
369 .is_none()
370 );
371 }
372
373 #[test]
374 fn parse_errors_on_malformed() {
375 let jar = MockJar::with(&[("cart", "not json")]);
376 assert!(cookie_store::<Cart, _>(&jar, "cart").parse().is_err());
377 }
378
379 #[test]
380 fn parse_or_default_falls_back_when_absent_or_malformed() {
381 let absent = MockJar::with(&[]);
382 assert_eq!(
383 cookie_store::<Cart, _>(&absent, "cart")
384 .parse_or_default()
385 .get(),
386 Cart::default()
387 );
388
389 let malformed = MockJar::with(&[("cart", "not json")]);
391 assert_eq!(
392 cookie_store::<Cart, _>(&malformed, "cart")
393 .parse_or_default()
394 .get(),
395 Cart::default()
396 );
397 }
398
399 #[test]
400 fn parse_or_and_parse_or_else_use_their_fallbacks() {
401 let jar = MockJar::with(&[]);
402 let from_value = cookie_store::<Cart, _>(&jar, "cart").parse_or(Cart {
403 items: vec!["x".to_owned()],
404 });
405 assert_eq!(from_value.read().items, ["x"]);
406
407 let from_closure = cookie_store::<Cart, _>(&jar, "cart").parse_or_else(|| Cart {
408 items: vec!["y".to_owned()],
409 });
410 assert_eq!(from_closure.read().items, ["y"]);
411 }
412
413 #[test]
414 fn commit_writes_serialized_cookie_and_returns_value() {
415 let jar = MockJar::with(&[]);
416 let cart = cookie_store::<Cart, _>(&jar, "cart")
417 .parse_or_default()
418 .update(|cart| cart.items.push("widget".to_owned()))
419 .commit()
420 .unwrap();
421
422 assert_eq!(cart.items, ["widget"]);
424
425 let added = jar.added();
427 assert_eq!(added.len(), 1);
428 assert_eq!(added[0].name(), "cart");
429 assert_eq!(added[0].value(), r#"{"items":["widget"]}"#);
430 }
431
432 #[test]
433 fn get_clones_and_read_borrows() {
434 let jar = MockJar::with(&[("cart", r#"{"items":["a"]}"#)]);
435 let store = cookie_store::<Cart, _>(&jar, "cart").parse_or_default();
436
437 assert_eq!(store.read().items, ["a"]);
438 assert_eq!(store.get(), *store.read());
439 }
440
441 #[test]
442 fn nothing_is_written_without_commit() {
443 let jar = MockJar::with(&[]);
444 let _ = cookie_store::<Cart, _>(&jar, "cart")
445 .parse_or_default()
446 .update(|cart| cart.items.push("z".to_owned()));
447
448 assert_eq!(jar.added(), Vec::<Cookie<'_>>::new());
449 }
450
451 #[test]
452 fn rollback_writes_nothing() {
453 let jar = MockJar::with(&[]);
454 cookie_store::<Cart, _>(&jar, "cart")
455 .parse_or_default()
456 .update(|cart| cart.items.push("z".to_owned()))
457 .rollback();
458
459 assert_eq!(jar.added(), Vec::<Cookie<'_>>::new());
460 }
461
462 #[test]
463 fn unparsed_set_overwrites_without_reading() {
464 let jar = MockJar::with(&[("cart", r#"{"items":["old"]}"#)]);
465 cookie_store::<Cart, _>(&jar, "cart")
466 .set(Cart {
467 items: vec!["new".to_owned()],
468 })
469 .commit()
470 .unwrap();
471
472 assert_eq!(jar.added()[0].value(), r#"{"items":["new"]}"#);
473 }
474
475 #[test]
476 fn remove_queues_a_removal() {
477 let parsed = MockJar::with(&[("cart", r#"{"items":[]}"#)]);
479 cookie_store::<Cart, _>(&parsed, "cart")
480 .parse_or_default()
481 .remove();
482 let removed = parsed.removed();
483 assert_eq!(removed.len(), 1);
484 assert_eq!(removed[0].name(), "cart");
485 assert_eq!(parsed.added(), Vec::<Cookie<'_>>::new());
486
487 let unparsed = MockJar::with(&[]);
489 cookie_store::<Cart, _>(&unparsed, "cart").remove();
490 assert_eq!(unparsed.removed()[0].name(), "cart");
491 }
492
493 fn cx_with(cookie_headers: &[&str]) -> Cx {
495 let mut builder = Request::builder();
496 for value in cookie_headers {
497 builder = builder.header(header::COOKIE, *value);
498 }
499 let (parts, ()) = builder.body(()).unwrap().into_parts();
500
501 CxTestBuilder::new()
502 .request_context(parts)
503 .request_context(CookieJarCell::new())
504 .build()
505 }
506
507 fn echoed_pair(cx: &Cx) -> String {
509 let mut headers = HeaderMap::new();
510 write_cookies(cx, &mut headers);
511 let set = headers
512 .get(header::SET_COOKIE)
513 .unwrap()
514 .to_str()
515 .unwrap()
516 .to_owned();
517 set.split(';').next().unwrap().to_owned()
518 }
519
520 #[test]
521 fn round_trips_through_a_signed_jar() {
522 let key = Key::generate();
525
526 let writer = cx_with(&[]);
527 cookie_store::<Cart, _>(cookies(&writer).signed(&key), "cart")
528 .set(Cart {
529 items: vec!["widget".to_owned()],
530 })
531 .commit()
532 .unwrap();
533 let echoed = echoed_pair(&writer);
534
535 let reader = cx_with(&[&echoed]);
536 let cart = cookie_store::<Cart, _>(cookies(&reader).signed(&key), "cart")
537 .parse()
538 .unwrap()
539 .expect("the signed cookie should verify and deserialize");
540
541 assert_eq!(cart.read().items, ["widget"]);
542 }
543}