feat(cli): initial upend cli
parent
acfd8432dc
commit
d5f6a615ba
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "upend_cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.0.32", features = ["derive", "color"] }
|
||||
reqwest = { version = "0.11.13", features = ["blocking", "json"] }
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
multibase = "0.9"
|
||||
multihash = { version = "*", default-features = false, features = [
|
||||
"alloc",
|
||||
"multihash-impl",
|
||||
"sha2",
|
||||
"identity",
|
||||
] }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
||||
filebuffer = "0.4.0"
|
||||
|
||||
log = "0.4"
|
||||
anyhow = "1.0.68"
|
||||
env_logger = "0.10.0"
|
|
@ -0,0 +1,114 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use multihash::{Code, Hasher, Multihash, MultihashDigest};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Entry {
|
||||
pub entity: String,
|
||||
pub attribute: String,
|
||||
pub value: EntryValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImmutableEntry(pub Entry);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InvariantEntry {
|
||||
pub attribute: String,
|
||||
pub value: EntryValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum EntryValue {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Address(String),
|
||||
Null,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl std::str::FromStr for EntryValue {
|
||||
type Err = std::convert::Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if s.len() < 2 {
|
||||
match s.chars().next() {
|
||||
Some('S') => Ok(EntryValue::String("".into())),
|
||||
Some('X') => Ok(EntryValue::Null),
|
||||
_ => Ok(EntryValue::Invalid),
|
||||
}
|
||||
} else {
|
||||
let (type_char, content) = s.split_at(1);
|
||||
match (type_char, content) {
|
||||
("S", content) => Ok(EntryValue::String(String::from(content))),
|
||||
("N", content) => {
|
||||
if let Ok(n) = content.parse::<f64>() {
|
||||
Ok(EntryValue::Number(n))
|
||||
} else {
|
||||
Ok(EntryValue::Invalid)
|
||||
}
|
||||
}
|
||||
("O", content) => {
|
||||
Ok(EntryValue::Address(String::from(content)))
|
||||
}
|
||||
_ => Ok(EntryValue::Invalid),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Hash(pub Vec<u8>);
|
||||
|
||||
impl AsRef<[u8]> for Hash {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash<T: AsRef<[u8]>>(input: T) -> Hash {
|
||||
let mut hasher = multihash::Sha2_256::default();
|
||||
hasher.update(input.as_ref());
|
||||
Hash(Vec::from(hasher.finalize()))
|
||||
}
|
||||
|
||||
fn b58_encode<T: AsRef<[u8]>>(vec: T) -> String {
|
||||
multibase::encode(multibase::Base::Base58Btc, vec.as_ref())
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub enum Address {
|
||||
Hash(Hash),
|
||||
Attribute(String),
|
||||
Url(String),
|
||||
}
|
||||
|
||||
// multihash SHA2-256
|
||||
const SHA2_256: u64 = 0x12;
|
||||
// multihash identity
|
||||
const IDENTITY: u64 = 0x00;
|
||||
|
||||
impl Address {
|
||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||
let hash = match self {
|
||||
Self::Hash(hash) => Multihash::wrap(SHA2_256, &hash.0).map_err(|err| anyhow!(err))?,
|
||||
Self::Attribute(attribute) => {
|
||||
Code::Identity.digest(&[&[b'A'], attribute.as_bytes()].concat())
|
||||
}
|
||||
Self::Url(url) => Code::Identity.digest(&[&[b'X'], url.as_bytes()].concat()),
|
||||
};
|
||||
|
||||
Ok(hash.to_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Address {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
b58_encode(self.encode().map_err(|_| std::fmt::Error)?)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
use anyhow::anyhow;
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use filebuffer::FileBuffer;
|
||||
use log::{debug, error, trace};
|
||||
use reqwest::Url;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::common::EntryValue;
|
||||
|
||||
mod common;
|
||||
|
||||
/// Command-line client for UpEnd
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None, arg_required_else_help=true)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Output format
|
||||
#[arg(short, long)]
|
||||
format: Option<OutputFormat>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
Query {
|
||||
/// URL of the UpEnd instance to query.
|
||||
#[arg(short, long)]
|
||||
url: Option<Url>,
|
||||
/// The query itself, in L-expression format.
|
||||
query: String,
|
||||
},
|
||||
/// Insert an entry.
|
||||
Insert {
|
||||
/// URL of the UpEnd instance to query.
|
||||
#[arg(short, long)]
|
||||
url: Option<Url>,
|
||||
/// The address of the entity.
|
||||
entity: String,
|
||||
// The attribute.
|
||||
attribute: String,
|
||||
// The value.
|
||||
value: common::EntryValue,
|
||||
},
|
||||
/// Get the address of a file, attribute or URL.
|
||||
Address {
|
||||
/// Path to a file.
|
||||
filepath: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
|
||||
enum OutputFormat {
|
||||
Json,
|
||||
Tsv,
|
||||
Raw,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
let args = Args::parse();
|
||||
|
||||
let format = args.format.unwrap_or(OutputFormat::Tsv);
|
||||
|
||||
match args.command {
|
||||
Some(Commands::Query { url, query }) => {
|
||||
let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
|
||||
let api_url = url.join("/api/query")?;
|
||||
|
||||
debug!("Querying \"{}\"", api_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.post(api_url).body(query).send()?;
|
||||
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => match format {
|
||||
OutputFormat::Json | OutputFormat::Raw => println!("{}", response.text()?),
|
||||
OutputFormat::Tsv => todo!(),
|
||||
},
|
||||
Err(err) => {
|
||||
error!("{}", response.text()?);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Insert {
|
||||
url,
|
||||
entity,
|
||||
attribute,
|
||||
value,
|
||||
}) => {
|
||||
let url = url.unwrap_or("http://localhost:8093".parse().unwrap());
|
||||
let api_url = url.join("/api/obj")?;
|
||||
|
||||
if let EntryValue::Invalid = value {
|
||||
return Err(anyhow!("Invalid entry value."));
|
||||
}
|
||||
|
||||
let body = json!({
|
||||
"entity": entity,
|
||||
"attribute": attribute,
|
||||
"value": value
|
||||
});
|
||||
|
||||
debug!("Inserting {:?} at \"{}\"", body, api_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.put(api_url).json(&body).send()?;
|
||||
|
||||
match response.error_for_status_ref() {
|
||||
Ok(_) => {
|
||||
let data: Vec<String> = response.json()?;
|
||||
println!("{}", data[0]);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("{}", response.text()?);
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Commands::Address { filepath }) => {
|
||||
debug!("Hashing {:?}...", filepath);
|
||||
let fbuffer = FileBuffer::open(&filepath)?;
|
||||
let digest = common::hash(&fbuffer);
|
||||
trace!("Finished hashing {:?}...", &filepath);
|
||||
let address = common::Address::Hash(digest);
|
||||
|
||||
match format {
|
||||
OutputFormat::Json => todo!(),
|
||||
OutputFormat::Tsv | OutputFormat::Raw => println!("{}", address),
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue