1use anyhow::Result;
2use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
3use thiserror::Error;
4
5use crate::recipe::Recipe;
6
7#[derive(Error, Debug)]
8pub enum DecodeError {
9 #[error("Failed to decode recipe deeplink")]
10 AllMethodsFailed,
11}
12
13pub fn encode(recipe: &Recipe) -> Result<String, serde_json::Error> {
14 let recipe_json = serde_json::to_string(recipe)?;
15 let encoded = URL_SAFE_NO_PAD.encode(recipe_json.as_bytes());
16 Ok(encoded)
17}
18
19pub fn decode(link: &str) -> Result<Recipe, DecodeError> {
20 if let Ok(decoded_bytes) = URL_SAFE_NO_PAD.decode(link) {
22 if let Ok(recipe_json) = String::from_utf8(decoded_bytes) {
23 if let Ok(recipe) = serde_json::from_str::<Recipe>(&recipe_json) {
24 return Ok(recipe);
25 }
26 }
27 }
28
29 if let Ok(url_decoded) = urlencoding::decode(link) {
31 if let Ok(decoded_bytes) =
32 base64::engine::general_purpose::STANDARD.decode(url_decoded.as_bytes())
33 {
34 if let Ok(recipe_json) = String::from_utf8(decoded_bytes) {
35 if let Ok(recipe) = serde_json::from_str::<Recipe>(&recipe_json) {
36 return Ok(recipe);
37 }
38 }
39 }
40 }
41
42 Err(DecodeError::AllMethodsFailed)
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::recipe::Recipe;
49
50 fn create_test_recipe() -> Recipe {
51 Recipe::builder()
52 .title("Test Recipe")
53 .description("A test recipe for deeplink encoding/decoding")
54 .instructions("Act as a helpful assistant")
55 .build()
56 .expect("Failed to build test recipe")
57 }
58
59 #[test]
60 fn test_encode_decode_round_trip() {
61 let original_recipe = create_test_recipe();
62
63 let encoded = encode(&original_recipe).expect("Failed to encode recipe");
64 assert!(!encoded.is_empty());
65
66 let decoded_recipe = decode(&encoded).expect("Failed to decode recipe");
67
68 assert_eq!(original_recipe.title, decoded_recipe.title);
69 assert_eq!(original_recipe.description, decoded_recipe.description);
70 assert_eq!(original_recipe.instructions, decoded_recipe.instructions);
71 assert_eq!(original_recipe.version, decoded_recipe.version);
72 }
73
74 #[test]
75 fn test_decode_legacy_standard_base64() {
76 let recipe = create_test_recipe();
77 let recipe_json = serde_json::to_string(&recipe).unwrap();
78 let legacy_encoded =
79 base64::engine::general_purpose::STANDARD.encode(recipe_json.as_bytes());
80
81 let decoded_recipe = decode(&legacy_encoded).expect("Failed to decode legacy format");
82 assert_eq!(recipe.title, decoded_recipe.title);
83 assert_eq!(recipe.description, decoded_recipe.description);
84 assert_eq!(recipe.instructions, decoded_recipe.instructions);
85 }
86
87 #[test]
88 fn test_decode_legacy_url_encoded_base64() {
89 let recipe = create_test_recipe();
90 let recipe_json = serde_json::to_string(&recipe).unwrap();
91 let base64_encoded =
92 base64::engine::general_purpose::STANDARD.encode(recipe_json.as_bytes());
93 let url_encoded = urlencoding::encode(&base64_encoded);
94
95 let decoded_recipe =
96 decode(&url_encoded).expect("Failed to decode URL-encoded legacy format");
97 assert_eq!(recipe.title, decoded_recipe.title);
98 assert_eq!(recipe.description, decoded_recipe.description);
99 assert_eq!(recipe.instructions, decoded_recipe.instructions);
100 }
101
102 #[test]
103 fn test_decode_invalid_input() {
104 let result = decode("invalid_base64!");
105 assert!(result.is_err());
106 assert!(matches!(result.unwrap_err(), DecodeError::AllMethodsFailed));
107 }
108}