use std::io::Read;
use std::process::ExitCode;
use ytsaurus_client::{Client, ClientError, Method, Repeatable, yson_build};
use ytsaurus_yson::{YsonFormat, YsonNode, from_slice};
const ROOT: &str = "//tmp/ytsaurus_rs_raw";
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("\nraw failed: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), ClientError> {
let client = Client::from_env()?;
if client.exists(ROOT)? {
client.remove_tree(ROOT)?;
}
client.create("map_node", ROOT)?;
supported_features(&client)?;
a_file_through_the_raw_door(&client)?;
a_raw_command_inside_a_transaction(&client)?;
a_read_the_caller_says_is_repeatable(&client)?;
what_it_refuses(&client)?;
client.remove_tree(ROOT)?;
println!("\nFour unmodelled commands, sent without forking the crate.");
Ok(())
}
fn supported_features(client: &Client) -> Result<(), ClientError> {
step("A command with no parameters at all");
let body = client.raw_command(
Method::Get,
"get_supported_features",
&yson_build::empty_map(),
None,
)?;
let answer = decode(&body, "get_supported_features")?;
let features = field(&answer, "features").ok_or_else(|| ClientError::Decode {
command: "get_supported_features".to_owned(),
reason: format!("no \"features\" key in {}", String::from_utf8_lossy(&body)),
})?;
let named = match &features.node {
YsonNode::Map(m) => m
.keys()
.map(|k| String::from_utf8_lossy(k).into_owned())
.collect::<Vec<_>>(),
_ => Vec::new(),
};
check(
&format!("the cluster described: {}", named.join(", ")),
!named.is_empty(),
)?;
let codecs = field(&features, "compression_codecs")
.map(|c| match &c.node {
YsonNode::List(items) => items.len(),
_ => 0,
})
.unwrap_or(0);
check(
&format!("this build offers {codecs} compression codecs"),
codecs > 0,
)
}
fn a_file_through_the_raw_door(client: &Client) -> Result<(), ClientError> {
step("A file, uploaded and streamed back");
let path = format!("{ROOT}/blob");
client.create("file", &path)?;
let contents: Vec<u8> = (0..4_000_000_u32).map(|n| (n % 251) as u8).collect();
client.raw_command_upload(
Method::Put,
"write_file",
&yson_build::map([("path", yson_build::string(&path))]),
std::io::Cursor::new(&contents),
)?;
let mut stream = client.raw_command_streaming(
Method::Get,
"read_file",
&yson_build::map([("path", yson_build::string(&path))]),
)?;
let mut sum = 0_u64;
let mut read = 0_u64;
let mut buffer = vec![0_u8; 64 * 1024];
loop {
let n = stream
.read(&mut buffer)
.map_err(|e| ClientError::Config(format!("reading {path}: {e}")))?;
if n == 0 {
break;
}
read += n as u64;
sum += buffer[..n].iter().map(|b| u64::from(*b)).sum::<u64>();
}
let expected: u64 = contents.iter().map(|b| u64::from(*b)).sum();
check(
&format!("{read} bytes came back, byte-for-byte what went up"),
read == contents.len() as u64 && sum == expected,
)?;
check(
&format!("the reader counted the same {read} bytes"),
stream.bytes_read() == read,
)
}
fn a_raw_command_inside_a_transaction(client: &Client) -> Result<(), ClientError> {
step("A raw command joins the transaction it was sent through");
let path = format!("{ROOT}/staged");
let transaction = client.start_transaction()?;
transaction.raw_command(
Method::Post,
"create",
&yson_build::map([
("type", yson_build::string("map_node")),
("path", yson_build::string(&path)),
]),
None,
)?;
check(
"the node is invisible outside the transaction",
!client.exists(&path)?,
)?;
transaction.commit()?;
check(
"and there once the transaction commits",
client.exists(&path)?,
)
}
fn a_read_the_caller_says_is_repeatable(client: &Client) -> Result<(), ClientError> {
step("A read the caller marks as safe to repeat");
let body = client.raw_command_with(
Method::Get,
"list_operations",
&yson_build::map([("limit", yson_build::int(1))]),
None,
Repeatable::Freely,
None,
)?;
let answer = decode(&body, "list_operations")?;
let keys = match &field(&answer, "value").unwrap_or(answer).node {
YsonNode::Map(m) => m.len(),
_ => 0,
};
check(
&format!("the scheduler answered with {keys} keys"),
keys > 0,
)?;
Ok(())
}
fn what_it_refuses(client: &Client) -> Result<(), ClientError> {
step("What it will not send");
let error = client
.raw_command(
Method::Get,
"get/../../hosts",
&yson_build::empty_map(),
None,
)
.err();
check(
"a command name that would change the URL is refused",
matches!(error, Some(ClientError::Config(_))),
)?;
let error = client
.raw_command(
Method::Get,
"read_file",
&yson_build::empty_map(),
Some(b"x"),
)
.err();
check(
"a payload on a GET is refused rather than dropped",
matches!(error, Some(ClientError::Config(_))),
)?;
Ok(())
}
fn decode(body: &[u8], command: &str) -> Result<ytsaurus_yson::YsonValue, ClientError> {
from_slice(body, YsonFormat::Text).map_err(|e| ClientError::Decode {
command: command.to_owned(),
reason: format!("{e}; body was {}", String::from_utf8_lossy(body)),
})
}
fn field(value: &ytsaurus_yson::YsonValue, key: &str) -> Option<ytsaurus_yson::YsonValue> {
match &value.node {
YsonNode::Map(m) => m.get(key.as_bytes()).cloned(),
_ => None,
}
}
fn step(what: &str) {
println!("\n== {what}");
}
fn check(what: &str, passed: bool) -> Result<(), ClientError> {
if passed {
println!(" ok {what}");
return Ok(());
}
eprintln!(" FAIL {what}");
Err(ClientError::Config(format!("check failed: {what}")))
}