octra-sqlite 0.5.2

Real SQLite inside an Octra Circle, with a Rust CLI and client library
Documentation
use super::error::{Error, Result};
use serde::{Deserialize, Serialize};

/// Read authentication mode for a database target.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum ReadMode {
    /// Probe Circle metadata before choosing signed or unsigned reads.
    Auto,
    /// Signed Octra view auth reads.
    #[default]
    Sealed,
    /// Unsigned public Circle view reads.
    Public,
}

impl ReadMode {
    pub fn as_str(self) -> &'static str {
        match self {
            ReadMode::Auto => "auto",
            ReadMode::Sealed => "sealed",
            ReadMode::Public => "public",
        }
    }

    pub fn allows_unsigned_read(self) -> bool {
        matches!(self, ReadMode::Auto | ReadMode::Public)
    }
}

/// Parsed `oct://` target plus read mode and RPC context.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DatabaseTarget {
    pub raw: String,
    pub network: String,
    pub circle: String,
    pub rpc: String,
    pub read_mode: ReadMode,
}

pub fn parse_database_target(
    value: &str,
    default_network: Option<&str>,
    default_rpc: Option<&str>,
) -> Result<DatabaseTarget> {
    let default_rpc = default_rpc.unwrap_or_default().to_string();
    if let Some(rest) = value.strip_prefix("oct://") {
        let without_query = rest.split('?').next().unwrap_or(rest);
        let pieces: Vec<&str> = without_query
            .trim_matches('/')
            .split('/')
            .filter(|p| !p.is_empty())
            .collect();
        let (network, circle) = match pieces.as_slice() {
            [circle] => (
                default_network
                    .ok_or_else(|| Error::new("network is required for oct://<circle-id> URIs"))?
                    .to_string(),
                (*circle).to_string(),
            ),
            [network, circle] => ((*network).to_string(), (*circle).to_string()),
            _ => {
                return Err(Error::new(
                    "oct database URI must look like oct://NETWORK/<circle-id>",
                ));
            }
        };
        if !circle.starts_with("oct") {
            return Err(Error::new("Circle ID must start with oct"));
        }
        let read_mode = read_mode_from_query(rest)?;
        return Ok(DatabaseTarget {
            raw: value.to_string(),
            network,
            circle,
            rpc: default_rpc,
            read_mode,
        });
    }
    if value.starts_with("oct") {
        return Ok(DatabaseTarget {
            raw: value.to_string(),
            network: default_network
                .ok_or_else(|| Error::new("network is required for bare Circle IDs"))?
                .to_string(),
            circle: value.to_string(),
            rpc: default_rpc,
            read_mode: ReadMode::Auto,
        });
    }
    Err(Error::new(format!(
        "unknown database {value}; use a database name, Circle ID, or oct://NETWORK/<circle-id>"
    )))
}

fn read_mode_from_query(rest: &str) -> Result<ReadMode> {
    let Some((_, query)) = rest.split_once('?') else {
        return Ok(ReadMode::Auto);
    };
    for pair in query.split('&') {
        let Some((key, value)) = pair.split_once('=') else {
            continue;
        };
        if key == "read_mode" {
            return match value {
                "auto" => Ok(ReadMode::Auto),
                "sealed" => Ok(ReadMode::Sealed),
                "public" => Ok(ReadMode::Public),
                _ => Err(Error::new("read_mode must be auto, sealed, or public")),
            };
        }
    }
    Ok(ReadMode::Auto)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_oct_database_target() {
        let target =
            parse_database_target("oct://devnet/octABC", None, Some("http://rpc")).unwrap();
        assert_eq!(target.network, "devnet");
        assert_eq!(target.circle, "octABC");
        assert_eq!(target.rpc, "http://rpc");
        assert_eq!(target.read_mode, ReadMode::Auto);
    }

    #[test]
    fn bare_circle_uses_default_network() {
        let target = parse_database_target("octABC", Some("devnet"), None).unwrap();
        assert_eq!(target.network, "devnet");
        assert_eq!(target.circle, "octABC");
        assert_eq!(target.read_mode, ReadMode::Auto);
    }

    #[test]
    fn parses_read_mode_query() {
        let target =
            parse_database_target("oct://devnet/octABC?read_mode=public", None, None).unwrap();
        assert_eq!(target.read_mode, ReadMode::Public);
    }
}