feat(cli): initial upend cli

feat/type-attributes
Tomáš Mládek 2023-01-04 21:17:48 +01:00
parent acfd8432dc
commit d5f6a615ba
5 changed files with 1758 additions and 0 deletions

1
tools/upend_cli/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1479
tools/upend_cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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)?)
)
}
}

136
tools/upend_cli/src/main.rs Normal file
View File

@ -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(())
}