octra_sqlite/protocol/
target.rs1use super::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
6#[serde(rename_all = "snake_case")]
7pub enum ReadMode {
8 Auto,
10 #[default]
12 Sealed,
13 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#[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}