Skip to main content

xurl/store/
migration.rs

1//! Legacy format migration — JSON and `.twurlrc` import.
2
3use std::collections::BTreeMap;
4
5use serde::Deserialize;
6
7use super::TokenStore;
8use super::types::{App, OAuth1Token, StoreFile, Token, TokenType};
9use crate::error::Result;
10
11// ── Legacy JSON structure (for migration) ────────────────────────────
12
13#[derive(Debug, Deserialize)]
14struct LegacyStore {
15    oauth2_tokens: Option<BTreeMap<String, Token>>,
16    #[serde(rename = "oauth1_tokens")]
17    oauth1_token: Option<Token>,
18    bearer_token: Option<Token>,
19}
20
21// ── Twurlrc structures ──────────────────────────────────────────────
22
23#[derive(Debug, Deserialize)]
24#[allow(dead_code)] // Fields populated by serde deserialization
25struct TwurlrcProfile {
26    username: Option<String>,
27    consumer_key: Option<String>,
28    consumer_secret: Option<String>,
29    token: Option<String>,
30    secret: Option<String>,
31}
32
33#[derive(Debug, Deserialize)]
34struct TwurlrcConfig {
35    profiles: Option<BTreeMap<String, BTreeMap<String, TwurlrcProfile>>>,
36    bearer_tokens: Option<BTreeMap<String, String>>,
37}
38
39// ── Migration methods ───────────────────────────────────────────────
40
41impl TokenStore {
42    /// Tries YAML first, then falls back to legacy JSON migration.
43    pub(crate) fn load_from_data(&mut self, data: &[u8]) {
44        // Try new YAML format first
45        if let Ok(sf) = serde_yaml::from_slice::<StoreFile>(data)
46            && !sf.apps.is_empty()
47        {
48            self.apps = sf.apps;
49            self.default_app = sf.default_app;
50            return;
51        }
52
53        // Fall back to legacy JSON
54        if let Ok(legacy) = serde_json::from_slice::<LegacyStore>(data) {
55            let app = App {
56                client_id: String::new(),
57                client_secret: String::new(),
58                default_user: String::new(),
59                redirect_uri: String::new(),
60                oauth2_tokens: legacy.oauth2_tokens.unwrap_or_default(),
61                oauth1_token: legacy.oauth1_token,
62                bearer_token: legacy.bearer_token,
63                unnamed_oauth2_token: None,
64            };
65            self.apps.insert("default".to_string(), app);
66            self.default_app = "default".to_string();
67            // Persist in new YAML format immediately
68            let _ = self.save_to_file();
69        }
70    }
71
72    /// Imports tokens from a `.twurlrc` file into the active app.
73    ///
74    /// # Errors
75    ///
76    /// Returns an error if the file cannot be read, parsed, or the store cannot be saved.
77    pub fn import_from_twurlrc(&mut self, file_path: &std::path::Path) -> Result<()> {
78        let data = std::fs::read(file_path)?;
79        let twurlrc: TwurlrcConfig = serde_yaml::from_slice(&data)?;
80
81        let app = self.active_app_or_create();
82
83        // Import the first OAuth1 tokens from twurlrc
84        if let Some(profiles) = &twurlrc.profiles {
85            'outer: for consumer_keys in profiles.values() {
86                if let Some((consumer_key, profile)) = consumer_keys.iter().next() {
87                    if app.oauth1_token.is_none() {
88                        app.oauth1_token = Some(Token {
89                            token_type: TokenType::Oauth1,
90                            bearer: None,
91                            oauth2: None,
92                            oauth1: Some(OAuth1Token {
93                                access_token: profile.token.clone().unwrap_or_default(),
94                                token_secret: profile.secret.clone().unwrap_or_default(),
95                                consumer_key: consumer_key.clone(),
96                                consumer_secret: profile
97                                    .consumer_secret
98                                    .clone()
99                                    .unwrap_or_default(),
100                            }),
101                        });
102                    }
103                    break 'outer;
104                }
105            }
106        }
107
108        // Import the first bearer token from twurlrc
109        if let Some(bearer_tokens) = &twurlrc.bearer_tokens
110            && let Some(bearer_token) = bearer_tokens.values().next()
111        {
112            app.bearer_token = Some(Token {
113                token_type: TokenType::Bearer,
114                bearer: Some(bearer_token.clone()),
115                oauth2: None,
116                oauth1: None,
117            });
118        }
119
120        self.save_to_file()
121    }
122}