ys memos

Blog

Rustで簡単なKVSを実装してみた ~3. Query実装~


rust

2023/11/04


  1. 概要
  2. 型定義など
  3. handler定義
  4. Query定義
  5. KvsServer実装
  6. main&使ってみる

ここでは、traitを用いた標準ライブラリの拡張の実装例、およびTryFrom<T>を用いたシンプルで直感的な型変換の例を含んでいる。


src/query/*の実装。ここは受け取ったリクエストを多様なmethodとして扱えるためのenumを定義している。

method (GET, SET)のパースやそのargsのサイズのバリデーションも含む。


コード配置については洗練の余地があるとは感じるが、現時点ではメソッド数が2つだけのため、シンプルさを重視し、必要最低限の分離に留めた。


Queryは、Methodとそれに対応するArgsのenumであり、複数の処理方式を対等に扱うためのレイヤー。

クライアントから受け取ったメッセージ (&str)を気軽にQueryに変換するメソッドとして、 TryFrom<&str>を実装した。

このResult<Query<'a>, Self::Error>KvsResult<Query<'a>>と同義であり、ショートハンドとしてそちらを使っても良いのだが、関連型を書き換えたときの混乱を防ぐために慣例に則りSelf::Errorとした。

へルパ関数として定義したparse_method(&str) -> KvsResult<(Method, &str)>は、リクエストメッセージの先頭のみをパースし、Methodとそれ以外に分離する役割を持つ。

src/query/mod.rs
mod args;
mod method;
mod str_ssv_array;

use std::str::FromStr;

pub use self::args::*;
use self::{method::Method, str_ssv_array::SsvArray};
use crate::error::{KvsError, KvsResult};

#[derive(Debug, PartialEq)]
pub enum Query<'a> {
    Get(GetArgs<'a>),
    Set(SetArgs<'a>),
}

impl<'a> TryFrom<&'a str> for Query<'a> {
    type Error = KvsError;

    fn try_from(s: &'a str) -> Result<Query<'a>, Self::Error> {
        let (m, s) = parse_method(s)?;
        match m {
            Method::Get => {
                let args = GetArgs::new(s.ssv_array()?);
                Ok(Query::Get(args))
            }
            Method::Set => {
                let args = SetArgs::new(s.ssv_array()?);
                println!("{}", s);
                Ok(Query::Set(args))
            }
        }
    }
}

/// query parser for plain message
/// must to be formatted as `<method> <key> <...args>`
fn parse_method(query: &str) -> KvsResult<(Method, &str)> {
    match query.split_once(' ') {
        Some((key, subquery)) => {
            let method = Method::from_str(key)?;
            Ok((method, subquery))
        }
        None => Err(KvsError::InvalidQueryFormat),
    }
}

#[cfg(test)]
mod tests {
    use crate::query::{
        args::{GetArgs, SetArgs},
        Query,
    };

    #[test]
    fn get_query() {
        let input = "GET t";
        let query = Query::try_from(input);
        assert!(query.is_ok());
        assert_eq!(query.unwrap(), (Query::Get(GetArgs::new(["t"]))));
    }

    #[test]
    fn set_query() {
        let query = "SET t 1";
        let parsed = Query::try_from(query);
        assert!(parsed.is_ok());
        assert_eq!(parsed.unwrap(), (Query::Set(SetArgs::new(["t", "1"]))));
    }
}

Methodは、GET,SETといったリクエストの処理タイプを意味するenumである。

enum Methodの各バリアントのコメントの// mutable, // immutableは必須ではないが、メソッドの見通しをよくするためにつけた。

Methodと文字列の関係性は、単なる見た目そのものの変換であり、FromStr, ToStringを介して相互に変換する。これによりリクエストメッセージのパースを行う。

src/query/method.rs
use std::str::FromStr;

use crate::error::KvsError;

/// Method for Kvs
#[derive(Debug, PartialEq)]
pub enum Method {
    Set, // mutable
    Get, // immutable
}

impl FromStr for Method {
    type Err = KvsError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "SET" => Ok(Method::Set),
            "GET" => Ok(Method::Get),
            _ => Err(KvsError::InvalidMethodName(s.into())),
        }
    }
}

impl ToString for Method {
    fn to_string(&self) -> String {
        match self {
            Self::Get => "GET".to_owned(),
            Self::Set => "SET".to_owned(),
        }
    }
}

#[cfg(test)]
mod tests {
    mod from_str {
        use std::str::FromStr;

        use crate::query::method::Method;

        fn valid_method(s: &str) -> Method {
            let method_res = Method::from_str(s);
            assert!(method_res.is_ok());
            method_res.unwrap()
        }

        #[test]
        fn valid_method_set() {
            assert_eq!(valid_method("SET"), Method::Set);
        }

        #[test]
        fn valid_method_get() {
            assert_eq!(valid_method("GET"), Method::Get);
        }

        #[test]
        fn invalid_method() {
            assert!(Method::from_str("HOGE").is_err());
        }
    }

    mod to_str {
        use crate::query::method::Method;

        #[test]
        fn method_get_to_string() {
            assert_eq!(Method::Get.to_string(), "GET");
        }

        #[test]
        fn method_set_to_string() {
            assert_eq!(Method::Set.to_string(), "SET");
        }
    }
}

ここでは、各Methodの引数を管理するための構造体群を定義している。

associated-itemsとして定数を準備し、それによって引数のサイズを確定させる。続いて記載する str_ssv_arrayの戻り値の配列サイズをここでバリデーションし、これにより不正な要素アクセスをコンパイル時に検知できるようになる。

これらのフィールドは、構造体生成後は不変のため、getterメソッドのみ提供する。これによりコードの見通しを改善する。

src/query/args.rs
#[derive(Debug, PartialEq)]
pub struct GetArgs<'a> {
    key: &'a str,
}

#[derive(Debug, PartialEq)]
pub struct SetArgs<'a> {
    key: &'a str,
    val: &'a str,
}

impl<'a> GetArgs<'a> {
    const SIZE: usize = 1;

    pub fn new(strs: [&'a str; GetArgs::SIZE]) -> Self {
        Self { key: strs[0] }
    }

    pub fn key(&self) -> &str {
        self.key
    }
}

impl<'a> SetArgs<'a> {
    const SIZE: usize = 2;

    pub fn new(strs: [&'a str; SetArgs::SIZE]) -> Self {
        Self {
            key: strs[0],
            val: strs[1],
        }
    }

    pub fn key(&self) -> &str {
        self.key
    }

    pub fn val(&self) -> &str {
        self.val
    }
}

これは、Queryパース時に用いるスペース区切り(SpaceSeparatedArray)のためのStr拡張。

どこに実装を配置してもいいが、Query関連のパースでしか使わないので、最低限の視認性のためここに配置する。

<const N: usize>は戻り値の配列のサイズであり、各argsに渡すタイミングで配列サイズを決定させる。 変換の要素数に対するOk/Errの判定をここで担い、続く処理での要素数に対する憂いを払拭する。

src/query/str_ssv_array.rs
use crate::error::{KvsError, KvsResult};

pub trait SsvArray {
    fn ssv_array<const N: usize>(&self) -> KvsResult<[&str; N]>;
}

impl SsvArray for str {
    fn ssv_array<const N: usize>(&self) -> KvsResult<[&str; N]> {
        let strs = self.split(' ').collect::<Vec<&str>>();
        match strs.try_into() {
            Ok(arr) => Ok(arr),
            Err(_) => Err(KvsError::InvalidPayloadSize(N)),
        }
    }
}

#[cfg(test)]
mod tests {
    mod parser {
        use crate::query::str_ssv_array::SsvArray;

        #[test]
        fn payloads_1() {
            let strs = "t".ssv_array::<1>();
            assert!(strs.is_ok());
            assert_eq!(strs.unwrap(), ["t"]);

            let strs = "t t".ssv_array::<1>();
            assert!(strs.is_err());

            let strs = "t t t".ssv_array::<1>();
            assert!(strs.is_err());
        }

        #[test]
        fn payloads_2() {
            let strs = "t".ssv_array::<2>();
            assert!(strs.is_err());

            let strs = "t t".ssv_array::<2>();
            assert!(strs.is_ok());
            assert_eq!(strs.unwrap(), ["t", "t"]);

            let strs = "t t t".ssv_array::<2>();
            assert!(strs.is_err());
        }
    }
}

0.概要#おわりにに集約します。


0.概要#参考に集約します。

関連タグを探す