Skip to main content

aster/
recipe_deeplink.rs

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    // Handle the current format: URL-safe Base64 without padding.
21    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    // Handle legacy formats of 'standard base64 encoded' and standard base64 encoded that was then url encoded.
30    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}