tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 5d9c5fc540cc2a53ec68131cfe85fdffa61054dc
parent 6cfa491458464a233fb8769483d25a1b7c9fc166
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 22:24:03 -0700

store-surreal: add surreal connection config

Diffstat:
MCargo.lock | 4++++
MCargo.toml | 1+
Acrates/tangle_store_surreal/Cargo.toml | 13+++++++++++++
Acrates/tangle_store_surreal/src/lib.rs | 206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 224 insertions(+), 0 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -419,6 +419,10 @@ dependencies = [ ] [[package]] +name = "tangle_store_surreal" +version = "0.1.0" + +[[package]] name = "tangle_test_support" version = "0.1.0" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/tangle_nips", "crates/tangle_protocol", "crates/tangle_store", + "crates/tangle_store_surreal", "crates/tangle_test_support", ] resolver = "2" diff --git a/crates/tangle_store_surreal/Cargo.toml b/crates/tangle_store_surreal/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tangle_store_surreal" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +description = "SurrealDB storage backend for tangle" + +[dependencies] + +[lints] +workspace = true diff --git a/crates/tangle_store_surreal/src/lib.rs b/crates/tangle_store_surreal/src/lib.rs @@ -0,0 +1,206 @@ +#![forbid(unsafe_code)] + +use core::fmt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SurrealConnectionMode { + Memory, + Http { endpoint: String }, + WebSocket { endpoint: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SurrealConnectionConfig { + mode: SurrealConnectionMode, + namespace: String, + database: String, +} + +impl SurrealConnectionConfig { + pub fn memory(namespace: &str, database: &str) -> Result<Self, SurrealConfigError> { + Self::new(SurrealConnectionMode::Memory, namespace, database) + } + + pub fn http( + endpoint: &str, + namespace: &str, + database: &str, + ) -> Result<Self, SurrealConfigError> { + let endpoint = normalized_endpoint(endpoint, "http endpoint")?; + Self::new( + SurrealConnectionMode::Http { endpoint }, + namespace, + database, + ) + } + + pub fn websocket( + endpoint: &str, + namespace: &str, + database: &str, + ) -> Result<Self, SurrealConfigError> { + let endpoint = normalized_endpoint(endpoint, "websocket endpoint")?; + Self::new( + SurrealConnectionMode::WebSocket { endpoint }, + namespace, + database, + ) + } + + pub fn mode(&self) -> &SurrealConnectionMode { + &self.mode + } + + pub fn namespace(&self) -> &str { + &self.namespace + } + + pub fn database(&self) -> &str { + &self.database + } + + fn new( + mode: SurrealConnectionMode, + namespace: &str, + database: &str, + ) -> Result<Self, SurrealConfigError> { + Ok(Self { + mode, + namespace: normalized_identifier(namespace, "namespace")?, + database: normalized_identifier(database, "database")?, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SurrealConfigError { + message: String, +} + +impl SurrealConfigError { + fn new(message: &str) -> Self { + Self { + message: message.to_owned(), + } + } + + pub fn message(&self) -> &str { + &self.message + } +} + +impl fmt::Display for SurrealConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) + } +} + +impl std::error::Error for SurrealConfigError {} + +fn normalized_identifier(value: &str, field: &str) -> Result<String, SurrealConfigError> { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(SurrealConfigError::new(&format!( + "surreal {field} must not be empty" + ))); + } + if !trimmed + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') + { + return Err(SurrealConfigError::new(&format!( + "surreal {field} must use ASCII letters, digits, or underscore" + ))); + } + Ok(trimmed.to_owned()) +} + +fn normalized_endpoint(value: &str, field: &str) -> Result<String, SurrealConfigError> { + let trimmed = value.trim(); + if trimmed.is_empty() { + return Err(SurrealConfigError::new(&format!( + "surreal {field} must not be empty" + ))); + } + Ok(trimmed.to_owned()) +} + +#[cfg(test)] +mod tests { + use super::{SurrealConfigError, SurrealConnectionConfig, SurrealConnectionMode}; + + #[test] + fn memory_config_normalizes_namespace_and_database() { + let config = + SurrealConnectionConfig::memory(" tangle_test ", " relay_one ").expect("memory config"); + + assert_eq!(config.mode(), &SurrealConnectionMode::Memory); + assert_eq!(config.namespace(), "tangle_test"); + assert_eq!(config.database(), "relay_one"); + } + + #[test] + fn remote_config_preserves_trimmed_endpoints() { + let http = SurrealConnectionConfig::http(" http://127.0.0.1:8000 ", "ns", "db") + .expect("http config"); + let websocket = SurrealConnectionConfig::websocket(" ws://127.0.0.1:8000 ", "ns", "db") + .expect("websocket config"); + + assert_eq!( + http.mode(), + &SurrealConnectionMode::Http { + endpoint: "http://127.0.0.1:8000".to_owned() + } + ); + assert_eq!( + websocket.mode(), + &SurrealConnectionMode::WebSocket { + endpoint: "ws://127.0.0.1:8000".to_owned() + } + ); + } + + #[test] + fn config_rejects_empty_namespace_database_and_endpoint() { + assert_eq!( + SurrealConnectionConfig::memory(" ", "db") + .expect_err("namespace error") + .to_string(), + "surreal namespace must not be empty" + ); + assert_eq!( + SurrealConnectionConfig::memory("ns", "") + .expect_err("database error") + .message(), + "surreal database must not be empty" + ); + assert_eq!( + SurrealConnectionConfig::http("", "ns", "db").expect_err("endpoint error"), + SurrealConfigError { + message: "surreal http endpoint must not be empty".to_owned() + } + ); + assert_eq!( + SurrealConnectionConfig::websocket(" ", "ns", "db") + .expect_err("websocket endpoint error") + .to_string(), + "surreal websocket endpoint must not be empty" + ); + } + + #[test] + fn config_rejects_non_portable_identifiers() { + assert_eq!( + SurrealConnectionConfig::memory("tangle-test", "db") + .expect_err("namespace syntax") + .to_string(), + "surreal namespace must use ASCII letters, digits, or underscore" + ); + assert_eq!( + SurrealConnectionConfig::memory("ns", "relay.db") + .expect_err("database syntax") + .to_string(), + "surreal database must use ASCII letters, digits, or underscore" + ); + } +}