Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Hypermedia: The Missing Element to Building Adaptable Web APIs in Rails (増補日本語版)

3,109 views

Published on

RubyKaigi 2014での発表に、当日カットしたスライドを加えて「RESTful Web APIs 読書会」で話したものです。
http://www.circleaf.com/events/155

オリジナルバージョン http://www.slideshare.net/tkawa1/rubykaigi2014-hypermedia-the-missing-element

Published in: Technology
  • Be the first to comment

Hypermedia: The Missing Element to Building Adaptable Web APIs in Rails (増補日本語版)

  1. 1. HYPERMEDIA: THE MISSING ELEMENT to Building Adaptable Web APIs in Rails ハイパーメディア: RailsでWeb APIをつくるには、これが足りない Toru Kawamura @tkawa ! RubyKaigi 2014 RESTful Web APIs 読書会 #19 2014.10.09
  2. 2. @tkawa Toru Kawamura • フリーランス Ruby/Rails プログラマ • Technology Assistance Partner at SonicGarden Inc. • REST厨 (RESTafarian) inspired by Yohei Yamamoto (@yohei) • Sendagaya.rb 共同主催 • “RESTful Web APIs” 読書会主催
  3. 3. Web API
  4. 4. “Web” http://www.opte.org/the-internet/
  5. 5. http://pixabay.com/en/spider-web-net-grid-silk-drops-13516/ 「クモの巣」
  6. 6. https://www.flickr.com/photos/tamaki/260594564/ 「クモの糸」
  7. 7. • プライベート • 内部から使われる • SPAや専用の クライアントが使う • だいたい予想できる コントロールできる • パブリック • 外部から使われる • 汎用的な目的の クライアントが使う • 予想しづらい コントロールしづらい
  8. 8. – 「WebAPIのこれまでとこれから」by @yohei http://www.slideshare.net/yohei/webapi-36871915
  9. 9. 今回は「クモの巣」メインの 話です http://pixabay.com/en/spider-web-net-grid-silk-drops-13516/
  10. 10. Change
  11. 11. 変化は避けられない ! Web APIは変化に適応しなければならない
  12. 12. 2種類の Change バージョンが変わるバージョンが変わらない 互換性がない互換性がある クライアントを壊すクライアントを壊さない
  13. 13. 2種類の Change バージョンが変わるバージョンが変わらない 互換性がない互換性がある クライアントを壊すクライアントを壊さない 壊す Change 壊さない Change
  14. 14. 壊すChangeはよくない • ひどいユーザ体験を生む • クライアント開発者にコードの書き直し・ 再デプロイを強いる • もし     だったら…
  15. 15. なぜ起こる? バージョンが変わるバージョンが変わらない 互換性がない互換性がある クライアントを壊すクライアントを壊さない 壊す Change 壊さない Change
  16. 16. 人間が読める説明書から作られる クライアントがたくさんある GET /v1/statuses?id=#{id} GET /v1/statuses?id=#{id}
  17. 17. GET /v2/statuses/#{id} GET /v1/statuses?id=#{id} ×コードの書き直しが必要
  18. 18. 機械が読める説明書から作られる クライアントもある { "apiVersion": "1.0.0", "basePath": "http:// petstore.swagger.wordnik.com/api", "resourcePath": "/store", "produces": [ "application/json" ], "apis": [ { "path": "/store/order/{orderId}", "operations": [ { GET /v1/statuses?id=#{id} GET /v1/statuses?id=#{id} "method": "GET", "summary": "Find purchase order by ID", "notes": "For valid response try integer IDs with value <= 5. Anything above 5 or nonintegers will generate API errors", "type": "Order", "nickname": "getOrderById", "authorizations": {}, "parameters": [
  19. 19. GET /v2/statuses/#{id} GET /v1/statuses?id=#{id} ×コードの再生成が必要 { "apiVersion": "2.0.0", "basePath": "http:// petstore.swagger.wordnik.com/api", "resourcePath": "/store", "produces": [ "application/json" ], "apis": [ { "path": "/store/order/{orderId}", "operations": [ { "method": "GET", "summary": "Find purchase order by ID", "notes": "For valid response try integer IDs with value <= 5. Anything above 5 or nonintegers will generate API errors", "type": "Order", "nickname": "getOrderById", "authorizations": {}, "parameters": [
  20. 20. { uber: { version: "1.0", data: [{ url: "http://www.ishuran.dev/notes/1", name: "Article", data: [ { name: "articleBody", value: "First note's text" }, { name: "datePublished", value: null }, { name: "dateCreated", value: "2014-09-11T12:00:31+09:00" }, { name: "dateModified", value: "2014-09-11T12:00:31+09:00" }, { name: "isPartOf", rel: "collection", url: "/notes"
  21. 21. { uber: { 「密結合」が原因 version: "1.0", data: [{ url: "http://www.ishuran.dev/notes/1", name: "Article", data: [ • APIの変更がクライアントに反映されればよい { name: "articleBody", value: "First note's text" }, { name: "datePublished", value: null }, { name: "dateCreated", value: "2014-09-11T12:00:31+09:00" }, { name: "dateModified", value: "2014-09-11T12:00:31+09:00" }, { name: "isPartOf", rel: "collection", url: "/notes" • APIの説明を分割して各レスポンスに埋め込む のが良い方法 • APIについての多大な仮定は密結合を生む
  22. 22. バージョンが変わるバージョンが変わらない 互換性がない互換性がある クライアントを壊すクライアントを壊さない 壊す Change 壊さない Change 密結合による疎結合による
  23. 23. 例で見る疎結合: FizzBuzzaaS • by Stephen Mizell http://fizzbuzzaas.herokuapp.com/ http://smizell.com/weblog/2014/solving-fizzbuzz-with-hypermedia • サーバは与えられた100までの数の FizzBuzzを計算できる • サーバは次のFizzBuzzが何になるか知っ ている • クライアントは1から最後まで順番にす http://sef.kloninger.com/posts/ べてのFizzBuzzが欲しい 201205fizzbuzz-for-managers. html
  24. 24. 密結合なクライアント answer = HTTP.get("/v1/fizzbuzz?number=#{i}") puts answer end "/v2/fizzbuzz/#{i}" (1..1000) (1..100).each do |i| • すべてのURLとパラメータがハードコードされている • カウントアップのような、サーバロジックと同じこと をクライアントでも行っている
  25. 25. 疎結合なクライアント root = HTTP.get_root answer = root.link('first').follow puts answer while answer.link('next').present? answer = answer.link('next').follow puts answer end next リンクが重要 • ハードコードされたURLなし • URLや条件を変えてもクライアントは壊れ ない
  26. 26. • 実際にリンクをたど る代わりに、 埋め込みリソースを 使うことで リクエスト数を減ら すことが可能 http://techlife.cookpad.com/entry/2014/09/08/093000
  27. 27. 「APIコール」のメタファーは危険 • クライアントがあらかじめURLとパラメータを用意 してAPIを呼ぶ、というRPCのようなパラダイムから 離れよう • クライアントが次にすることは、レスポンスの中の リンクから選ぶこと == ハイパーメディア
  28. 28. これは想像上のものではなく すでにHTMLにある
  29. 29. HTMLのWeb • WebアプリやWebサイ トはずっと変わり続け ているが、ブラウザは 壊れていない • HTMLのWebでは、なぜ HTMLのリンクブラウザは壊れない? http://www.youtypeitwepostit.com/messages
  30. 30. HTMLの中のデータ • HTMLが表現する意味は 「ヒューマンリーダブルなドキュメント」 • 段落、リスト、表、セクション、… • これが人間にゆるく伝わればよい
  31. 31. HTMLの中のワークフロー • Webアプリはワークフ ロー(の提案)を含む • ワークフローは一連の 画面遷移で表現される — リンク と フォーム ”RESTful Web APIs” p.11 Figure 1-7
  32. 32. ハイパーメディアはワークフローを示す • 各画面は次に何ができる かの「メニュー」としてリ ンクやフォームを含む • ブラウザはその「メニュー」 の中から選んで次へ進む • これがハイパーメディア で、FizzBuzzaaSもやってい たこと 3 4
  33. 33. もしHTMLにリンクがなかったら? 代わりにワークフロー手順書がある? • 各WebアプリごとにURLやパラ メータをハードコードした専用 クライアントを作りたくなる • クライアントとサーバのコード が密結合して、変更できない Webアプリになってしまう • これが今のWeb APIがやってい ること メッセージWebアプリ利用の手順書 1. アドレスバーに /messages と入力し て GET 2. アドレスは /messages のまま、 title と body のパラメータに文字列を セットして POST 3. message-id を受け取って、アドレス バーに /messages/{message-id} と入 力して GET
  34. 34. クローラにはもう1つヒントが • クローラはリンクをたどる(submitできるフォームもある) • クローラはHTMLドキュメントの中にあるデータと その「意味」を理解している • どうやって? https://support.google.com/webmasters/answer/99170
  35. 35. Microdata
  36. 36. Microdata <div itemscope itemtype="http://schema.org/Person"> My name is <span itemprop="name">Bob Smith</span> but people call me <span itemprop="nickname">Smithy</span>. Here is my home page: <a href="http://www.example.com" itemprop="url">www.example.com</a> I live in Albuquerque, NM and work as an <span itemprop="title">engineer</span> at <span itemprop="affiliation">ACME Corp</span>. </div> • 構造化データをHTMLドキュメントに埋め込むしくみ • データを変えることなくドキュメントの構造を変えられる • データをURLに結びつけることで、大まかな「データの意 味」も表す(これもリンクの一種)
  37. 37. Microdata <div itemscope itemtype="http://schema.org/Person"> My name is <span itemprop="name">Bob Smith</span> but people call me <span schema.itemprop="org nickname">Smithy</span>. Here is my home page: <a href="http://www.example.com" itemprop="url">www.example.com</a> I live in Albuquerque, 標準語彙NM (and ボキャブラリー) work as an <span itemprop="at <span itemprop="by Bing, affiliation">Google, title">Yahoo! engineer</ACME Corp</and span> span>. Yandex </div> • 構造化データをHTMLドキュメントに埋め込むしくみ • データを変えることなくドキュメントの構造を変えられる • データをURLに結びつけることで、大まかな「データの意 味」も表す(これもリンクの一種) http://getschema.org/index.php/Main_Page
  38. 38. schema.org • フィールド名・カラム名は変わるが、標準名は変わらない • 変えたときに壊れないように、変わらない基盤としての 標準と結びつける • schema.org はコーポレートスタンダードの1つといえる Bing, Google, Yahoo! and Yandex • 現在700種類以上のデータタイプを定義
  39. 39. 変化に適応するために必要なもの data link form HTML - ✓ ✓ HTML +Microdata ✓✓ ✓ ✓ • APIには構造化データが必要 ✓✓: 「データの意味」を含む • 柔軟なワークフローにはリンクとフォームが必要
  40. 40. HTMLでWeb APIを作ることもできる var user = document.getItems('http://schema.org/Person')[0]; var name = user.properties['name'][0].itemValue; alert('Hello ' + name + '!'); • “Microdata DOM API” でHTMLからデータを抽出できる http://www.w3.org/TR/microdata/#using-the-microdata-dom-api • JavaScriptの実装: https://github.com/termi/Microdata-JS • MicrodataからJSONに変換する仕様もいくつかある • HTMLはリンクとフォームを持っているのが大きなアドバンテージ
  41. 41. でもきっとJSON Web APIが欲しいはず data link form HTML +Microdata ✓✓ ✓ ✓ JSON ✓ - - ✓✓: 「データの意味」を含む • リンクとフォームを埋めればいい (できればデータの意味も)
  42. 42. JSONでリンク・フォームを表す • リンクやフォームを表 現できるJSONベースの フォーマットを使う • 他にも Siren, Collection+JSON, Mason, Verbose など… data link form JSON ✓ - - JSON +Link header ✓ ✓ - HAL ✓ ✓ - JSON-LD ✓✓ ✓ - JSON-LD +Hydra ✓✓ ✓ ✓ UBER ✓ ✓ ✓ ✓✓: 「データの意味」を含む
  43. 43. JSONでデータの意味を表す: 「プロファイル」 • ALPSプロファイル • MicrodataをHTML以外のどんなフォーマットにも適用 可能にする • JSON-LDコンテキスト • ドキュメントとコンテキストの両方を同じ1つの フォーマットで扱える
  44. 44. A Solution
  45. 45. Hypermicrodata gem https://github.com/tkawa/hypermicrodata • サーバサイドでHTMLをJSONに変換 • Microdataだけではなく リンクとフォームもHTMLから抽出 • ベースのALPSプロファイルを用意して、データの意味 も表しやすい形でJSONベースのフォーマットを生成
  46. 46. class PeopleController < ApplicationController before_action :set_message, only: %i(show edit update destroy) include Hypermicrodata::Rails::HtmlBasedJsonRenderer ... end .person{itemscope: true, itemtype: 'http://schema.org/Person', itemid: person_url(@person), data: {main_item: true}} .media .media-image.pull-left = image_tag @person.picture_path, alt: '', itemprop: 'image' .media-body %h1.media-heading %span{itemprop: 'name'}= @person.name = link_to 'collection', people_path, rel: 'collection' Example in HAL (application/hal+json) { "image": "/assets/bob.png", "name": "Bob Smith", "isPartOf": "/people", "_links": { "self": { "href": "http://www.example.com/people/1" }, "type": { "href": "http://schema.org/Person" }, "collection": { "href": "/people" }, "profile": { "href": "/assets/person.alps" } } }
  47. 47. Hypermicrodata gemを使った Railsによる設計手順 1. リソース設計 2. 状態遷移図を描く 3. データの名前を対応するURLに結びつける 4. HTMLテンプレート(Haml, Slimなど)を書いて、Microdata でマークアップする (その後、必要ならschema.org定義にないプロファイルと説明を書く)
  48. 48. Example: Note API
  49. 49. 1. リソース設計 カラム名説明タイプ text noteの内容のテキストtext published_at noteの公開時間datetime (id, created_at, updated_at) (自動生成) $ rails g model Note text:text published_at:datetime model: Note controller: NotesController routing: resources :notes
  50. 50. 2. 状態遷移図を描く Railsの Collection & Member リソースパターンから始める (API ver.) item collection Collection Member create*† update*, delete* * 安全でない † 冪等でない
  51. 51. Collection of Note Note (text, published_at, created_at, updated_at, id) item collection create*† update*, delete*, publish* next, prev Home notes home * 安全でない † 冪等でない
  52. 52. 3. データの名前を対応するURLに 結びつける Collection of Note http://schema.org/ItemList Note http://schema.org/Article text http://schema.org/articleBody published_at http://schema.org/datePublished created_at http://schema.org/dateCreated updated_at http://schema.org/dateModified id (各noteは個別のURLを持つので不要) Home http://schema.org/SiteNavigationElement
  53. 53. item IANA ‘item’ & http://schema.org/hasPart collection IANA ‘collection’ & http://schema.org/isPartOf notes - create Activity Streams ‘create' update Activity Streams ‘update’ delete Activity Streams ‘delete’ publish Activity Streams ‘post’ IANA registered Link Relation: http://www.iana.org/assignments/link-relations/ Activity Streams Verbs: http://activitystrea.ms/registry/verbs/
  54. 54. 4. HTMLテンプレートとMicrodataを書く Collection of Note %div{itemscope: true, itemtype: 'http://schema.org/ItemList', itemid: notes_url, data: {main_item: true}} - @notes.each do |note| = link_to note.text.truncate(20), note, rel: 'item', itemprop: 'hasPart' /app/views/notes/index.html.haml GET /notes HTTP/1.1 Host: www.example.com Accept: application/vnd.amundsen-uber+json = form_for Note.new do |f| = f.text_field :text = f.submit rel: 'create' { "uber": { "version": "1.0", "data": [{ "url": "http://www.example.com/notes", "name": "ItemList", "data": [ { "name": "hasPart", "rel": "item", "url": "/notes/1" }, { "name": "hasPart", "rel": "item", "url": "/notes/2" }, { "rel": "create", "url": "/notes", "action": "append", "model": "note%5Btext%5D={text}" }, { "rel": "profile", "url": "/assets/note.alps"} ] }] } } Link Form
  55. 55. %div{itemscope: true, itemtype: 'http://schema.org/Article', itemid: note_url(@note), data: {main_item: true}} /app/views/notes/show.html.haml %span{itemprop: 'articleBody'}= @note.text %span{itemprop: 'datePublished'}= @note.published_at %span{itemprop: 'dateCreated'}= @note.created_at %span{itemprop: 'dateModified'}= @note.updated_at = form_for @note, method: :put do |f| = f.text_field :text = f.submit rel: 'update' = button_to 'Destroy', @note, method: :delete, rel: 'delete' = button_to 'Publish', publish_note_path(@note), rel: 'publish' unless @note.published? = link_to 'Next note', note_path(@note.next), rel: 'next' if @note.next = link_to 'Prev note', note_path(@note.prev), rel: 'prev' if @note.prev = link_to 'Collection of Note', notes_path, rel: 'collection', itemprop: 'isPartOf' GET /notes/1 HTTP/1.1 Host: www.example.com Accept: application/vnd.amundsen-uber+json Note { "uber": { "version": "1.0", "data": [{ "url": "http://www.example.com/notes/1", "name": "Article", "data": [ { "name": "articleBody", "value": "First note's text" }, { "name": "datePublished", "value": null }, { "name": "dateCreated", "value": "2014-09-11T12:00:31+09:00" }, { "name": "dateModified", "value": "2014-09-11T12:00:31+09:00" }, { "name": "isPartOf", "rel": "collection", "url": "/notes" }, { "rel": "update", "url": "/notes/1", "action": "replace", "model": "note%5Btext%5D={text}" }, { "rel": "delete", "url": "/notes/1", "action": "remove" }, { "rel": "publish", "url": "/notes/1/publish", "action": "append" }, { "rel": "next", "url": "/notes/2" }, { "rel": "profile", "url": "/assets/note.alps" } ] }] } }
  56. 56. %div{itemscope: true, itemtype: 'http://schema.org/Article', itemid: note_url(@note), data: {main_item: true}} %span{itemprop: 'articleBody'}= @note.text %span{itemprop: 'datePublished'}= @note.published_at %span{itemprop: 'dateCreated'}= @note.created_at %span{itemprop: 'dateModified'}= @note.updated_at = form_for @note, method: :put do |f| = f.text_field :text = f.submit rel: 'update' = button_to 'Destroy', @note, method: :delete, rel: 'delete' = button_to 'Publish', publish_note_path(@note), rel: 'publish' unless @note.published? = link_to 'Next note', note_path(@note.next), rel: 'next' if @note.next = link_to 'Prev note', note_path(@note.prev), rel: 'prev' if @note.prev = link_to 'Collection of Note', notes_path, rel: 'collection', itemprop: 'isPartOf' Note = button_to 'Publish', publish_note_path(@note), = link_to 'Next note', note_path(@note.next), = link_to 'Prev note', note_path(@note.prev), { "uber": { rel: 'publish' unless @note.published? rel: 'next' if @note.next rel: 'prev' if @note.prev "version": "1.0", "data": [{ "url": "http://www.example.com/notes/1", "name": "Article", "data": [ 条件の表現 publishできるが prevには行けない { "name": "articleBody", "value": "First note's text" }, { "name": "datePublished", "value": null }, { "name": "dateCreated", "value": "2014-09-11T12:00:31+09:00" }, { "{ rel": "name": "publish", "dateModified", "url": "value": "/"2014-notes/09-11T12:1/publish", 00:31+09:00" }, "{ action": "name": "isPartOf", "append" "rel": }, "collection", "url": "/notes" }, { "rel": "update", "url": "/notes/1", "action": "replace", { "rel": "model": "next", "note%5Btext%"url": 5D={text}" "/notes/}, 2" }, { "rel": "delete", "url": "/notes/1", "action": "remove" }, { "rel": "publish", "url": "/notes/1/publish", "action": "append" }, { "rel": "next", "url": "/notes/2" }, { "rel": "profile", "url": "/assets/note.alps" } ] }] } }
  57. 57. この設計手順の 3つのメリット
  58. 58. メリット 1: DRY • リンクとフォームをHTMLテンプレートに一度 書けば、JSON生成時にも再利用できる • Microdataマークアップもそのまま再利用できる • Bonus: HTMLのMicrodataはSEO効果を上げる (JSONにも可能性あり)
  59. 59. メリット 2: リンクとフォームを 意識できる • JSON Web APIを作るときには、状態遷移の重要 性を見落としやすい • APIをHTMLのWebアプリのように表現すること で、状態遷移に着目して適切にリンクとフォー ムを実装できる
  60. 60. メリット 3: 制約 • 「HTMLドキュメントをMicrodataでマークアップし て、それを一定のルールでフォーマットされた JSONに変換する」という制約 • この制約はよりよい設計のガイド • “Constraints are liberating” 「制約は自由をもたらす」
  61. 61. もしJSONだけを書くときは 注意すること • リンク・フォームを意識するために: • 状態遷移図を描きましょう • APIを疎結合に保つために: • model.to_json の代わりに Jbuilder/RABL のようなビューテンプレートや リプレゼンターを使いましょう • リンクとフォームを持ったJSONベースのフォーマットを使いましょう • さらに schema.org のような標準名を使うとベター
  62. 62. “WebアプリとWeb APIを分けて考えない” – 「Webを支える技術」@yohei
  63. 63. 結論: Web APIはHTML Webアプリと 同じように設計しよう • Web APIは特別なものではなく、ただ表現 フォーマットが違うだけ • 状態遷移図を描いて状態遷移を意識すること で、リンクやフォームを忘れずにすむ
  64. 64. 最後に • 残念ながら、JSONフォーマット、クライアント実 装やライブラリにはデファクトスタンダードがない • RESTの制約・原則を意識するともっとうまくでき る • ハイパーメディアはRESTの最も重要な要素の1つで 変化に適応できるWeb APIへの重要なステップ
  65. 65. よりよい、変化に適応できるWeb APIを作りましょう Thank you for your attention. References • L. Richardson & M. Amundsen “RESTful Web APIs” (O’Reilly) • 山本陽平 “Webを支える技術” (技術評論社) • Designing for Reuse: Creating APIs for the Future http://www.oscon.com/oscon2014/public/schedule/detail/34922 • API Design Workshop 配布資料 http://events.layer7tech.com/tokyo-wrk • https://speakerdeck.com/zdne/robust-mobile-clients-v2 • http://www.slideshare.net/yohei/webapi-36871915 • http://smizell.com/weblog/2014/solving-fizzbuzz-with-hypermedia • 山口 徹 “Web API デザインの鉄則” WEB+DB PRESS Vol.82

×