use serde::Deserialize;
use std::collections::BTreeMap;
use std::io::{self, Read};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Binding {
#[serde(default)]
pub remote: Option<String>,
#[serde(default)]
pub stealth: bool,
pub tasks_branch: String,
pub store: String,
#[serde(default)]
pub landing: String,
pub invocation_path: String,
}
pub struct Input {
pub binding: Binding,
pub id: Option<String>,
}
#[derive(Deserialize)]
struct Envelope {
binding: Binding,
#[serde(default)]
command: Option<Command>,
#[serde(default)]
metadata: Option<BTreeMap<String, Vec<String>>>,
}
#[derive(Deserialize)]
struct Command {
#[serde(default)]
id: Option<String>,
}
pub fn read_input(input: &mut impl Read) -> io::Result<Input> {
let mut buf = String::new();
input.read_to_string(&mut buf)?;
let e: Envelope = serde_json::from_str(&buf).map_err(io::Error::other)?;
let from_trailer = e.metadata.and_then(|m| m.get("bl-id").and_then(|v| v.first().cloned()));
Ok(Input { binding: e.binding, id: e.command.and_then(|c| c.id).or(from_trailer) })
}
#[cfg(test)]
mod tests {
use super::*;
fn read(json: &str) -> io::Result<Binding> {
read_input(&mut json.as_bytes()).map(|i| i.binding)
}
#[test]
fn the_op_ball_comes_from_command_id_or_the_read_wires_trailer() {
let b = r#""binding":{"tasks_branch":"b","store":"/s","invocation_path":"/p"}"#;
let with_cmd = format!(r#"{{{b},"command":{{"op":"claim","id":"bl-0001"}}}}"#);
assert_eq!(read_input(&mut with_cmd.as_bytes()).unwrap().id.as_deref(), Some("bl-0001"));
let read_wire = format!(r#"{{{b},"metadata":{{"bl-id":["bl-0002"]}}}}"#);
assert_eq!(read_input(&mut read_wire.as_bytes()).unwrap().id.as_deref(), Some("bl-0002"));
let diffless = format!("{{{b}}}");
assert!(read_input(&mut diffless.as_bytes()).unwrap().id.is_none());
}
#[test]
fn reads_a_tracked_binding_and_ignores_extra_wire_fields() {
let b = read(
r#"{"op":"sync","phase":"pre","actor":"x","command":{"op":"sync"},
"binding":{"remote":"git@h:r","tasks_branch":"balls/tasks",
"store":"/store","landing":"/landing","invocation_path":"/proj"}}"#,
)
.unwrap();
assert_eq!(b.remote.as_deref(), Some("git@h:r"));
assert_eq!(b.tasks_branch, "balls/tasks");
assert_eq!(b.store, "/store");
assert_eq!(b.landing, "/landing");
assert_eq!(b.invocation_path, "/proj");
assert!(!b.stealth); }
#[test]
fn an_absent_remote_or_landing_is_the_stealth_binding() {
let b = read(
r#"{"binding":{"tasks_branch":"balls/tasks","store":"/store","invocation_path":"/p"}}"#,
)
.unwrap();
assert_eq!(b.remote, None);
assert_eq!(b.landing, "");
}
#[test]
fn an_explicit_stealth_flag_rides_the_binding() {
let b = read(
r#"{"binding":{"stealth":true,"tasks_branch":"balls/tasks","store":"/store","invocation_path":"/p"}}"#,
)
.unwrap();
assert!(b.stealth);
assert_eq!(b.remote, None);
}
#[test]
fn malformed_json_is_an_error() {
assert!(read("not json").is_err());
}
}