Skip to main content

octra_sqlite/protocol/
target.rs

1use super::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3
4/// Read authentication mode for a database target.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum ReadMode {
8    /// Probe Circle metadata before choosing signed or unsigned reads.
9    Auto,
10    /// Signed Octra view auth reads.
11    #[default]
12    Sealed,
13    /// Unsigned public Circle view reads.
14    Public,
15}
16
17impl ReadMode {
18    pub fn as_str(self) -> &'static str {
19        match self {
20            ReadMode::Auto => "auto",
21            ReadMode::Sealed => "sealed",
22            ReadMode::Public => "public",
23        }
24    }
25
26    pub fn allows_unsigned_read(self) -> bool {
27        matches!(self, ReadMode::Auto | ReadMode::Public)
28    }
29}
30
31/// Parsed `oct://` target plus read mode and RPC context.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct DatabaseTarget {
34    pub raw: String,
35    pub network: String,
36    pub circle: String,
37    pub rpc: String,
38    pub read_mode: ReadMode,
39}
40
41pub fn parse_database_target(
42    value: &str,
43    default_network: Option<&str>,
44    default_rpc: Option<&str>,
45) -> Result<DatabaseTarget> {
46    let default_rpc = default_rpc.unwrap_or_default().to_string();
47    if let Some(rest) = value.strip_prefix("oct://") {
48        let without_query = rest.split('?').next().unwrap_or(rest);
49        let pieces: Vec<&str> = without_query
50            .trim_matches('/')
51            .split('/')
52            .filter(|p| !p.is_empty())
53            .collect();
54        let (network, circle) = match pieces.as_slice() {
55            [circle] => (
56                default_network
57                    .ok_or_else(|| Error::new("network is required for oct://<circle-id> URIs"))?
58                    .to_string(),
59                (*circle).to_string(),
60            ),
61            [network, circle] => ((*network).to_string(), (*circle).to_string()),
62            _ => {
63                return Err(Error::new(
64                    "oct database URI must look like oct://NETWORK/<circle-id>",
65                ));
66            }
67        };
68        if !circle.starts_with("oct") {
69            return Err(Error::new("Circle ID must start with oct"));
70        }
71        let read_mode = read_mode_from_query(rest)?;
72        return Ok(DatabaseTarget {
73            raw: value.to_string(),
74            network,
75            circle,
76            rpc: default_rpc,
77            read_mode,
78        });
79    }
80    if value.starts_with("oct") {
81        return Ok(DatabaseTarget {
82            raw: value.to_string(),
83            network: default_network
84                .ok_or_else(|| Error::new("network is required for bare Circle IDs"))?
85                .to_string(),
86            circle: value.to_string(),
87            rpc: default_rpc,
88            read_mode: ReadMode::Auto,
89        });
90    }
91    Err(Error::new(format!(
92        "unknown database {value}; use a database name, Circle ID, or oct://NETWORK/<circle-id>"
93    )))
94}
95
96fn read_mode_from_query(rest: &str) -> Result<ReadMode> {
97    let Some((_, query)) = rest.split_once('?') else {
98        return Ok(ReadMode::Auto);
99    };
100    for pair in query.split('&') {
101        let Some((key, value)) = pair.split_once('=') else {
102            continue;
103        };
104        if key == "read_mode" {
105            return match value {
106                "auto" => Ok(ReadMode::Auto),
107                "sealed" => Ok(ReadMode::Sealed),
108                "public" => Ok(ReadMode::Public),
109                _ => Err(Error::new("read_mode must be auto, sealed, or public")),
110            };
111        }
112    }
113    Ok(ReadMode::Auto)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn parses_oct_database_target() {
122        let target =
123            parse_database_target("oct://devnet/octABC", None, Some("http://rpc")).unwrap();
124        assert_eq!(target.network, "devnet");
125        assert_eq!(target.circle, "octABC");
126        assert_eq!(target.rpc, "http://rpc");
127        assert_eq!(target.read_mode, ReadMode::Auto);
128    }
129
130    #[test]
131    fn bare_circle_uses_default_network() {
132        let target = parse_database_target("octABC", Some("devnet"), None).unwrap();
133        assert_eq!(target.network, "devnet");
134        assert_eq!(target.circle, "octABC");
135        assert_eq!(target.read_mode, ReadMode::Auto);
136    }
137
138    #[test]
139    fn parses_read_mode_query() {
140        let target =
141            parse_database_target("oct://devnet/octABC?read_mode=public", None, None).unwrap();
142        assert_eq!(target.read_mode, ReadMode::Public);
143    }
144}