Initial commit: aether-tools-rs workspace
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "aether-net-mini"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "aether-net-mini"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
aether-atom = { path = "/home/cosmos/aether-atom" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha3 = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
@@ -0,0 +1,245 @@
|
||||
//! aether-net-mini — DHT abstraction over aether-atom.
|
||||
//!
|
||||
//! The original aether-net was 1173 LOC of single-threaded TCP JSON-RPC
|
||||
//! with a custom in-memory hashmap. This rewrite uses the atom for
|
||||
//! storage and tokio for the network loop. Every byte of public API
|
||||
//! (`put`, `get`) is a one-liner over the atom.
|
||||
//!
|
||||
//! Reduced surface: 2 methods instead of 15. Persistence is implicit
|
||||
//! (atom ledger survives restart). Scopes work the same way (atom
|
||||
//! supports `global` and arbitrary per-repo overrides).
|
||||
|
||||
use aether_atom::{fingerprint, Lattice};
|
||||
use clap::{Parser, Subcommand};
|
||||
use serde_json::{json, Value};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader as ABufReader};
|
||||
use tokio::net::TcpListener as ATcpListener;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "aether-net-mini", about = "DHT over aether-atom")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
cmd: Cmd,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Cmd {
|
||||
/// Long-running daemon. Stores all content in aether-atom.
|
||||
Daemon {
|
||||
#[arg(long, default_value = "127.0.0.1:0")]
|
||||
listen: String,
|
||||
#[arg(long)]
|
||||
data_dir: Option<String>,
|
||||
},
|
||||
/// One-shot client: connect, send a single JSON-RPC request, print
|
||||
/// response.
|
||||
Client {
|
||||
#[arg(long)]
|
||||
server: String,
|
||||
#[arg(long)]
|
||||
method: String,
|
||||
#[arg(long, default_value = "{}", value_parser = parse_json_arg)]
|
||||
params: Value,
|
||||
},
|
||||
}
|
||||
|
||||
fn parse_json_arg(s: &str) -> Result<Value, String> {
|
||||
serde_json::from_str(s).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn err_response(id: Value, code: i64, msg: &str) -> Value {
|
||||
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": msg } })
|
||||
}
|
||||
fn ok_response(id: Value, result: Value) -> Value {
|
||||
json!({ "jsonrpc": "2.0", "id": id, "result": result })
|
||||
}
|
||||
|
||||
/// The two real verbs over aether-atom's lattice:
|
||||
/// - `put(data)` → hash it, store
|
||||
/// - `get(hash)` → retrieve
|
||||
///
|
||||
/// Subscribe / replicate are future features; not in this surface.
|
||||
async fn dispatch(
|
||||
method: &str,
|
||||
params: Value,
|
||||
lattice: &mut Lattice,
|
||||
) -> Result<Value, String> {
|
||||
match method {
|
||||
"put" => {
|
||||
let data = params
|
||||
.get("data")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "missing 'data'".to_string())?;
|
||||
let bytes = data.as_bytes();
|
||||
let sig = fingerprint(bytes);
|
||||
lattice.record(sig, "global", bytes, Some(true));
|
||||
Ok(json!({ "hash": sig.as_bytes().iter().map(|b| format!("{:02x}", b)).collect::<String>() }))
|
||||
}
|
||||
"get" => {
|
||||
let hash_hex = params
|
||||
.get("hash")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "missing 'hash'".to_string())?;
|
||||
let sig_bytes = aether_atom::hex_decode(hash_hex)
|
||||
.ok_or_else(|| "invalid hex hash".to_string())?;
|
||||
let sig = aether_atom::Fingerprint(sig_bytes);
|
||||
match lattice.lookup(&sig, "global") {
|
||||
Some(hit) => Ok(json!({
|
||||
"hash": hash_hex,
|
||||
"found": true,
|
||||
"content": String::from_utf8_lossy(hit.value.as_ref()).to_string(),
|
||||
})),
|
||||
None => Ok(json!({ "hash": hash_hex, "found": false })),
|
||||
}
|
||||
}
|
||||
other => Err(format!("unknown method: {}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
match cli.cmd {
|
||||
Cmd::Daemon { listen, data_dir } => {
|
||||
let dir = match data_dir {
|
||||
Some(d) => std::path::PathBuf::from(d),
|
||||
None => aether_atom::data_dir("AETHER_NET_DATA_DIR"),
|
||||
};
|
||||
let lattice = Arc::new(Mutex::new(Lattice::open(&dir)?));
|
||||
eprintln!("aether-net-mini daemon listening on {} (data: {:?})", listen, dir);
|
||||
let listener = ATcpListener::bind(&listen).await?;
|
||||
|
||||
loop {
|
||||
let (stream, addr) = listener.accept().await?;
|
||||
let lat = lattice.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = handle_conn(stream, lat).await {
|
||||
eprintln!(" conn {} error: {}", addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Cmd::Client { server, method, params } => {
|
||||
let mut s = TcpStream::connect(&server)?;
|
||||
let req = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params });
|
||||
let req_str = serde_json::to_string(&req)? + "\n";
|
||||
s.write_all(req_str.as_bytes())?;
|
||||
let mut reader = BufReader::new(s);
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line)?;
|
||||
println!("{}", line.trim());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_conn(
|
||||
stream: tokio::net::TcpStream,
|
||||
lattice: Arc<Mutex<Lattice>>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (read_half, mut write_half) = stream.into_split();
|
||||
let mut reader = ABufReader::new(read_half);
|
||||
let mut line = String::new();
|
||||
|
||||
loop {
|
||||
line.clear();
|
||||
let n = reader.read_line(&mut line).await?;
|
||||
if n == 0 {
|
||||
break; // EOF
|
||||
}
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let resp = match serde_json::from_str::<Value>(&line) {
|
||||
Ok(msg) => {
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
let method = msg.get("method").and_then(|m| m.as_str()).unwrap_or("");
|
||||
let params = msg.get("params").cloned().unwrap_or_else(|| json!({}));
|
||||
let mut lat = lattice.lock().await;
|
||||
match dispatch(method, params, &mut lat).await {
|
||||
Ok(result) => ok_response(id, result),
|
||||
Err(msg) => err_response(id, -32000, &msg),
|
||||
}
|
||||
}
|
||||
Err(_) => continue, // skip malformed lines
|
||||
};
|
||||
let s = serde_json::to_string(&resp)? + "\n";
|
||||
write_half.write_all(s.as_bytes()).await?;
|
||||
write_half.flush().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn put_then_get_round_trip() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
|
||||
let v = dispatch(
|
||||
"put",
|
||||
json!({ "data": "hello world" }),
|
||||
&mut lat,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let hash = v["hash"].as_str().unwrap().to_string();
|
||||
|
||||
let v = dispatch(
|
||||
"get",
|
||||
json!({ "hash": hash }),
|
||||
&mut lat,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(v["found"], true);
|
||||
assert_eq!(v["content"], "hello world");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_unknown_returns_not_found() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let v = dispatch(
|
||||
"get",
|
||||
json!({ "hash": "0000000000000000000000000000000000000000000000000000000000000000" }),
|
||||
&mut lat,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(v["found"], false);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn put_missing_data_errors() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let r = dispatch("put", json!({}), &mut lat).await;
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("data"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_invalid_hex_errors() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let r = dispatch("get", json!({ "hash": "Z" }), &mut lat).await;
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_method_errors() {
|
||||
let dir = tempdir().unwrap();
|
||||
let mut lat = Lattice::open(dir.path()).unwrap();
|
||||
let r = dispatch("foo", json!({}), &mut lat).await;
|
||||
assert!(r.is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user