use crate::protocol::diff::{DiffDecoder, LexicalForm, decoder_for, supported_diffs};
use crate::protocol::escaping::percent_decode;
use crate::protocol::{ProtocolError, field_value_error};
#[derive(Debug, Clone, PartialEq, Eq)]
enum FieldSlot {
Unset,
Null,
Value {
text: String,
form: LexicalForm,
},
}
impl FieldSlot {
#[must_use]
fn as_sent(text: impl Into<String>) -> Self {
Self::Value {
text: text.into(),
form: LexicalForm::AsSent,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ItemState {
fields: Vec<FieldSlot>,
}
impl ItemState {
#[must_use]
pub(crate) fn new(field_count: usize) -> Self {
Self {
fields: vec![FieldSlot::Unset; field_count],
}
}
#[must_use]
#[inline]
pub(crate) fn field_count(&self) -> usize {
self.fields.len()
}
#[must_use]
pub(crate) fn field(&self, index: usize) -> Option<Option<&str>> {
match self.fields.get(index)? {
FieldSlot::Unset | FieldSlot::Null => Some(None),
FieldSlot::Value { text, .. } => Some(Some(text.as_str())),
}
}
#[must_use]
fn has_baseline(&self, index: usize) -> bool {
matches!(
self.fields.get(index),
Some(FieldSlot::Null | FieldSlot::Value { .. })
)
}
#[cold]
#[inline(never)]
fn no_baseline_error(token: &str, index: usize) -> ProtocolError {
field_value_error(format!(
"{token} leaves field {index} unchanged, but no update has ever set it; §2.3 states \
this never happens on the first update of a subscription"
))
}
pub(crate) fn apply(&mut self, raw_values: &str) -> Result<UpdateOutcome, ProtocolError> {
let field_count = self.fields.len();
let mut pointer: usize = 0;
let mut staged: Vec<(usize, FieldSlot)> = Vec::new();
let mut changed: Vec<usize> = Vec::new();
for token in raw_values.split('|') {
if pointer >= field_count {
return Err(field_value_error(format!(
"value list carries more tokens than the {field_count} fields of the schema"
)));
}
if token.is_empty() {
if !self.has_baseline(pointer) {
return Err(Self::no_baseline_error("an empty token", pointer));
}
pointer += 1;
continue;
}
if token == "#" {
staged.push((pointer, FieldSlot::Null));
changed.push(pointer);
pointer += 1;
continue;
}
if token == "$" {
staged.push((pointer, FieldSlot::as_sent(String::new())));
changed.push(pointer);
pointer += 1;
continue;
}
if let Some(rest) = token.strip_prefix('^') {
let mut rest_chars = rest.chars();
let Some(marker) = rest_chars.next() else {
return Err(field_value_error(
"value list token `^` has no directive after the caret",
));
};
if marker.is_ascii_digit() {
let count = parse_unchanged_run(rest)?;
let end = pointer.checked_add(count).ok_or_else(|| {
field_value_error(format!(
"unchanged-field run `^{rest}` overflows the field pointer"
))
})?;
if end > field_count {
return Err(field_value_error(format!(
"unchanged-field run `^{rest}` reaches field {end} of a \
{field_count}-field schema"
)));
}
if let Some(index) = (pointer..end).find(|index| !self.has_baseline(*index)) {
return Err(Self::no_baseline_error(
&format!("the run `^{rest}`"),
index,
));
}
pointer = end;
continue;
}
if marker.is_ascii_alphabetic() {
let payload = percent_decode(rest_chars.as_str())?;
let decoder = decoder_for(marker).ok_or_else(|| {
field_value_error(format!(
"unsupported diff format `{marker}`; this client advertised `{}`",
supported_diffs()
))
})?;
let base = match self.fields.get(pointer) {
Some(FieldSlot::Value { text, form }) => {
Self::diffable_base(decoder, marker, pointer, text, *form)?
}
Some(FieldSlot::Null) => {
return Err(field_value_error(format!(
"`^{marker}` diff applies to field {pointer}, which is null"
)));
}
Some(FieldSlot::Unset) | None => {
return Err(field_value_error(format!(
"`^{marker}` diff applies to field {pointer}, which has never \
been set"
)));
}
};
let text = (decoder.apply)(base, payload.as_ref())?;
staged.push((
pointer,
FieldSlot::Value {
text,
form: decoder.result_form,
},
));
changed.push(pointer);
pointer += 1;
continue;
}
return Err(field_value_error(format!(
"value list token `{token}` has `{marker}` after the caret, which is neither \
an ASCII digit nor a diff format tag"
)));
}
let decoded = percent_decode(token)?;
staged.push((pointer, FieldSlot::as_sent(decoded.into_owned())));
changed.push(pointer);
pointer += 1;
}
if pointer < field_count {
return Err(field_value_error(format!(
"value list ended after {pointer} of the {field_count} fields of the schema"
)));
}
for (index, slot) in staged {
if let Some(target) = self.fields.get_mut(index) {
*target = slot;
}
}
Ok(UpdateOutcome { changed })
}
fn diffable_base<'slot>(
decoder: &DiffDecoder,
marker: char,
index: usize,
text: &'slot str,
form: LexicalForm,
) -> Result<&'slot str, ProtocolError> {
match form {
LexicalForm::AsSent => Ok(text),
LexicalForm::ClientDerived if decoder.needs_base_as_sent => {
Err(field_value_error(format!(
"`^{marker}` diff applies to field {index}, whose current value was rebuilt \
by this client from a `^P` patch; a character diff cannot be applied to a \
value whose spelling is not the server's"
)))
}
LexicalForm::ClientDerived => Ok(text),
}
}
}
fn parse_unchanged_run(digits: &str) -> Result<usize, ProtocolError> {
if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(field_value_error(format!(
"unchanged-field count `{digits}` is not a run of ASCII digits"
)));
}
digits
.parse::<usize>()
.map_err(|_| field_value_error(format!("unchanged-field count `{digits}` is out of range")))
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) struct UpdateOutcome {
changed: Vec<usize>,
}
impl UpdateOutcome {
#[must_use]
#[inline]
pub(crate) fn changed_fields(&self) -> &[usize] {
&self.changed
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) fn is_field_changed(&self, index: usize) -> bool {
self.changed.contains(&index)
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) fn changed_count(&self) -> usize {
self.changed.len()
}
#[allow(dead_code)]
#[must_use]
#[inline]
pub(crate) fn is_empty(&self) -> bool {
self.changed.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
const QUOTE_FIELDS: usize = 10;
const EX1: &str = "20:00:33|3.04|0.0|2.41|3.67|3.03|3.04|#|#|$";
const EX2: &str = "20:00:54|3.07|0.98|||3.06|3.07|||Suspended";
const EX3: &str = "20:04:16|3.02|-0.65|||3.01|3.02|||$";
const EX4: &str = "20:04:40|^4|3.02|3.03|||";
const EX5: &str = "20:06:10|3.05|0.32|^7";
const EX6: &str = "20:06:49|3.08|1.31|||3.08|3.09|||";
fn quote_state_after(lines: &[&str]) -> ItemState {
let mut state = ItemState::new(QUOTE_FIELDS);
for line in lines {
assert!(state.apply(line).is_ok(), "failed to apply `{line}`");
}
state
}
#[cfg(feature = "json-patch")]
fn text_of(state: &ItemState, index: usize) -> String {
match state.field(index) {
Some(Some(text)) => text.to_owned(),
other => panic!("field {index} should hold text, found {other:?}"),
}
}
fn assert_quote(state: &ItemState, expected: [Option<&str>; QUOTE_FIELDS]) {
for (index, want) in expected.iter().enumerate() {
assert_eq!(state.field(index), Some(*want), "field {index}");
}
assert_eq!(state.field(QUOTE_FIELDS), None, "past end of schema");
}
#[test]
fn test_apply_example_1_snapshot_sets_every_field_p51() -> Result<(), ProtocolError> {
let mut state = ItemState::new(QUOTE_FIELDS);
let outcome = state.apply(EX1)?;
assert_quote(
&state,
[
Some("20:00:33"),
Some("3.04"),
Some("0.0"),
Some("2.41"),
Some("3.67"),
Some("3.03"),
Some("3.04"),
None,
None,
Some(""),
],
);
assert_eq!(outcome.changed_fields(), &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
Ok(())
}
#[test]
fn test_apply_example_2_four_unchanged_fields_p51() -> Result<(), ProtocolError> {
let mut state = quote_state_after(&[EX1]);
let outcome = state.apply(EX2)?;
assert_quote(
&state,
[
Some("20:00:54"),
Some("3.07"),
Some("0.98"),
Some("2.41"),
Some("3.67"),
Some("3.06"),
Some("3.07"),
None,
None,
Some("Suspended"),
],
);
assert_eq!(outcome.changed_fields(), &[0, 1, 2, 5, 6, 9]);
Ok(())
}
#[test]
fn test_apply_example_3_dollar_resets_status_to_empty_p52() -> Result<(), ProtocolError> {
let mut state = quote_state_after(&[EX1, EX2]);
let outcome = state.apply(EX3)?;
assert_quote(
&state,
[
Some("20:04:16"),
Some("3.02"),
Some("-0.65"),
Some("2.41"),
Some("3.67"),
Some("3.01"),
Some("3.02"),
None,
None,
Some(""),
],
);
assert_eq!(outcome.changed_fields(), &[0, 1, 2, 5, 6, 9]);
assert!(outcome.is_field_changed(9));
Ok(())
}
#[test]
fn test_apply_example_4_caret_run_of_four_p52() -> Result<(), ProtocolError> {
let mut state = quote_state_after(&[EX1, EX2, EX3]);
let outcome = state.apply(EX4)?;
assert_quote(
&state,
[
Some("20:04:40"),
Some("3.02"),
Some("-0.65"),
Some("2.41"),
Some("3.67"),
Some("3.02"),
Some("3.03"),
None,
None,
Some(""),
],
);
assert_eq!(outcome.changed_fields(), &[0, 5, 6]);
Ok(())
}
#[test]
fn test_apply_example_5_caret_run_reaching_end_of_schema_p52() -> Result<(), ProtocolError> {
let mut state = quote_state_after(&[EX1, EX2, EX3, EX4]);
let outcome = state.apply(EX5)?;
assert_quote(
&state,
[
Some("20:06:10"),
Some("3.05"),
Some("0.32"),
Some("2.41"),
Some("3.67"),
Some("3.02"),
Some("3.03"),
None,
None,
Some(""),
],
);
assert_eq!(outcome.changed_fields(), &[0, 1, 2]);
Ok(())
}
#[test]
fn test_apply_example_6_final_update_p52() -> Result<(), ProtocolError> {
let mut state = quote_state_after(&[EX1, EX2, EX3, EX4, EX5]);
let outcome = state.apply(EX6)?;
assert_quote(
&state,
[
Some("20:06:49"),
Some("3.08"),
Some("1.31"),
Some("2.41"),
Some("3.67"),
Some("3.08"),
Some("3.09"),
None,
None,
Some(""),
],
);
assert_eq!(outcome.changed_fields(), &[0, 1, 2, 5, 6]);
Ok(())
}
const DIFF_SNAPSHOT: &str =
r#"20:00:33|{ "text": "aa%7Cbb", "attributes": [ { "font": "courier" }, "..." ] }"#;
const DIFF_SNAPSHOT_TEXT: &str =
r#"{ "text": "aa|bb", "attributes": [ { "font": "courier" }, "..." ] }"#;
#[test]
fn test_apply_diff_example_1_snapshot_percent_decodes_pipe_p52() -> Result<(), ProtocolError> {
let mut state = ItemState::new(2);
let outcome = state.apply(DIFF_SNAPSHOT)?;
assert_eq!(state.field(0), Some(Some("20:00:33")));
assert_eq!(state.field(1), Some(Some(DIFF_SNAPSHOT_TEXT)));
assert_eq!(outcome.changed_fields(), &[0, 1]);
Ok(())
}
#[test]
fn test_apply_diff_example_3_empty_token_leaves_text_unchanged_p53() -> Result<(), ProtocolError>
{
let mut state = ItemState::new(2);
state.apply(DIFF_SNAPSHOT)?;
let outcome = state.apply("20:04:16|")?;
assert_eq!(state.field(0), Some(Some("20:04:16")));
assert_eq!(state.field(1), Some(Some(DIFF_SNAPSHOT_TEXT)));
assert_eq!(outcome.changed_fields(), &[0]);
Ok(())
}
#[cfg(feature = "json-patch")]
#[test]
fn test_apply_diff_example_2_json_patch_p53() -> Result<(), Box<dyn std::error::Error>> {
let mut state = ItemState::new(2);
state.apply(DIFF_SNAPSHOT)?;
let line = concat!(
r#"20:00:54|^P[{"op":"replace","path":"/text","value":"aa%7Cbb%7Ccc"},"#,
r#"{"op":"add","path":"/attributes/1","value":"bold"}]"#
);
let outcome = state.apply(line)?;
assert_eq!(state.field(0), Some(Some("20:00:54")));
assert_eq!(outcome.changed_fields(), &[0, 1]);
let got: serde_json::Value = serde_json::from_str(&text_of(&state, 1))?;
let want: serde_json::Value = serde_json::from_str(
r#"{"text":"aa|bb|cc","attributes":[{"font":"courier"},"bold","..."]}"#,
)?;
assert_eq!(got, want);
Ok(())
}
#[cfg(feature = "json-patch")]
#[test]
fn test_apply_json_patch_that_does_not_apply_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply(r#"{"a":1}"#)?;
let result = state.apply(r#"^P[{"op":"remove","path":"/nope"}]"#);
assert!(matches!(result, Err(ProtocolError::FieldValue { .. })));
assert_eq!(state.field(0), Some(Some(r#"{"a":1}"#)));
Ok(())
}
#[cfg(feature = "json-patch")]
const LEXICALLY_AWKWARD_JSON: &str = r#"{ "z" : 1.50 , "a" : "A" , "n" : [ 1, 2 ] }"#;
#[cfg(feature = "json-patch")]
#[test]
fn test_a_tlcp_diff_after_a_json_patch_is_rejected() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply(LEXICALLY_AWKWARD_JSON)?;
state.apply(r#"^P[{"op":"replace","path":"/z","value":2}]"#)?;
let after_patch = text_of(&state, 0);
assert_ne!(
after_patch, LEXICALLY_AWKWARD_JSON,
"the reserialisation is what makes this case dangerous"
);
assert_field_value_error(state.apply("^Tbdzap"));
assert_eq!(state.field(0), Some(Some(after_patch.as_str())));
Ok(())
}
#[cfg(feature = "json-patch")]
#[test]
fn test_a_json_patch_after_a_json_patch_is_accepted() -> Result<(), Box<dyn std::error::Error>>
{
let mut state = ItemState::new(1);
state.apply(LEXICALLY_AWKWARD_JSON)?;
state.apply(r#"^P[{"op":"replace","path":"/z","value":2}]"#)?;
state.apply(r#"^P[{"op":"replace","path":"/a","value":"B"}]"#)?;
let got: serde_json::Value = serde_json::from_str(&text_of(&state, 0))?;
let want: serde_json::Value = serde_json::from_str(r#"{"z":2,"a":"B","n":[1,2]}"#)?;
assert_eq!(got, want);
Ok(())
}
#[cfg(feature = "json-patch")]
#[test]
fn test_a_full_value_restores_the_baseline_a_tlcp_diff_needs() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply(LEXICALLY_AWKWARD_JSON)?;
state.apply(r#"^P[{"op":"replace","path":"/z","value":2}]"#)?;
state.apply("foobar")?;
state.apply("^Tbdzapcd")?;
assert_eq!(state.field(0), Some(Some("fzapbar")));
Ok(())
}
#[cfg(feature = "json-patch")]
#[test]
fn test_a_tlcp_diff_chain_stays_a_legal_baseline() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("foobar")?;
state.apply("^Tbdzapcd")?;
state.apply("^Tdczz")?;
assert_eq!(state.field(0), Some(Some("fzazz")));
Ok(())
}
#[cfg(feature = "json-patch")]
#[test]
fn test_apply_json_patch_onto_non_json_base_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("not json")?;
let result = state.apply(r#"^P[{"op":"replace","path":"/a","value":1}]"#);
assert!(matches!(result, Err(ProtocolError::FieldValue { .. })));
Ok(())
}
#[test]
fn test_apply_tlcp_diff_through_item_state() -> Result<(), ProtocolError> {
let mut state = ItemState::new(2);
state.apply("t0|foobar")?;
let outcome = state.apply("t1|^Tbdzapcd")?;
assert_eq!(state.field(1), Some(Some("fzapbar")));
assert_eq!(outcome.changed_fields(), &[0, 1]);
Ok(())
}
#[test]
fn test_apply_tlcp_diff_onto_multi_byte_value_through_item_state() -> Result<(), ProtocolError>
{
let mut state = ItemState::new(1);
state.apply("日本語テキスト")?;
let outcome = state.apply("^Tdcです")?;
assert_eq!(state.field(0), Some(Some("日本語です")));
assert_eq!(outcome.changed_fields(), &[0]);
Ok(())
}
#[test]
fn test_null_and_empty_are_distinct() -> Result<(), ProtocolError> {
let mut state = ItemState::new(2);
state.apply("#|$")?;
assert_eq!(state.field(0), Some(None));
assert_eq!(state.field(1), Some(Some("")));
assert_ne!(state.field(0), state.field(1));
Ok(())
}
#[test]
fn test_field_index_past_schema_is_none() {
let state = ItemState::new(2);
assert_eq!(state.field(2), None);
assert_eq!(state.field(0), Some(None));
}
#[test]
fn test_null_can_be_replaced_by_a_value_and_back() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("#")?;
assert_eq!(state.field(0), Some(None));
state.apply("v")?;
assert_eq!(state.field(0), Some(Some("v")));
state.apply("#")?;
assert_eq!(state.field(0), Some(None));
Ok(())
}
#[test]
fn test_escaped_markers_are_content_not_markers() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("%23|%24|%5E")?;
assert_eq!(state.field(0), Some(Some("#")));
assert_eq!(state.field(1), Some(Some("$")));
assert_eq!(state.field(2), Some(Some("^")));
Ok(())
}
#[test]
fn test_hash_and_dollar_match_only_as_a_whole_token() -> Result<(), ProtocolError> {
let mut state = ItemState::new(2);
state.apply("%23abc|%24x")?;
assert_eq!(state.field(0), Some(Some("#abc")));
assert_eq!(state.field(1), Some(Some("$x")));
Ok(())
}
#[test]
fn test_pipe_inside_a_value_is_percent_encoded() -> Result<(), ProtocolError> {
let mut state = ItemState::new(2);
let outcome = state.apply("aa%7Cbb|cc")?;
assert_eq!(state.field(0), Some(Some("aa|bb")));
assert_eq!(state.field(1), Some(Some("cc")));
assert_eq!(outcome.changed_count(), 2);
Ok(())
}
#[test]
fn test_caret_run_is_inclusive_of_the_pointed_field() -> Result<(), ProtocolError> {
let mut state = ItemState::new(4);
state.apply("a|b|c|d")?;
let outcome = state.apply("^3|z")?;
assert_eq!(state.field(0), Some(Some("a")));
assert_eq!(state.field(1), Some(Some("b")));
assert_eq!(state.field(2), Some(Some("c")));
assert_eq!(state.field(3), Some(Some("z")));
assert_eq!(outcome.changed_fields(), &[3]);
Ok(())
}
#[test]
fn test_caret_run_at_the_end_of_the_schema() -> Result<(), ProtocolError> {
let mut state = ItemState::new(4);
state.apply("a|b|c|d")?;
let outcome = state.apply("z|^3")?;
assert_eq!(state.field(0), Some(Some("z")));
assert_eq!(state.field(3), Some(Some("d")));
assert_eq!(outcome.changed_fields(), &[0]);
Ok(())
}
#[test]
fn test_caret_run_covering_the_whole_schema_changes_nothing() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
let outcome = state.apply("^3")?;
assert!(outcome.is_empty());
assert_eq!(outcome.changed_fields(), &[] as &[usize]);
Ok(())
}
#[test]
fn test_caret_run_of_zero_advances_nothing() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("a")?;
let outcome = state.apply("^0|b")?;
assert_eq!(state.field(0), Some(Some("b")));
assert_eq!(outcome.changed_fields(), &[0]);
Ok(())
}
#[test]
fn test_empty_token_on_the_first_update_is_rejected() {
let mut state = ItemState::new(2);
assert_field_value_error(state.apply("|a"));
assert_eq!(state.field(0), Some(None));
assert_eq!(state.field(1), Some(None));
}
#[test]
fn test_caret_run_on_the_first_update_is_rejected() {
let mut state = ItemState::new(3);
assert_field_value_error(state.apply("^2|c"));
assert_field_value_error(state.apply("a|^2"));
}
#[test]
fn test_unchanged_tokens_are_accepted_over_every_kind_of_baseline() -> Result<(), ProtocolError>
{
let mut state = ItemState::new(3);
state.apply("a|#|$")?;
let outcome = state.apply("|^2")?;
assert!(outcome.is_empty());
assert_eq!(state.field(0), Some(Some("a")));
assert_eq!(state.field(1), Some(None));
assert_eq!(state.field(2), Some(Some("")));
Ok(())
}
#[test]
fn test_a_run_of_zero_needs_no_baseline() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("^0|a")?;
assert_eq!(state.field(0), Some(Some("a")));
Ok(())
}
#[test]
fn test_caret_run_with_leading_zeros_is_accepted() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
assert!(state.apply("^003").is_ok());
Ok(())
}
fn assert_field_value_error(result: Result<UpdateOutcome, ProtocolError>) {
assert!(
matches!(result, Err(ProtocolError::FieldValue { .. })),
"expected ProtocolError::FieldValue, got {result:?}"
);
}
#[test]
fn test_unknown_diff_tag_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("base")?;
assert_field_value_error(state.apply("^Zwhatever"));
assert_eq!(state.field(0), Some(Some("base")));
Ok(())
}
#[cfg(not(feature = "json-patch"))]
#[test]
fn test_json_patch_tag_is_unknown_when_the_feature_is_off() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply(r#"{"a":1}"#)?;
assert_field_value_error(state.apply(r#"^P[{"op":"remove","path":"/a"}]"#));
Ok(())
}
#[test]
fn test_bare_caret_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("a")?;
assert_field_value_error(state.apply("^"));
Ok(())
}
#[test]
fn test_caret_followed_by_neither_letter_nor_digit_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("a")?;
assert_field_value_error(state.apply("^!x"));
assert_field_value_error(state.apply("^-1"));
assert_field_value_error(state.apply("^^"));
Ok(())
}
#[test]
fn test_caret_run_with_trailing_garbage_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
assert_field_value_error(state.apply("^2x|z"));
Ok(())
}
#[test]
fn test_caret_run_out_of_range_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
assert_field_value_error(state.apply("^99999999999999999999999999"));
Ok(())
}
#[test]
fn test_caret_run_overrunning_the_schema_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
assert_field_value_error(state.apply("z|^5"));
assert_eq!(state.field(0), Some(Some("a")));
Ok(())
}
#[test]
fn test_line_shorter_than_the_schema_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
assert_field_value_error(state.apply("z|y"));
assert_eq!(state.field(0), Some(Some("a")));
Ok(())
}
#[test]
fn test_line_longer_than_the_schema_is_an_error() {
let mut state = ItemState::new(2);
assert_field_value_error(state.apply("a|b|c"));
}
#[test]
fn test_diff_onto_a_null_field_is_an_error() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("#")?;
assert_field_value_error(state.apply("^Td"));
assert_eq!(state.field(0), Some(None));
Ok(())
}
#[test]
fn test_diff_onto_a_never_set_field_is_an_error() {
let mut state = ItemState::new(1);
assert_field_value_error(state.apply("^Td"));
}
#[test]
fn test_diff_onto_an_empty_field_is_accepted() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("$")?;
let outcome = state.apply("^Tadnew")?;
assert_eq!(state.field(0), Some(Some("new")));
assert_eq!(outcome.changed_fields(), &[0]);
Ok(())
}
#[test]
fn test_malformed_tlcp_diff_payloads_are_errors() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("foobar")?;
assert_field_value_error(state.apply("^T"));
assert_field_value_error(state.apply("^TAB"));
assert_field_value_error(state.apply("^T3"));
assert_field_value_error(state.apply("^Tazz"));
assert_field_value_error(state.apply("^TAdacAa"));
assert_eq!(state.field(0), Some(Some("foobar")));
Ok(())
}
#[test]
fn test_tlcp_diff_rejects_an_astral_base_through_item_state() -> Result<(), ProtocolError> {
let mut state = ItemState::new(1);
state.apply("a😀c")?;
assert_field_value_error(state.apply("^Tbbz"));
assert_eq!(state.field(0), Some(Some("a😀c")));
Ok(())
}
#[test]
fn test_a_rejected_update_leaves_every_field_untouched() -> Result<(), ProtocolError> {
let mut state = ItemState::new(3);
state.apply("a|b|c")?;
assert_field_value_error(state.apply("x|y|^"));
assert_eq!(state.field(0), Some(Some("a")));
assert_eq!(state.field(1), Some(Some("b")));
assert_eq!(state.field(2), Some(Some("c")));
Ok(())
}
}