use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::mapreduce::phase::Phase;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct KeyDatum {
pub bucket: String,
pub key: String,
#[serde(default)]
pub value: Option<Value>,
#[serde(default)]
pub data: Option<Value>,
}
impl KeyDatum {
#[must_use]
pub fn with_value(bucket: impl Into<String>, key: impl Into<String>, value: Value) -> Self {
Self {
bucket: bucket.into(),
key: key.into(),
value: Some(value),
data: None,
}
}
#[must_use]
pub fn pair(bucket: impl Into<String>, key: impl Into<String>) -> Self {
Self {
bucket: bucket.into(),
key: key.into(),
value: None,
data: None,
}
}
#[must_use]
pub fn to_value(&self) -> Value {
serde_json::json!({
"bucket": self.bucket,
"key": self.key,
"value": self.value.clone().unwrap_or(Value::Null),
"data": self.data.clone().unwrap_or(Value::Null),
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Inputs {
Pairs(Vec<(String, String)>),
KeyData(Vec<KeyDatum>),
Bucket(String),
}
impl Inputs {
pub fn items(&self) -> Option<Vec<KeyDatum>> {
match self {
Self::Pairs(pairs) => Some(
pairs
.iter()
.map(|(b, k)| KeyDatum::pair(b.clone(), k.clone()))
.collect(),
),
Self::KeyData(data) => Some(data.clone()),
Self::Bucket(_) => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct MapReduceJob {
pub inputs: Inputs,
#[serde(rename = "query")]
pub phases: Vec<Phase>,
#[serde(default, rename = "timeout")]
pub timeout_ms: Option<u64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_datum_to_value_fills_nulls() {
let kd = KeyDatum::pair("b", "k");
let v = kd.to_value();
assert_eq!(v["bucket"], "b");
assert_eq!(v["key"], "k");
assert!(v["value"].is_null());
assert!(v["data"].is_null());
}
#[test]
fn inputs_pairs_roundtrip_to_json() {
let i = Inputs::Pairs(vec![("b".into(), "k".into())]);
let s = serde_json::to_string(&i).expect("encode");
let back: Inputs = serde_json::from_str(&s).expect("decode");
assert_eq!(back, i);
}
#[test]
fn inputs_bucket_roundtrips_to_json() {
let i = Inputs::Bucket("users".into());
let s = serde_json::to_string(&i).expect("encode");
let back: Inputs = serde_json::from_str(&s).expect("decode");
assert_eq!(back, i);
}
#[test]
fn inputs_keydata_roundtrips_to_json() {
let i = Inputs::KeyData(vec![KeyDatum::with_value(
"b",
"k",
serde_json::json!({"value": 7}),
)]);
let s = serde_json::to_string(&i).expect("encode");
let back: Inputs = serde_json::from_str(&s).expect("decode");
assert_eq!(back, i);
}
#[test]
fn job_roundtrips_through_json() {
let job = MapReduceJob {
inputs: Inputs::Pairs(vec![("b".into(), "k".into())]),
phases: vec![Phase::Map {
fn_name: "map_object_value".into(),
arg: None,
keep: false,
}],
timeout_ms: Some(60_000),
};
let s = serde_json::to_string(&job).expect("encode");
let back: MapReduceJob = serde_json::from_str(&s).expect("decode");
assert_eq!(back, job);
}
#[test]
fn job_decodes_riak_style_json() {
let s = r#"{
"inputs": [["b","k1"],["b","k2"]],
"query": [{"map": {"name": "map_object_value", "keep": false}}],
"timeout": 1000
}"#;
let job: MapReduceJob = serde_json::from_str(s).expect("decode");
assert_eq!(job.timeout_ms, Some(1000));
assert_eq!(job.phases.len(), 1);
match &job.inputs {
Inputs::Pairs(p) => assert_eq!(p.len(), 2),
_ => panic!("expected Pairs"),
}
}
}