RustでRESTサービスを作成します。 パート5:ハンドラー、リファクタリング、およびマクロ

みなさんこんにちは!



Rustで引き続きWebサービスを作成します。 目次:



パート1:プロトタイプ

パート2:INIを読みます。 多錆

パート3:コンソールからデータベースを更新する

パート4:REST APIへの移行

パート5(これ):ハンドラー、リファクタリング、およびマクロ



次に、実際のAPIリクエストハンドラを見て、以前の怖いコードを書き換えます。 一般に、これはシリーズの最後の記事なので、リファクタリング、スタイル、マクロ、そしてすべてすべてがあります。 これが最も長い部分です。



Arcを2回クローン化した理由



APIパスを調整するコードは次のようになります。



let sdb = Arc::new(Mutex::new(db)); let mut router = router::Router::new(); { let sdb_ = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(sdb_.clone(), req)); } { let sdb_ = sdb.clone(); router.get("/api/v1/records/:id", move |req: &mut Request| handlers::get_record(sdb_.clone(), req)); } …
      
      





手始めに、ハンドラー自体。 ここで、たとえば、ハンドラー:: get_records():



ハンドラー:: get_records
 pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> { let url = req.url.clone().into_generic_url(); let mut name: Option<String> = None; if let Some(qp) = url.query_pairs() { for (key, value) in qp { match (&key[..], value) { ("name", n) => { if let None = name { name = Some(n); } else { return Ok(Response::with((status::BadRequest, "passed name in query more than once"))); } } _ => return Ok(Response::with((status::BadRequest, "unexpected query parameters"))), } } } else { return Ok(Response::with((status::BadRequest, "passed names don't parse as application/x-www-form-urlencoded or there are no parameters"))); } let json_records; if let Ok(recs) = ::db::read(sdb, name.as_ref().map(|s| &s[..])) { use rustc_serialize::json; if let Ok(json) = json::encode(&recs) { json_records = Some(json); } else { return Ok(Response::with((status::InternalServerError, "couldn't convert records to JSON"))); } } else { return Ok(Response::with((status::InternalServerError, "couldn't read records from database"))); } let content_type = Mime( TopLevel::Application, SubLevel::Json, Vec::new()); Ok(Response::with( (content_type, status::Ok, json_records.unwrap()))) }
      
      







その署名が、クロージャー内のデータベースでArcのクローンを作成する必要があった理由です。



 pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> {
      
      





ご覧のとおり、Arcは値によって(つまり、所有権付きで)ここに渡されますが、これは単純にコピーされるタイプではありません。 このため、Arcを複製してハンドラーに渡しました。



ハンドラーで何が起こるか



一般に、ハンドラーは同じタイプであるため、get_recordsを比較的詳細に見るだけです。これは最も複雑です。 ハンドラーはパターンマッチングを積極的に使用してエラー状況を判断します。



最初に、 Url Ironからrust-url形式のUrlを取得します。



  let url = req.url.clone().into_generic_url();
      
      





これを実行してquery_pairsメソッドを使用します。このメソッドは、URLをapplication / x-www-form-urlencodedデータとして解析し、(場合によっては)キーと値のペアの反復子を返します。



させて



次に、「if let」という新しい構文を示し、その本質を説明します。



  if let Some(qp) = url.query_pairs() { for (key, value) in qp {
      
      





このエントリの意味をすでに推測している可能性があります。 if letステートメントはパターンとの一致を試み、成功した場合、if letの後のブロックに実行を渡します。 このブロックでは、値を関連付けたばかりの名前(この場合はqp)を使用できます。 値をテンプレートと一致させることができなかった場合(query_pairs()がNoneを返しました)、elseブランチが実行されます-通常のifのように見えます。



無効なHTTPステータスを返す



したがって、イテレータが返されなかった場合、これはエラーです。



  } else { return Ok(Response::with((status::BadRequest, “passed names don't parse as application/x-www-form-urlencoded or there are no parameters”))); }
      
      





ここに、サーバーの応答を説明するかっこ内のタプルがあります:HTTPステータスとメッセージ。



リクエストパラメーターを取得する



イテレータが返された場合、それを回避して名前パラメータを取得し、名前変数に保存します。



  let mut name: Option<String> = None; if let Some(qp) = url.query_pairs() { for (key, value) in qp { match (&key[..], value) { ("name", n) => { if let None = name { name = Some(n); } else { return Ok(Response::with((status::BadRequest, "passed name in query more than once"))); } } _ => return Ok(Response::with((status::BadRequest, "unexpected query parameters"))), } } }
      
      





ここでループは、イテレータをバイパスし、ペアのベクトルから目的の要素を引き出し、所有権に問題がないようにするために必要です。 しかし、実際には、名前と呼ばれる要求パラメーターが1つだけ与えられていない状況はエラーです。 ループを削除してみましょう。



パラメーターによってサイクルを削除します



.query_pairs()は、実際にはOption <Vec <(String、String)>>を返します。 したがって、ベクトルの長さと単一のパラメーターの名前を簡単に確認できます。



  let mut name: Option<String> = None; if let Some(mut qp) = url.query_pairs() { if qp.len() != 1 { return Ok(Response::with((status::BadRequest, "passed more than one parameter or no parameters at all"))); } let (key, value) = qp.pop().unwrap(); if key == "name" { name = Some(value); } } else {
      
      





ここで、ベクトルを迂回するのではなく、その長さを確認し、興味のあるパラメーターを右に回します。



ここには重要なポイントがあります:



  let (key, value) = qp.pop().unwrap();
      
      





pop()を使用することが重要です-所有権を持つベクター要素を提供します。 通常のインデックス呼び出し(qp [0])はリンクを提供し、値をペアからSome(値)に移動してすべてを名前に入れることはできません。



文字列比較が&strで機能するのはなぜですか?



また、ベクターのペア(String、String)に格納されていることにも注意してください。 しかし、キーを「名前」と直接比較します-文字列リテラル:



  if key == "name" { name = Some(value); }
      
      





覚えているように、それは型& 'static strです。 これが機能するのは、StringがPartialEqを実装して& 'a strと比較するためです。



 impl<'a> PartialEq<&'a str> for String
      
      





したがって、ここでは型変換は行われません。



そのような型がなかった場合、スライス構文を使用して文字列を&strに変換できます。&キー[..]は、文字列全体に沿ってスライスを返します。つまり、 リンクは同じコンテンツの&strです。



次に、データベースへの実際のアクセスを行います。



初期化されていない変数-危険ですか?



まず、RESTアクセスポイントが返すJSONレコードの名前を宣言します。



  let json_records;
      
      





うーん、値で初期化しませんか? 自分の足で撮影したいですか?



いいえ、Rustは宣言された名前を初期化するまで使用しません。 たとえば、そのようなコードでは



 fn main() { let a; if true { a = 5; } else { println!("{}", a); } }
      
      





エラーが発生します:



 test.rs:6:24: 6:25 error: use of possibly uninitialized variable: `a` [E0381] test.rs:6 println!("{}", a); ^
      
      







データベースからレコードを読み取ります。 Option :: mapを使用します



次に、データベースからエントリを読み取ります。



  if let Ok(recs) = ::db::read(sdb, name.as_ref().map(|s| &s[..])) {
      
      





議論の中で奇妙なことが起こっているのはなぜですか?



  name.as_ref().map(|s| &s[..])
      
      





これから説明します。 まず、:: db :: read()の署名を見てください:



 pub fn read(sdb: Arc<Mutex<Connection>>, name: Option<&str>) -> Result<Vec<Record>, ()> {
      
      





ご覧のとおり、名前はOption<&str>



形式で名前を取りOption<&str>



。 名前のタイプはOption<String>



です。 しかし、それは重要ではありません.as_ref()



メソッドは、 Option<T>



Option<&T>



変換しOption<&T>



-このようにして、 Option<&String>



を取得します。



残念ながら、なぜなら &String



はオプションにラップされ、自動的に&str



変換されません。 したがって、匿名関数では上記のスライス構文を使用します。



  .map(|s| &s[..])
      
      





.mapは、関数をOptionのコンテンツに適用し、TをOption<T>



から他のタイプに変換します。 この場合、 &String



&str



変換し&str



。 これはHaskell fmap :: Functor f => (a -> b) -> fa -> fb



似ています。



微妙な点があります。 name: Option<T>



で.mapをすぐに呼び出せませんでした。なぜなら、 リンクは、呼び出されたときに関数パラメーターのスコープ内でのみ有効になります。 この場合、クロージャ内にリンクを取得し、クロージャが存続している間のみリンクが存続します。 ただし、どこにも保存されず、パラメーターを関数に渡すと破棄されます。 このようなリンクは一時オブジェクトになります。



 handlers.rs:25:53:25:54エラー: `s`は十分な長さではありません
 handlers.rs:25 if Ok(recs)= :: db :: read(sdb、name.map(| s |&s [..])){
                                                                    ^
 handlers.rs:25:23:25:60注:参照は25:22の呼び出しに対して有効でなければなりません...
 handlers.rs:25 if Ok(recs)= :: db :: read(sdb、name.map(| s |&s [..])){
                                      ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 handlers.rs:25:52:25:58注:...ただし、借用した値は25:51の関数のパラメーターのスコープでのみ有効です
 handlers.rs:25 if Ok(recs)= :: db :: read(sdb、name.map(| s |&s [..])){
                                                                   ^ ~~~~~ 


.as_ref()の場合、Option自体が存続している間にリンクが存続するため、すべてが機能します。



しかし、マルチスレッドについてはどうでしょうか?



:: db ::を見て、賞賛されているデータレーシング保護がどのように機能するかを読んでみましょう。



  if let Ok(rs) = show(&*sdb.lock().unwrap(), name) { Ok(rs) } else { Err(()) }
      
      





showを呼び出したい:



 pub fn show(db: &Connection, arg: Option<&str>) -> ::postgres::Result<Vec<Record>> {
      
      





この関数はConnectionへのリンクを受け入れ、 Arc<Mutex<Connection>>



ます。 参照カウンタを展開してミューテックスをキャプチャしない限り、関心のあるデータベース接続にアクセスできません。 型システムは無効な状態を表現できないようにします。



ほとんど魔法



そのため、ミューテックスを引き継ぎたいです。 参照カウンターにネストされています。 ここでは、2つのことが関係します。逆参照時の変換と、メソッド呼び出し時の自動逆参照です。



今のところ、奇妙な&*を無視して、sdb.lock()自体を見てください。 sdbはArcですが、 Arc<T>



Deref<T>



実装しています。



 impl<T> Deref for Arc<T> where T: ?Sized type Target = T fn deref(&self) -> &T
      
      





したがって、必要に応じてArc<T>



は自動的に&Tに変換されます。 これにより、 &Mutex<Connection>



が得られます。



次に、メソッドを呼び出すときに自動逆参照が作用します。 要するに、コンパイラはメソッド呼び出しと同数の逆参照を挿入します。



以下に簡単な例を示します。



 struct Foo; impl Foo { fn foo(&self) { println!("Foo"); } } let f = Foo; f.foo(); (&f).foo(); (&&f).foo(); (&&&&&&&&f).foo();
      
      





最後の4行はすべて同じことを行います。



RAIIを使用したミューテックスセーフリリース



Mutex :: lockは、 LockResult<MutexGuard<T>>



を返しLockResult<MutexGuard<T>>



。 結果によりエラーを処理でき、 MutexGuard<T>



はRAII値であり、 MutexGuard<T>



作用を停止するとすぐにミューテックスを自動的に開きます。



同じ&*



MutexGuard<T>



を&Tに変換しMutexGuard<T>



-最初にそれを逆参照してTを取得し、次にアドレスを取得して通常のリンク&Tを取得します。



なぜlock()がArc<Mutex<Connection>>



で直接機能し、MutexGuardを手動で変換する必要があるのですか? ロックはメソッドであり、メソッド呼び出しは実際にリンクを逆参照するだけでなく、一部のリンクを他のリンクに変換する(つまり、 &*



類似する)ためです。 関数に引数を渡す場合、これは手動で行う必要があります。



連載



レコードを受け取ったら、それらをJSONにシリアル化します。 これを行うには、 rustc_serializeを使用します



  use rustc_serialize::json;
      
      





ご覧のとおり、モジュールをグローバルにインポートできるだけでなく、単一の関数またはブロックのスコープ内でもインポートできます。 これは、グローバル名前空間を詰まらせないようにするのに役立ちます。



シリアル化自体は、次のコードで実行されます。



  if let Ok(json) = json::encode(&recs) { json_records = Some(json); } ...
      
      





この場合、シリアライザーコードは自動的に生成されます! レコードの型をシリアライズ可能(および同時にデシリアライズ可能)として宣言するだけです。



 #[derive(RustcEncodable, RustcDecodable)] pub struct Record { id: Option<i32>, pub name: String, pub phone: String, }
      
      







すべてを送り返します



最後に、適切なヘッダーを使用してJSONを正しいHTTPでラップして返します。



  let content_type = Mime( TopLevel::Application, SubLevel::Json, Vec::new()); Ok(Response::with( (content_type, status::Ok, json_records.unwrap())))
      
      





残りのハンドラーも同様に機能するため、自分で繰り返すのではなく、コードをリファクタリングします。



一般的に、私たちのプログラムは終わりました! これで、すばらしい電話帳をコマンドラインからだけでなく、流行のWeb APIからも更新できます。 動作を確認するには、 GitHubの feature-completeタグを使用してコードのバージョンを取得します



リファクタリングはそれほど複雑ではありません。Rustコードも美しいことを確信させるために、このプロセスを示します。 機能を実装するプロセスで私たちが別れた読み取り不能な混乱は、単にラッシュの結果です。 Rustはこれを責めるものではありません。エレガントなコードを書くことができます。



クローンをクローンしません



まず、前のパートで説明した二重アーククローンについて説明します。



  { let sdb_ = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(sdb_.clone(), req)); }
      
      





勝つことは非常に簡単です。 ハンドラーの署名を変更します:: get_records with



 pub fn get_records(sdb: Arc<Mutex<Connection>>, req: &mut Request) -> IronResult<Response> {
      
      









 pub fn get_records(sdb: &Mutex<Connection>, req: &mut Request) -> IronResult<Response> {
      
      





そして一般的に、ハンドラーとデータベース関数では、どこでも&Mutex<Connection>



を使用します。 すべて、二重クローニングはもう必要ありません。



  { let sdb = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(&*sdb, req)); }
      
      





巨大なメイン​​も一見の価値があります。 すべてのアクションを関数に取り込んで、クールでコンパクトなメインを取得します。



 fn main() { let (params, sslmode) = params(); let db = Connection::connect(params, &sslmode).unwrap(); init_db(&db); let args: Vec<String> = std::env::args().collect(); match args.get(1) { Some(text) => { match text.as_ref() { "add" => add(&db, &args), "del" => del(&db, &args), "edit" => edit(&db, &args), "show" => show(&db, &args), "help" => println!("{}", HELP), "serve" => serve(db), command => panic!( format!("Invalid command: {}", command)) } } None => panic!("No command supplied"), } }
      
      







rustfmt!



最後に、甘い: rustfmt ! Rustソースコードフォーマットユーティリティはまだ完成していませんが、小さなプロジェクトのコードを装飾するのにすでに適しています。



リポジトリのクローンを作成したら、cargo build --releaseを作成し、結果の実行可能ファイルを$ PATHのどこかにコピーします。 次に、プロジェクトのルートで、



 $ rustfmt src / main.rs 


そしてそれだけです、 プロジェクト全体のコード即座にフォーマットされます! rustfmtは他のモジュールへのリンクをたどり、それらもフォーマットします。



gofmtとは異なり、rustfmtを使用すると、ソースを書き換えるかなり詳細なスタイルを設定できます。



現在のデフォルトスタイルは、コンパイラが記述されているスタイルに似ています。 ただし、 公式のスタイルガイドが完成すると、rustfmtも更新されます。



この「合理的な」リファクタリングは終わり、始まります...物議を醸すものですが、間違いなく楽しいものです。マクロを使用して同様のコードの残りの繰り返しを削除しましょう。



マクロ



私はどのような繰り返しについて話しているのですか? これについて:



  { let sdb = sdb.clone(); router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(&*sdb, req)); } { let sdb = sdb.clone(); router.get("/api/v1/records/:id", move |req: &mut Request| handlers::get_record(&*sdb, req)); } { let sdb = sdb.clone(); router.post("/api/v1/records", move |req: &mut Request| handlers::add_record(&*sdb, req)); } { let sdb = sdb.clone(); router.put("/api/v1/records/:id", move |req: &mut Request| handlers::update_record(&*sdb, req)); } { let sdb = sdb.clone(); router.delete("/api/v1/records/:id", move |req: &mut Request| handlers::delete_record(&*sdb, req)); }
      
      





明らかに、ここにはコードに反映できなかったいくつかの高レベルの構造があります。 これらのブロックは、ルーターが呼び出す必要のあるメソッドが異なるため、通常の関数でこれらのオプションをすべて処理するために、引数に応じて対応するメソッドを呼び出す列挙によって照合する必要があります。



これは、一般的に言えば、オプションであり、このコードを職場で書いた場合、おそらくこれを実行しようとしますが、ここでは楽しんでおり、Rustでマクロを試してみたかったのです。 それでは始めましょう。



まず、ここでの繰り返し構造は、Arcを複製してからステートメントを実行するブロックです。 対応するマクロを書きましょう:



 macro_rules! clone_pass_bound { ($arc:ident, $stmt:stmt) => { { let $arc = $arc.clone(); $stmt; } } }
      
      





最初の行は、clone_pass_boundというマクロの定義を開始したことを示しています。 バカな名前ですが、うまくいきませんでした。 これはそれ自体が症状であり、おそらく動作中のコードでは実行すべきではありません。 しかし、さあ-これは今私たちのケースではありません。



Rustのマクロには型が付けられており、2つの引数-型識別子の$ arcと型演算子の$ stmt(文、stmt)を取ります。 よく見ると、一致するマクロの定義の類似性に気付くことができます-ここでは、引数の特定の組み合わせが特定の本体と比較されます。 マクロには、matchのような多くのブランチを含めることができます。これは、再帰の場合に役立ちます。



矢印の後に、2組のブレースがあります。 マクロ記述の構文に応じて、いくつかは-通常、通常の一致の場合と同様に必要です。



2番目のペアを使用して、マクロがブロックに展開されると言います。 ブロック内で、sdbを$ arcに置き換えて、ほぼ通常のコードを記述します。 これは簡単な一般化です。 クローニングの後にオペレーターが続きます。



これがこのマクロの呼び方です:



  clone_pass_bound!( sdb, router.get("/api/v1/records", move |req: &mut Request| handlers::get_records(&*sdb, req)));
      
      





これまでのところ、ボリュームに関しては何も保存していませんが、理解できない電話しか受けていません。 しかし、絶望しないでください-私たちは始まったばかりです!



マクロ上のマクロ



これで、データベースへの接続、ルーター、追加するメソッド(get、postなど)、定義したハンドラーの名前の4つのパラメーターを使用して1つのハンドラーを記述できることが明らかになりました。 このためのマクロを書きましょう。



 macro_rules! define_handler { ($connection:ident, $router: ident.$method:ident, $route:expr, $handler:path) => { clone_pass_bound!( $connection, $router.$method( $route, move |req: &mut Request| $handler(&*$connection, req))); } }
      
      





ここでは、まず、マクロ呼び出しと通常のパターンマッチングの類似性をもう一度強調する価値があります。 ご覧のとおり、マクロ引数の区切り文字はコンマである必要はありません。通常のコードとの類似性を高めるために、ルーターとそのメソッドをドットで区切りました。



次に、すべての具体的な名前をマクロのメタ変数に置き換えて、以前のマクロを呼び出します-それほど怖くも難しくもありません。 通常、これらのマクロは両方とも最初の試行で作成しました。



これで非常識なマクロを20行作成し、最終的にカットしたいコードが減少し始めました。



  define_handler!(sdb, router.get, "/api/v1/records", handlers::get_records); define_handler!(sdb, router.get, "/api/v1/records/:id", handlers::get_record); define_handler!(sdb, router.post, "/api/v1/records", handlers::add_record); define_handler!(sdb, router.put, "/api/v1/records/:id", handlers::update_record); define_handler!(sdb, router.delete, "/api/v1/records/:id", handlers::delete_record);
      
      





これは制限ではありません。最後のマクロを定義します。これにより、定義が非常にコンパクトになり、非常に理解しやすくなります。 これで、コードの変更部分が非常に明白になり、コードを完全に乾燥させるのに何も妨げられません。



最後のマクロでは、わずか1つの重要な瞬間があります。



マクロドライブ



最後のマクロは次のようになります。



 macro_rules! define_handlers_family { ($connection:ident, $router:ident, $( [$method:ident, $route:expr, $handler:path]),+ ) => { $( define_handler!($connection, $router.$method, $route, $handler); )+ } }
      
      





かなり小さいです。 重要な点は、引数に再現性を導入したことです:



  ($connection:ident, $router:ident, $( [$method:ident, $route:expr, $handler:path]),+ ) => {
      
      





$( … ),+



は、このマクロが呼び出されたときに、括弧で囲まれたグループを1回以上繰り返す必要があることを意味します。 正規表現のように見えます。



次は、モンスターマクロの本体です。 最初に私はこのように書いた:



  define_handler!($connection, $router.$method, $route, $handler);
      
      





コンパイラが反対したこと:



 main.rs:134:46: 134:53 error: variable 'method' is still repeating at this depth main.rs:134 define_handler!($connection, $router.$method, $route, $handler); ^~~~~~~
      
      





先ほど言ったように、$メソッド、$ルート、$ハンドラを定義する呼び出しの部分は繰り返すことができます。 Rustマクロには、呼び出しの繰り返しの特定の「レベル」にあるメタ変数を使用する場合、同じ繰り返しレベルにする必要があるというルールがあります。



このように考えることができます-マクロ呼び出しパラメーターのタプルは、対応するボディと同時に移動されます。 つまり 1つのパラメーターセットは1つのボディに対応する必要があります。 したがって、マクロの構造を理解しやすくなります-体は挑戦のようになります。



これで、マクロが1つだけのように記録されました。呼び出しパラメーターは繰り返されますが、ボディを繰り返すことはできません。 それでは、本体に$メソッドを正確に配置する必要がありますか? 明確ではありません。 ここでは、そのような状況を回避するために、パラメータをボディと「段階的に」列挙するルールが考案されました。



私たちにとって、これはすべて、パラメーターと同じ再現性修飾子でボディをラップする必要があることを意味します。



  $( define_handler!($connection, $router.$method, $route, $handler); )+
      
      





これで、$メソッド、$ルート、および$ハンドラーが重複パラメーターに対応します。 また、$接続と$ルーターは「グローバル」です。これらは繰り返し性の修飾子ではないため、各本体で複製されます。



この洗脳の報酬として、APIのすべてのパスの美しい定義を取得します。



  define_handlers_family!( sdb, router, [get, "/api/v1/records", handlers::get_records], [get, "/api/v1/records/:id", handlers::get_record], [post, "/api/v1/records", handlers::add_record], [put, "/api/v1/records/:id", handlers::update_record], [delete, "/api/v1/records/:id", handlers::delete_record]);
      
      





不必要な重複はなく、最終バージョンでは、初心者には比較的明確に見えます。



Rustのマクロは衛生的であることに注意してください-マクロ内の名前と外部の名前の衝突は除外されます。



そうそう、ほとんど忘れてしまった-コンパイラオプション--pretty-print = expandはマクロのデバッグに大いに役立つ。 そのため、マクロを標準出力ストリームに展開した後、コードを出力します。 CおよびC ++コンパイラの-Eオプションに似ています。



じゃあね!



それだけです これですべてです。このシリーズの記事は、Webを含むRustでコードの構築を開始できるほど十分に語ったと思います。



すでにRustで何かを始めている場合は、コメントに書いてください。 また、途中で発生する質問でチャットルームにアクセスします。彼らはそこで喜んでお手伝いします。



All Articles