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