Eliomを使用したOCamlおよびRESTful JSON API

こんにちは、Habr! Eliomを使用しRESTful JSON APIの翻訳を紹介します。



このチュートリアルでは、JSONをシリアル化形式として使用して、シンプルだが完全なREST APIを作成する方法を示します。



この例を説明するために、説明と座標(緯度と経度)を格納している場所のデータベースへのアクセスを提供するとします。



RESTfulであるために、インターフェイスは次の原則に準拠します。





これを念頭に置いて、私たちの目標はCRUD関数(作成、読み取り、更新、削除)を実装してリソースを処理することです。 次のクエリを有効にする必要があります。



GET http:// localhost /は、利用可能なすべての場所を返します。



GET http:// localhost / IDは、IDに関連付けられた場所を返します。



POST http:// localhost / ID with content:



{ "description": "Paris", "coordinates": { "latitude": 48.8567, "longitude": 2.3508 } }
      
      





この場所をデータベースに保存します。



PUT http:// localhost / IDは、一部のコンテンツとともに、識別子に関連付けられた場所を更新します。



DELETE http:// localhost / IDは、IDに関連付けられた場所を削除します。



依存関係





すでにEliomに精通していることが前提です。これはチュートリアルを完全に理解するために必要です。 このチュートリアルは、Eliomの紹介ではありません。

次のブラウザ拡張機能は、REST APIを手動で確認するのに役立ちます。





データ型



まず、データベースの種類、つまり場所と関連情報をどのように表現するかを定義することから始めましょう。 各場所には一意の任意の識別子が関連付けられ、説明と座標(緯度と経度で構成される)の情報も含まれます。



座標を10進度で表し、deriving-yojsonライブラリを使用してJSONの型を解析およびシリアル化します。



リクエストまたはリクエストの処理に問題がある場合に返される、強調表示されたエラータイプを使用します。



データベースについては、単純なOcsipersistテーブルを使用します。



 type coordinates = { latitude : float; longitude : float; } deriving (Yojson) type location = { description : string option; coordinates : coordinates; } deriving (Yojson) (* List of pairs (identifier * location) *) type locations = (string * location) list deriving (Yojson) type error = { error_message : string; } deriving (Yojson) let db : location Ocsipersist.table = Ocsipersist.open_table "locations"
      
      





サービス定義



まず、一般的なメンテナンスオプションを定義しましょう。





 let path = [] let get_params = Eliom_parameter.(suffix (neopt (string "id")))
      
      





次のステップは、サービスAPIを定義することです。 自由に使える4つのHTTPメソッドを使用して、同じパスでそれらの4つを定義します。





 let read_service = Eliom_service.Http.service ~path ~get_params () let create_service = Eliom_service.Http.post_service ~fallback:read_service ~post_params:Eliom_parameter.raw_post_data () let update_service = Eliom_service.Http.put_service ~path ~get_params () let delete_service = Eliom_service.Http.delete_service ~path ~get_params ()
      
      





ハンドラー



ハンドラーが使用するいくつかのヘルパー値と関数を使用して、ハンドラーの定義を始めましょう。



低レベル関数Eliom_registration.String.sendを使用して応答を送信するため、3つの特殊関数send_json、send_error、send_successに転送します(これにより、コンテンツなしで200 OKステータスコードのみが送信されます)。



別の機能は、結果のコンテンツタイプがMIMEタイプと一致することにより、期待されるコンテンツタイプであることを確認するのに役立ちます。 この例では、JSONを取得することを確認します。



read_raw_content関数は、指定または標準の長さの文字数をOcsigen raw_contentストリームから取得します。



 let json_mime_type = "application/json" let send_json ~code json = Eliom_registration.String.send ~code (json, json_mime_type) let send_error ~code error_message = let json = Yojson.to_string<error> { error_message } in send_json ~code json let send_success () = Eliom_registration.String.send ~code:200 ("", "") let check_content_type ~mime_type content_type = match content_type with | Some ((type_, subtype), _) when (type_ ^ "/" ^ subtype) = mime_type -> true | _ -> false let read_raw_content ?(length = 4096) raw_content = let content_stream = Ocsigen_stream.get raw_content in Ocsigen_stream.string_of_stream length content_stream
      
      





次に、必要なアクションを実行して応答を返すハンドラーを定義します。



POSTおよびPUTハンドラーは、JSONの元のコンテンツのコンテンツを読み取り、Yojsonを使用してタイプに変換します。



応答では、次の値を持つHTTPステータスコードを使用します。





GETハンドラーは、識別子が指定されている場合は単一の場所を返し、それ以外の場合はすべての既存の場所のリストを返します。



 let read_handler id_opt () = match id_opt with | None -> Ocsipersist.fold_step (fun id loc acc -> Lwt.return ((id, loc) :: acc)) db [] >>= fun locations -> let json = Yojson.to_string<locations> locations in send_json ~code:200 json | Some id -> catch (fun () -> Ocsipersist.find db id >>= fun location -> let json = Yojson.to_string<location> location in send_json ~code:200 json) (function | Not_found -> (* [id] hasn't been found, return a "Not found" message *) send_error ~code:404 ("Resource not found: " ^ id))
      
      





次に、非常によく似た振る舞いをするPOSTハンドラーとPUTハンドラーの汎用関数を作成しましょう。 唯一の違いは、存在しない識別子を持つPUT要求がエラーを返すことです(したがって、更新要求のみを受け入れ、作成要求を拒否します)が、POSTメソッドを使用した同じ要求は成功します(新しい場所が関連付けられて作成されます)識別子付き)。



 let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) = if not (check_content_type ~mime_type:json_mime_type content_type) then send_error ~code:400 "Content-type is wrong, it must be JSON" else match id_opt, raw_content_opt with | None, _ -> send_error ~code:400 "Location identifier is missing" | _, None -> send_error ~code:400 "Body content is missing" | Some id, Some raw_content -> read_raw_content raw_content >>= fun location_str -> catch (fun () -> (if create then Lwt.return_unit else Ocsipersist.find db id >>= fun _ -> Lwt.return_unit) >>= fun () -> let location = Yojson.from_string<location> location_str in Ocsipersist.add db id location >>= fun () -> send_success ()) (function | Not_found -> send_error ~code:404 ("Location not found: " ^ id) | Deriving_Yojson.Failed -> send_error ~code:400 "Provided JSON is not valid") let create_handler id_opt content = edit_handler_aux ~create:true id_opt content let update_handler id_opt content = edit_handler_aux ~create:false id_opt content
      
      





場所を削除するには、4番目のハンドラーが必要です。



 let delete_handler id_opt _ = match id_opt with | None -> send_error ~code:400 "An id must be provided to delete a location" | Some id -> Ocsipersist.remove db id >>= fun () -> send_success ()
      
      





サービス登録



最後に、Eliom_registration.Anyモジュールを使用してサービスを登録し、送信される応答を完全に制御します。 したがって、ハンドラーを定義するときに上記のように、リクエストの処理中に何が起こるかに応じて適切なHTTPステータスコードを送信できます(解析エラー、リソースが見つかりません...)。



 let () = Eliom_registration.Any.register read_service read_handler; Eliom_registration.Any.register create_service create_handler; Eliom_registration.Any.register update_service update_handler; Eliom_registration.Any.register delete_service delete_handler; ()
      
      





完全なソース



結果として得たすべて
 open Lwt (**** Data types ****) type coordinates = { latitude : float; longitude : float; } deriving (Yojson) type location = { description : string option; coordinates : coordinates; } deriving (Yojson) (* List of pairs (identifier * location) *) type locations = (string * location) list deriving (Yojson) type error = { error_message : string; } deriving (Yojson) let db : location Ocsipersist.table = Ocsipersist.open_table "locations" (**** Services ****) let path = [] let get_params = Eliom_parameter.(suffix (neopt (string "id"))) let read_service = Eliom_service.Http.service ~path ~get_params () let create_service = Eliom_service.Http.post_service ~fallback:read_service ~post_params:Eliom_parameter.raw_post_data () let update_service = Eliom_service.Http.put_service ~path ~get_params () let delete_service = Eliom_service.Http.delete_service ~path ~get_params () (**** Handler helpers ****) let json_mime_type = "application/json" let send_json ~code json = Eliom_registration.String.send ~code (json, json_mime_type) let send_error ~code error_message = let json = Yojson.to_string<error> { error_message } in send_json ~code json let send_success () = Eliom_registration.String.send ~code:200 ("", "") let check_content_type ~mime_type content_type = match content_type with | Some ((type_, subtype), _) when (type_ ^ "/" ^ subtype) = mime_type -> true | _ -> false let read_raw_content ?(length = 4096) raw_content = let content_stream = Ocsigen_stream.get raw_content in Ocsigen_stream.string_of_stream length content_stream (**** Handlers ****) let read_handler id_opt () = match id_opt with | None -> Ocsipersist.fold_step (fun id loc acc -> Lwt.return ((id, loc) :: acc)) db [] >>= fun locations -> let json = Yojson.to_string<locations> locations in send_json ~code:200 json | Some id -> catch (fun () -> Ocsipersist.find db id >>= fun location -> let json = Yojson.to_string<location> location in send_json ~code:200 json) (function | Not_found -> (* [id] hasn't been found, return a "Not found" message *) send_error ~code:404 ("Resource not found: " ^ id)) let edit_handler_aux ?(create = false) id_opt (content_type, raw_content_opt) = if not (check_content_type ~mime_type:json_mime_type content_type) then send_error ~code:400 "Content-type is wrong, it must be JSON" else match id_opt, raw_content_opt with | None, _ -> send_error ~code:400 "Location identifier is missing" | _, None -> send_error ~code:400 "Body content is missing" | Some id, Some raw_content -> read_raw_content raw_content >>= fun location_str -> catch (fun () -> (if create then Lwt.return_unit else Ocsipersist.find db id >>= fun _ -> Lwt.return_unit) >>= fun () -> let location = Yojson.from_string<location> location_str in Ocsipersist.add db id location >>= fun () -> send_success ()) (function | Not_found -> send_error ~code:404 ("Location not found: " ^ id) | Deriving_Yojson.Failed -> send_error ~code:400 "Provided JSON is not valid") let create_handler id_opt content = edit_handler_aux ~create:true id_opt content let update_handler id_opt content = edit_handler_aux ~create:false id_opt content let delete_handler id_opt _ = match id_opt with | None -> send_error ~code:400 "An id must be provided to delete a location" | Some id -> Ocsipersist.remove db id >>= fun () -> send_success () (* Register services *) let () = Eliom_registration.Any.register read_service read_handler; Eliom_registration.Any.register create_service create_handler; Eliom_registration.Any.register update_service update_handler; Eliom_registration.Any.register delete_service delete_handler; ()
      
      





ソース: Eliomを使用したRESTful JSON API



翻訳者から



OCamlコミュニティがより大きく成長し、言語がより速く成長し、言語が優れており、一部の場所では主流言語よりも優れていることを望みます。ここにはいくつかの利点があります:ネイティブで組み立てられ、その構文は非常に簡潔で理解しやすいです私にとってはHaskellよりも簡単ですが、一般的には上品です)、それはもちろんかなり便利な型システムであり、優れたOOPです。 この翻訳が誰かに役立つか、またはOCamlとそのエコシステムを調べさせてくれたら、それを試してみると、さらに翻訳をしたり、記事を書いたりすることができます。 PMでエラーを報告してください。



PS:

HabrでのOCamlとOcsigenについての入門記事で、おそらく初心者に馴染む価値があります。





もちろん、公式のマニュアルに精通している方が良いでしょう。記事は6〜7年前のものであるため、基本的な知識を得ることができます。特にOscigenに関する記事では、すべてがすぐそこにあることを保証できません。 すべての楽しい開発。



All Articles