diff options
-rw-r--r-- | adenosine-cli/plan.txt | 4 | ||||
-rw-r--r-- | adenosine-cli/src/bin/adenosine.rs | 28 | ||||
-rw-r--r-- | adenosine-cli/src/identifiers.rs | 4 | ||||
-rw-r--r-- | adenosine-cli/src/lib.rs | 64 |
4 files changed, 84 insertions, 16 deletions
diff --git a/adenosine-cli/plan.txt b/adenosine-cli/plan.txt index 668a746..32e5070 100644 --- a/adenosine-cli/plan.txt +++ b/adenosine-cli/plan.txt @@ -63,7 +63,3 @@ file; '-' to read a JSON file from stdin. profile <name>? search-users <query> ---- - -plan: -- first version which uses entirely schema-less / generic XRPC client diff --git a/adenosine-cli/src/bin/adenosine.rs b/adenosine-cli/src/bin/adenosine.rs index 1d23c27..33f74c9 100644 --- a/adenosine-cli/src/bin/adenosine.rs +++ b/adenosine-cli/src/bin/adenosine.rs @@ -123,11 +123,11 @@ enum Command { Create { collection: String, - fields: String, + fields: Vec<ArgField>, }, Update { uri: AtUri, - fields: String, + fields: Vec<ArgField>, }, Delete { uri: AtUri, @@ -144,7 +144,7 @@ enum Command { Xrpc { method: XrpcMethod, nsid: String, - fields: Option<String>, + fields: Vec<ArgField>, }, /// Sub-commands for managing account @@ -274,7 +274,7 @@ fn run(opt: Opt) -> Result<()> { if let Some(c) = cid { params.insert("cid".to_string(), c); } - xrpc_client.post("com.atproto.repoGetRecord", Some(params), json!({}))? + xrpc_client.get("com.atproto.repoGetRecord", Some(params))? } Command::Ls { uri } => { // TODO: option to print fully-qualified path? @@ -303,7 +303,9 @@ fn run(opt: Opt) -> Result<()> { } Command::Create { collection, fields } => { params.insert("collection".to_string(), collection); - unimplemented!() + update_params_from_fields(&fields, &mut params); + let val = value_from_fields(fields); + xrpc_client.post("com.atproto.repoCreateRecord", Some(params), val)? } Command::Update { uri, fields } => { params.insert("did".to_string(), uri.repository.to_string()); @@ -315,7 +317,13 @@ fn run(opt: Opt) -> Result<()> { "rkey".to_string(), uri.record.ok_or(anyhow!("record key required"))?, ); - unimplemented!() + // fetch existing, extend map with fields, put the updated value + let mut record = xrpc_client + .get("com.atproto.repoGetRecord", Some(params.clone()))? + .unwrap_or(json!({})); + update_params_from_fields(&fields, &mut params); + update_value_from_fields(fields, &mut record); + xrpc_client.post("com.atproto.repoPutRecord", Some(params), record)? } Command::Delete { uri } => { params.insert("did".to_string(), uri.repository.to_string()); @@ -334,11 +342,11 @@ fn run(opt: Opt) -> Result<()> { nsid, fields, } => { - let body: Value = ().into(); + update_params_from_fields(&fields, &mut params); + let body = value_from_fields(fields); match method { - // XXX: parse params - XrpcMethod::Get => xrpc_client.get(&nsid, None)?, - XrpcMethod::Post => xrpc_client.post(&nsid, None, body)?, + XrpcMethod::Get => xrpc_client.get(&nsid, Some(params))?, + XrpcMethod::Post => xrpc_client.post(&nsid, Some(params), body)?, } } Command::Account { diff --git a/adenosine-cli/src/identifiers.rs b/adenosine-cli/src/identifiers.rs index 7129ba5..7f88df5 100644 --- a/adenosine-cli/src/identifiers.rs +++ b/adenosine-cli/src/identifiers.rs @@ -118,6 +118,10 @@ fn test_aturi() { assert!(AtUri::from_str("at://bob.com/io.example.song").is_ok()); assert!(AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a").is_ok()); assert!(AtUri::from_str("at://bob.com/io.example.song/3yI5-c1z-cc2p-1a#/title").is_ok()); + assert!( + AtUri::from_str("at://did:plc:ltk4reuh7rkoy2frnueetpb5/app.bsky.follow/3jg23pbmlhc2a") + .is_ok() + ); let uri = AtUri { repository: DidOrHost::Did("some".to_string(), "thing".to_string()), diff --git a/adenosine-cli/src/lib.rs b/adenosine-cli/src/lib.rs index 8f462da..6d953ca 100644 --- a/adenosine-cli/src/lib.rs +++ b/adenosine-cli/src/lib.rs @@ -1,5 +1,7 @@ use anyhow::anyhow; pub use anyhow::Result; +use lazy_static::lazy_static; +use regex::Regex; use reqwest::header; use serde_json::Value; use std::collections::HashMap; @@ -178,5 +180,63 @@ fn test_parse_jwt() { assert!(parse_did_from_jwt("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9").is_err()); } -// TODO: parse at-uri -// at://did:plc:ltk4reuh7rkoy2frnueetpb5/app.bsky.follow/3jg23pbmlhc2a +/// Represents fields/content specified on the command line. +/// +/// Sort of like HTTPie. Query parameters are '==', body values (JSON) are '='. Only single-level +/// body values are allowed currently, not JSON Pointer assignment. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ArgField { + Query(String, serde_json::Value), + Body(String, serde_json::Value), +} + +impl FromStr for ArgField { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + lazy_static! { + static ref FIELD_RE: Regex = Regex::new(r"^([a-zA-Z_]+)=(=?)(.*)$").unwrap(); + } + if let Some(captures) = FIELD_RE.captures(s) { + let key = captures[1].to_string(); + let val = Value::from_str(&captures[3])?; + if captures.get(2).is_some() { + Ok(ArgField::Query(key, val)) + } else { + Ok(ArgField::Body(key, val)) + } + } else { + Err(anyhow!("could not parse as a field assignment: {}", s)) + } + } +} + +// TODO: what should type signature actually be here... +pub fn update_params_from_fields(fields: &[ArgField], params: &mut HashMap<String, String>) { + for f in fields.iter() { + if let ArgField::Query(ref k, ref v) = f { + params.insert(k.to_string(), v.to_string()); + } + } +} + +pub fn update_value_from_fields(fields: Vec<ArgField>, value: &mut Value) { + if let Value::Object(map) = value { + for f in fields.into_iter() { + if let ArgField::Body(k, v) = f { + map.insert(k, v); + } + } + } +} + +/// Consumes the entire Vec of fields passed in +pub fn value_from_fields(fields: Vec<ArgField>) -> Value { + let mut map: HashMap<String, Value> = HashMap::new(); + for f in fields.into_iter() { + if let ArgField::Body(k, v) = f { + map.insert(k, v); + } + } + Value::Object(serde_json::map::Map::from_iter(map.into_iter())) +} |