use super::Run;
use super::back_compat::{RunV1, RunV2, RunV3, RunV4};
use crate::HailError;
use crate::run::ser_des::SerDesRun;
use crate::{Result, error::DeError};
use std::{
io::{BufRead, BufReader, Read, Write},
str::FromStr,
};
use ron::Options;
use ron::extensions::Extensions;
use ron::ser::PrettyConfig;
impl Run {
pub fn from_reader(r: impl Read) -> Result<Self> {
let mut b = BufReader::new(r);
let mut s = String::new();
b.read_line(&mut s)?;
let s = s.trim_end();
if s.len() < 9 {
return Err(DeError::InvalidVersion.into());
}
Self::de_impl(s, b)
}
pub fn to_writer(&self, mut w: impl Write) -> Result<()> {
w.write_all(b"version 5\n")?;
let sd = Options::default().with_default_extension(Extensions::IMPLICIT_SOME);
sd.to_io_writer_pretty(w, &SerDesRun::from(self), PrettyConfig::new())?;
Ok(())
}
pub fn to_string(&self) -> Result<String> {
let mut s = String::from("version 5\n");
let sd = Options::default().with_default_extension(Extensions::IMPLICIT_SOME);
s.push_str(&sd.to_string_pretty(&SerDesRun::from(self), PrettyConfig::new())?);
Ok(s)
}
fn de_impl(l1: &str, rest: impl Read) -> Result<Self> {
let version: u8 = match l1.rsplit_once(' ') {
Some((_, n)) => n.parse().map_err(|_| DeError::InvalidVersion)?,
None => return Err(DeError::InvalidVersion.into()),
};
let sd = Options::default().with_default_extension(Extensions::IMPLICIT_SOME);
let mut r: Self = match version {
5 => sd.from_reader::<_, SerDesRun>(rest).map(|r| r.into()),
4 => sd.from_reader::<_, RunV4>(rest).map(|r| r.into()),
3 => sd.from_reader::<_, RunV3>(rest).map(|r| r.into()),
2 => sd.from_reader::<_, RunV2>(rest).map(|r| r.into()),
1 => sd.from_reader::<_, RunV1>(rest).map(|r| r.into()),
_ => return Err(DeError::InvalidVersion.into()),
}
.map_err(DeError::Ron)?;
r.verify()?;
r.normalize();
r.calc_avgs();
r.calc_segments();
Ok(r)
}
}
impl FromStr for Run {
type Err = HailError;
fn from_str(s: &str) -> Result<Self> {
let split = s.split_once('\n').ok_or(DeError::InvalidVersion)?;
Self::de_impl(split.0, split.1.as_bytes())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::TimeType;
#[test]
fn de_run_str_v5_rtaonly() {
let r = Run::from_str(RUN_V5_STR_RTAONLY).unwrap();
assert_eq!(r, gen_test_run_rtaonly());
}
#[test]
fn de_run_str_v5() {
let r = Run::from_str(RUN_V5_STR).unwrap();
assert_eq!(r, gen_test_run());
}
#[test]
fn de_run_str_v4_rtaonly() {
let r = Run::from_str(RUN_V4_STR_RTAONLY).unwrap();
assert_eq!(r, gen_test_run_rtaonly());
}
#[test]
fn de_run_str_v4() {
let r = Run::from_str(RUN_V4_STR).unwrap();
assert_eq!(r, gen_test_run());
}
#[test]
fn de_run_str_v3() {
let r = Run::from_str(RUN_V3_STR).unwrap();
assert_eq!(r, gen_test_run_rtaonly());
}
#[test]
fn de_run_str_v3_igt() {
let r = Run::from_str(RUN_V3_STR_IGT).unwrap();
let mut tr = gen_test_run_rtaonly();
tr.igt_pb_splits = tr.rta_pb_splits.clone();
tr.igt_pb_segments = tr.rta_pb_segments.clone();
tr.igt_gold_segments = tr.rta_gold_segments.clone();
tr.igt_avg_segments = tr.rta_avg_segments.clone();
tr.igt_sum_segments = tr.rta_sum_segments.clone();
tr.ingame_time = true;
assert_eq!(r, tr);
}
#[test]
fn de_run_str_v2() {
let r = Run::from_str(RUN_V2_STR).unwrap();
assert_eq!(r, gen_test_run_backcompat());
}
#[test]
fn de_run_str_v1() {
let r = Run::from_str(RUN_V1_STR).unwrap();
let mut r2 = gen_test_run_backcompat();
r2.rta_sum_segments = vec![(0, TimeType::None); r2.segment_names.len()];
r2.igt_sum_segments = vec![(0, TimeType::None); r2.segment_names.len()];
r2.rta_pb_splits[1] = TimeType::None;
r2.igt_pb_splits[1] = TimeType::None;
r2.calc_avgs();
assert_eq!(r, r2);
}
#[test]
fn de_run_read_v4_rtaonly() {
let r = Run::from_reader(RUN_V4_STR_RTAONLY.as_bytes()).unwrap();
assert_eq!(r, gen_test_run_rtaonly());
}
#[test]
fn de_run_read_v3() {
let r = Run::from_reader(RUN_V3_STR.as_bytes()).unwrap();
assert_eq!(r, gen_test_run_rtaonly());
}
#[test]
fn de_run_read_v2() {
let r = Run::from_reader(RUN_V2_STR.as_bytes()).unwrap();
assert_eq!(r, gen_test_run_backcompat());
}
#[test]
fn de_run_read_v1() {
let r = Run::from_reader(RUN_V1_STR.as_bytes()).unwrap();
let mut r2 = gen_test_run_backcompat();
r2.rta_sum_segments = vec![(0, TimeType::None); r2.segment_names.len()];
r2.igt_sum_segments = vec![(0, TimeType::None); r2.segment_names.len()];
r2.calc_avgs();
assert_eq!(r, r2);
}
#[test]
fn ser_run_str() {
let r = gen_test_run();
assert_eq!(r.to_string().unwrap(), RUN_V5_STR);
}
#[test]
fn ser_run_write() {
let r = gen_test_run();
let mut b: Vec<u8> = vec![];
r.to_writer(&mut b).unwrap();
let s = String::from_utf8(b).unwrap();
assert_eq!(s, RUN_V5_STR);
}
fn gen_test_run_rtaonly() -> Run {
use crate::types::TimeType::*;
Run {
game_title: "Test".into(),
category: "tseT".into(),
ingame_time: false,
offset: Time(-12345),
segment_names: vec![
"hello".into(),
"world".into(),
"foobar".into(),
"baz".into(),
],
rta_pb_splits: vec![Time(1000), None, Time(1100), Time(3100)],
rta_pb_segments: vec![Time(1000), None, None, Time(2000)],
rta_gold_segments: vec![Time(999), None, None, Time(1999)],
rta_sum_segments: vec![(2, Time(1999)), (1, Time(6000)), (0, None), (3, Time(4000))],
rta_avg_segments: vec![Time(999), Time(6000), None, Time(1333)],
..Default::default()
}
}
fn gen_test_run() -> Run {
use crate::types::TimeType::*;
Run {
game_title: "Test".into(),
category: "tseT".into(),
ingame_time: true,
offset: Time(-12345),
segment_names: vec![
"hello".into(),
"world".into(),
"foobar".into(),
"baz".into(),
],
rta_pb_splits: vec![Time(1000), None, Time(1100), Time(3100)],
igt_pb_splits: vec![Time(500), None, Time(600), Time(1600)],
rta_pb_segments: vec![Time(1000), None, None, Time(2000)],
igt_pb_segments: vec![Time(500), None, None, Time(1000)],
rta_gold_segments: vec![Time(999), None, None, Time(1999)],
igt_gold_segments: vec![Time(499), None, None, Time(999)],
rta_sum_segments: vec![(2, Time(1999)), (1, Time(6000)), (0, None), (3, Time(4000))],
igt_sum_segments: vec![(2, Time(999)), (1, Time(3000)), (0, None), (3, Time(2000))],
rta_avg_segments: vec![Time(999), Time(6000), None, Time(1333)],
igt_avg_segments: vec![Time(499), Time(3000), None, Time(666)],
}
}
fn gen_test_run_backcompat() -> Run {
use crate::types::TimeType::*;
Run {
game_title: "Test".into(),
category: "tseT".into(),
ingame_time: false,
offset: Time(-12345),
segment_names: vec![
"hello".into(),
"world".into(),
"foobar".into(),
"baz".into(),
],
rta_pb_splits: vec![Time(1000), None, Time(1500), Time(3000)],
igt_pb_splits: vec![Time(1000), None, Time(1500), Time(3000)],
rta_pb_segments: vec![Time(1000), None, None, Time(1500)],
igt_pb_segments: vec![Time(1000), None, None, Time(1500)],
rta_gold_segments: vec![Time(999), None, None, Time(1999)],
igt_gold_segments: vec![Time(999), None, None, Time(1999)],
rta_sum_segments: vec![(2, Time(1999)), (1, Time(6000)), (0, None), (3, Time(4000))],
igt_sum_segments: vec![(2, Time(1999)), (1, Time(6000)), (0, None), (3, Time(4000))],
rta_avg_segments: vec![Time(999), Time(6000), None, Time(1333)],
igt_avg_segments: vec![Time(999), Time(6000), None, Time(1333)],
}
}
const RUN_V5_STR_RTAONLY: &str = r#"version 5
(
game_title: "Test",
category: "tseT",
ingame_time: false,
offset: -12345,
segment_names: [
"hello",
"world",
"foobar",
"baz",
],
rta_pb_splits: [
1000,
None,
1100,
3100,
],
igt_pb_splits: [
None,
],
rta_gold_segments: [
999,
None,
None,
1999,
],
igt_gold_segments: [
None,
],
rta_sum_segments: [
(2, 1999),
(1, 6000),
(0, None),
(3, 4000),
],
igt_sum_segments: [
(0, None),
],
)"#;
const RUN_V5_STR: &str = r#"version 5
(
game_title: "Test",
category: "tseT",
ingame_time: true,
offset: -12345,
segment_names: [
"hello",
"world",
"foobar",
"baz",
],
rta_pb_splits: [
1000,
None,
1100,
3100,
],
igt_pb_splits: [
500,
None,
600,
1600,
],
rta_gold_segments: [
999,
None,
None,
1999,
],
igt_gold_segments: [
499,
None,
None,
999,
],
rta_sum_segments: [
(2, 1999),
(1, 6000),
(0, None),
(3, 4000),
],
igt_sum_segments: [
(2, 999),
(1, 3000),
(0, None),
(3, 2000),
],
)"#;
const RUN_V4_STR_RTAONLY: &str = r#"version 4
(
game_title: "Test",
category: "tseT",
ingame_time: false,
offset: Time(-12345),
segment_names: [
"hello",
"world",
"foobar",
"baz",
],
rta_pb_segments: [
Time(1000),
Skipped,
Time(100),
Time(2000),
],
igt_pb_segments: [
None,
],
rta_gold_segments: [
Time(999),
None,
None,
Time(1999),
],
igt_gold_segments: [
None,
],
rta_sum_segments: [
(2, Time(1999)),
(1, Time(6000)),
(0, None),
(3, Time(4000)),
],
igt_sum_segments: [
(0, None),
],
)"#;
const RUN_V4_STR: &str = r#"version 4
(
game_title: "Test",
category: "tseT",
ingame_time: true,
offset: Time(-12345),
segment_names: [
"hello",
"world",
"foobar",
"baz",
],
rta_pb_segments: [
Time(1000),
Skipped,
Time(100),
Time(2000),
],
igt_pb_segments: [
Time(500),
Skipped,
Time(100),
Time(1000),
],
rta_gold_segments: [
Time(999),
None,
None,
Time(1999),
],
igt_gold_segments: [
Time(499),
None,
None,
Time(999),
],
rta_sum_segments: [
(2, Time(1999)),
(1, Time(6000)),
(0, None),
(3, Time(4000)),
],
igt_sum_segments: [
(2, Time(999)),
(1, Time(3000)),
(0, None),
(3, Time(2000)),
],
)"#;
const RUN_V3_STR: &str = r#"version 3
(
game_title: "Test",
category: "tseT",
ingame_time: false,
offset: Time(-12345),
segment_names: [
"hello",
"world",
"foobar",
"baz",
],
pb_segments: [
Time(1000),
Skipped,
Time(100),
Time(2000),
],
gold_segments: [
Time(999),
None,
None,
Time(1999),
],
sum_segments: [
(2, Time(1999)),
(1, Time(6000)),
(0, None),
(3, Time(4000)),
],
)"#;
const RUN_V3_STR_IGT: &str = r#"version 3
(
game_title: "Test",
category: "tseT",
ingame_time: true,
offset: Time(-12345),
segment_names: [
"hello",
"world",
"foobar",
"baz",
],
pb_segments: [
Time(1000),
Skipped,
Time(100),
Time(2000),
],
gold_segments: [
Time(999),
None,
None,
Time(1999),
],
sum_segments: [
(2, Time(1999)),
(1, Time(6000)),
(0, None),
(3, Time(4000)),
],
)"#;
const RUN_V2_STR: &str = r#"version 2
(
game_title: "Test",
category: "tseT",
offset: Time(12345),
pb: Time(3000),
splits: [
"hello",
"world",
"foobar",
"baz",
],
pb_times: [
Time(1000),
Skipped(250),
Time(250),
Time(1500),
],
gold_times: [
Time(999),
None,
None,
Time(1999),
],
sum_times: [
(2, Time(1999)),
(1, Time(6000)),
(0, None),
(3, Time(4000)),
],
)"#;
const RUN_V1_STR: &str = r#"version 1
(
game_title: "Test",
category: "tseT",
offset: Some(12345),
pb: 3000,
splits: [
"hello",
"world",
"foobar",
"baz",
],
pb_times: [
1000,
0,
500,
1500,
],
gold_times: [
999,
0,
0,
1999,
],
)"#;
}