crux_kv 0.11.0

Key-Value capability for use with crux_core
Documentation
use anyhow::Result;
use crux_core::{
    App as _, Command,
    macros::effect,
    render::{RenderOperation, render},
};
use serde::{Deserialize, Serialize};

use crate::{
    KeyValueOperation, KeyValueResult,
    command::KeyValue,
    error::KeyValueError,
    protocol::{KeyValueResponse, Value},
};

#[derive(Default)]
pub struct App;

#[derive(Debug, Serialize, Deserialize)]
pub enum Event {
    Get,
    Set,
    Delete,
    Exists,
    ListKeys,
    GetThenSet,

    GetResponse(Result<Option<Vec<u8>>, KeyValueError>),
    SetResponse(Result<Option<Vec<u8>>, KeyValueError>),
    ExistsResponse(Result<bool, KeyValueError>),
    ListKeysResponse(Result<(Vec<String>, u64), KeyValueError>),
}

#[derive(Debug, Default)]
pub struct Model {
    pub value: i32,
    pub keys: Vec<String>,
    pub cursor: u64,
    pub successful: bool,
}

#[derive(Serialize, Deserialize, Default)]
pub struct ViewModel {
    pub result: String,
}

impl crux_core::App for App {
    type Event = Event;
    type Model = Model;
    type ViewModel = ViewModel;

    type Effect = Effect;

    fn update(&self, event: Event, model: &mut Model) -> Command<Effect, Event> {
        let key = "test".to_string();
        match event {
            Event::Get => KeyValue::get(key).then_send(Event::GetResponse),
            Event::Set => {
                KeyValue::set(key, 42i32.to_ne_bytes().to_vec()).then_send(Event::SetResponse)
            }
            Event::Delete => KeyValue::delete(key).then_send(Event::SetResponse),
            Event::Exists => KeyValue::exists(key).then_send(Event::ExistsResponse),
            Event::ListKeys => {
                KeyValue::list_keys("test:".to_string(), 0).then_send(Event::ListKeysResponse)
            }

            Event::GetThenSet => Command::new(|ctx| async move {
                let Result::Ok(Some(value)) = KeyValue::get("test_num".to_string())
                    .into_future(ctx.clone())
                    .await
                else {
                    panic!("expected get response with a value");
                };

                let num = i32::from_ne_bytes(value.try_into().unwrap());
                let result =
                    KeyValue::set("test_num".to_string(), (num + 1).to_ne_bytes().to_vec())
                        .into_future(ctx.clone())
                        .await;

                ctx.send_event(Event::SetResponse(result));
            }),

            Event::GetResponse(Ok(Some(value))) => {
                let (int_bytes, _rest) = value.split_at(std::mem::size_of::<i32>());
                model.value = i32::from_ne_bytes(int_bytes.try_into().unwrap());

                Command::done()
            }

            Event::GetResponse(Ok(None)) => {
                panic!("expected value");
            }

            Event::SetResponse(Ok(_response)) => {
                model.successful = true;

                render()
            }

            Event::ExistsResponse(Ok(_response)) => {
                model.successful = true;
                render()
            }

            Event::ListKeysResponse(Ok((keys, cursor))) => {
                model.keys = keys;
                model.cursor = cursor;

                render()
            }

            Event::GetResponse(Err(error))
            | Event::SetResponse(Err(error))
            | Event::ExistsResponse(Err(error))
            | Event::ListKeysResponse(Err(error)) => {
                panic!("Error: {error:?}");
            }
        }
    }

    fn view(&self, model: &Self::Model) -> Self::ViewModel {
        ViewModel {
            result: format!("Success: {}, Value: {}", model.successful, model.value),
        }
    }
}

#[effect]
pub enum Effect {
    KeyValue(KeyValueOperation),
    Render(RenderOperation),
}

#[test]
fn test_get() {
    let app = App;
    let mut model = Model::default();

    let mut cmd = app.update(Event::Get, &mut model);

    cmd.expect_no_events();
    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::Get {
            key: "test".to_string()
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::Get {
                value: 42i32.to_ne_bytes().to_vec().into(),
            },
        })
        .expect("effect should resolve");

    let event = cmd.expect_one_event();
    app.update(event, &mut model).expect_no_effect_or_events();

    assert_eq!(model.value, 42);
}

#[test]
fn test_set() {
    let app = App;
    let mut model = Model::default();

    let mut cmd = app.update(Event::Set, &mut model);

    cmd.expect_no_events();
    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::Set {
            key: "test".to_string(),
            value: 42i32.to_ne_bytes().to_vec(),
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::Set {
                previous: Value::None,
            },
        })
        .expect("effect should resolve");

    let event = cmd.expect_one_event();
    app.update(event, &mut model)
        .expect_one_effect()
        .expect_render();

    assert!(model.successful);
}

#[test]
fn test_delete() {
    let app = App;
    let mut model = Model::default();

    let mut cmd = app.update(Event::Delete, &mut model);

    cmd.expect_no_events();
    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::Delete {
            key: "test".to_string()
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::Delete {
                previous: Value::None,
            },
        })
        .expect("effect should resolve");

    let event = cmd.expect_one_event();
    app.update(event, &mut model)
        .expect_one_effect()
        .expect_render();

    assert!(model.successful);
}

#[test]
fn test_exists() {
    let app = App;
    let mut model = Model::default();

    let mut cmd = app.update(Event::Exists, &mut model);

    cmd.expect_no_events();
    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::Exists {
            key: "test".to_string()
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::Exists { is_present: true },
        })
        .expect("effect should resolve");

    let event = cmd.expect_one_event();
    app.update(event, &mut model)
        .expect_one_effect()
        .expect_render();

    assert!(model.successful);
}

#[test]
fn test_list_keys() {
    let app = App;
    let mut model = Model::default();

    let mut cmd = app.update(Event::ListKeys, &mut model);

    cmd.expect_no_events();
    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::ListKeys {
            prefix: "test:".to_string(),
            cursor: 0,
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::ListKeys {
                keys: vec!["test:1".to_string(), "test:2".to_string()],
                next_cursor: 2,
            },
        })
        .expect("effect should resolve");

    let event = cmd.expect_one_event();
    app.update(event, &mut model)
        .expect_one_effect()
        .expect_render();

    assert_eq!(model.keys, vec!["test:1".to_string(), "test:2".to_string()]);
    assert_eq!(model.cursor, 2);
}

#[test]
pub fn test_kv_async() {
    let app = App;
    let mut model = Model::default();

    let mut cmd = app.update(Event::GetThenSet, &mut model);

    cmd.expect_no_events();
    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::Get {
            key: "test_num".to_string()
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::Get {
                value: 17u32.to_ne_bytes().to_vec().into(),
            },
        })
        .expect("effect should resolve");

    let mut request = cmd.expect_one_effect().expect_key_value();

    assert_eq!(
        request.operation,
        KeyValueOperation::Set {
            key: "test_num".to_string(),
            value: 18u32.to_ne_bytes().to_vec(),
        }
    );

    request
        .resolve(KeyValueResult::Ok {
            response: KeyValueResponse::Set {
                previous: Value::None,
            },
        })
        .expect("effect should resolve");

    let event = cmd.expect_one_event();
    app.update(event, &mut model)
        .expect_one_effect()
        .expect_render();

    assert!(model.successful);
}

#[test]
fn test_kv_operation_debug_repr() {
    {
        // get
        let op = KeyValueOperation::Get {
            key: "my key".into(),
        };
        let repr = format!("{op:?}");
        assert_eq!(repr, r#"Get { key: "my key" }"#);
    }

    {
        // set small
        let op = KeyValueOperation::Set {
            key: "my key".into(),
            value: b"my value".to_vec(),
        };
        let repr = format!("{op:?}");
        assert_eq!(repr, r#"Set { key: "my key", value: "my value" }"#);
    }

    {
        // set big
        let op = KeyValueOperation::Set {
            key: "my key".into(),
            value:
                // we check that we handle unicode boundaries correctly
                "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu😀😀😀😀😀😀".as_bytes().to_vec(),
        };
        let repr = format!("{op:?}");
        assert_eq!(
            repr,
            r#"Set { key: "my key", value: "abcdefghijklmnopqrstuvwxyz abcdefghijklmnopqrstu😀😀"... }"#
        );
    }

    {
        // set binary
        let op = KeyValueOperation::Set {
            key: "my key".into(),
            value: vec![255, 255],
        };
        let repr = format!("{op:?}");
        assert_eq!(
            repr,
            r#"Set { key: "my key", value: <binary data - 2 bytes> }"#
        );
    }
}