{"id":86,"date":"2015-05-09T22:20:51","date_gmt":"2015-05-09T22:20:51","guid":{"rendered":"https:\/\/www.aya.io\/ayablog\/?p=86"},"modified":"2022-02-07T02:01:09","modified_gmt":"2022-02-07T02:01:09","slug":"un-mini-serveur-api-en-ruby","status":"publish","type":"post","link":"https:\/\/www.aya.io\/blog\/un-mini-serveur-api-en-ruby\/","title":{"rendered":"Un mini serveur d&rsquo;API en Ruby"},"content":{"rendered":"<p>On a souvent besoin de tester des requ\u00eates Internet quand on d\u00e9veloppe une application, par exemple pour v\u00e9rifier dans une app iOS que la connexion se fait bien en arri\u00e8re-plan, que le JSON re\u00e7u est bien d\u00e9cod\u00e9, etc.<\/p>\n<p>On serait tent\u00e9 de s'adresser \u00e0 son serveur de prod, ou m\u00eame d'utiliser Dropbox... mais il y a plus cool&nbsp;: se faire son propre mini serveur de tests.<\/p>\n<p>Et avec Ruby, c'est tr\u00e8s simple, et \u00e7a prend \u00e0 peine vingt lignes de code.<\/p>\n<p>Let's go!<\/p>\n<p><!-- more --><\/p>\n<h2>Pr\u00e9sentation<\/h2>\n<p>Ces temps-ci j'ai souvent besoin de tester la validit\u00e9 d'un payload JSON, ou la solidit\u00e9 d'un upload d'image vers le Web, ou la qualit\u00e9 d'une connexion et de son timeout, etc.<\/p>\n<p>Au lieu d'utiliser plusieurs services pour \u00e7a, et m\u00eame de laisser tra\u00eener des fichiers dans des dossiers prot\u00e9g\u00e9s de mon serveur de prod, comme on est souvent tent\u00e9 de le faire, ou de s'adresser \u00e0 des outils qui ne sont pas faits pour \u00e7a comme Dropbox, il est plus efficace - et m\u00eame plus simple - de se faire son propre serveur en local.<\/p>\n<p>Avec Ruby et Sinatra \u00e7a prend quelques lignes \u00e0 peine et \u00e7a rend un service incroyable.<\/p>\n<h2>Le serveur<\/h2>\n<p>On va avoir besoin de Sinatra, donc on l'installe avec ses d\u00e9pendances&nbsp;:<\/p>\n<pre><code class=\"language-bash\">gem install sinatra thin webrick<\/code><\/pre>\n<p>On cr\u00e9e ensuite un simple fichier Ruby <code>app.rb<\/code> avec cet en-t\u00eate&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">#!\/usr\/bin\/env ruby\n# encoding: utf-8\n\nrequire &quot;sinatra&quot;\nrequire &quot;json&quot;<\/code><\/pre>\n<p>On peut obtenir une version fonctionnelle pour tester l'install avec juste trois lignes \u00e0 ajouter&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">get &#039;\/&#039; do\n    &quot;Hello World&quot;\nend<\/code><\/pre>\n<p>Ca veut dire que Sinatra va surveiller l'URL racine, dans notre cas localhost sur le port 4567, et r\u00e9pondre par le contenu de <code>do ... end<\/code>.<\/p>\n<p>On le lance puis on teste&nbsp;:<\/p>\n<pre><code class=\"language-bash\">ruby app.rb\ncurl localhost:4567<\/code><\/pre>\n<p>R\u00e9sultat : &quot;Hello World&quot; s'affiche dans le terminal.<\/p>\n<p><em>Stoppez le serveur en faisant CTRL+C.<\/em><\/p>\n<p>On vient de voir que cr\u00e9er un serveur minimal \u00e9tait simplissime. On va maintenant cr\u00e9er des routes et fonctions plus utiles.<\/p>\n<h3>JSON<\/h3>\n<p>Par exemple, on veut une URL &quot;localhost:4567\/api&quot; qui retourne du JSON.<\/p>\n<p>On cr\u00e9e donc une route pour l'URL&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">get &#039;\/api&#039; do\nend<\/code><\/pre>\n<p>et dans cette methode on va cr\u00e9er le contenu qui sera retourn\u00e9.<\/p>\n<p>Comme on veut du JSON, on va faire un hash (un dictionnaire) et le convertir en JSON.<\/p>\n<pre><code class=\"language-ruby\">get &#039;\/api&#039; do\n    # On pr\u00e9cise le format pour Sinatra\n    content_type :json\n    # On cr\u00e9er un hash avec nos contenus\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 200,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer API.&#039;\n        }\n    }\n    # On transforme le hash en joli JSON\n    # Comme on est en Ruby, la derni\u00e8re d\u00e9claration est retourn\u00e9e, donc par convention on n&#039;indique pas explicitement `return`\n    JSON.pretty_generate(jj)\nend<\/code><\/pre>\n<p>R\u00e9sultat&nbsp;:<\/p>\n<pre><code class=\"language-bash\">ruby app.rb\ncurl localhost:4567\/api<\/code><\/pre>\n<pre><code class=\"language-json\">{\n  &quot;meta&quot;: {\n    &quot;code&quot;: 200,\n    &quot;message&quot;: &quot;Welcome to MiniServer API.&quot;\n  }\n}<\/code><\/pre>\n<h3>Fichier JSON<\/h3>\n<p>Et si on veut que notre serveur retourne du JSON plus complexe ? On peut lui demander, au lieu de cr\u00e9er un hash et de le convertir, de lire un fichier JSON existant.<\/p>\n<p>Dans notre exemple tr\u00e8s simple sans configuration, le fichier devra \u00eatre pr\u00e9sent au m\u00eame niveau que <code>app.rb<\/code>.<\/p>\n<pre><code class=\"language-ruby\">get &#039;\/file\/*.*&#039; do\n    # `params` est le nom de la variable globale de Sinatra qui contient les param\u00e8tres pass\u00e9s dans l&#039;URL, et `splat` repr\u00e9sente un format contenant `*`\n    name = params[&quot;splat&quot;].join(&quot;.&quot;)\n    File.read(name)\nend<\/code><\/pre>\n<p>Disons que j'aie un fichier <code>test.json<\/code>, je fais&nbsp;:<\/p>\n<pre><code class=\"language-bash\">curl localhost:4567\/file\/test.json<\/code><\/pre>\n<p>et j'obtiens le JSON.<\/p>\n<p>Sinatra autorise \u00e0 forcer une URL sous forme de nom de fichier avec les deux ast\u00e9risques et le point, qui retourne un array qu'il faut donc rejoindre avec un point. <\/p>\n<p>On lit ensuite le contenu du fichier, qui est automatiquement retourn\u00e9.<\/p>\n<p>Donc par exemple au lieu de taper sans cesse une vraie API en ligne qui distribue du JSON, vous pouvez enregistrer le r\u00e9sultat d'une requ\u00eate puis la rejouer autant que vous voulez dans votre propre mini serveur&nbsp;:<\/p>\n<pre><code class=\"language-bash\">curl www.big-api.com\/api\/bigdatachunk &gt; test.json\nruby app.rb\ncurl localhost:4567\/file\/test.json<\/code><\/pre>\n<h3>Fichier \u00e0 downloader<\/h3>\n<p>Ici j'utilise une image en exemple. Sinatra permet de faire \u00e7a tr\u00e8s simplement gr\u00e2ce \u00e0 un de ses helpers&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">get &#039;\/picture&#039; do\n    send_file &#039;\/Users\/you\/Images\/bird.jpg&#039;\nend<\/code><\/pre>\n<p>Collez <code>http:\/\/localhost:4567\/picture<\/code> dans un browser, et votre image s'affiche.<\/p>\n<h3>D\u00e9tails d'une requ\u00eate<\/h3>\n<p>On peut vouloir inspecter l'URL form\u00e9e \u00e0 partir de param\u00e8tres, on va donc partir de notre pr\u00e9c\u00e9dente m\u00e9thode associ\u00e9e \u00e0 l'URL <code>\/api<\/code> et lui ajouter ces fonctions&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">get &#039;\/api\/*&#039; do\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 200,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer. Request accepted.&#039;\n        }\n    }\n    if params[&#039;splat&#039;].first != &quot;&quot;\n        jj[&#039;data&#039;] = {}\n        jj[&#039;data&#039;][&#039;components&#039;] = params[&#039;splat&#039;].first.split(&#039;\/&#039;)\n        if !params[&quot;q&quot;].nil?\n            jj[&#039;data&#039;][&#039;query&#039;] = params[&quot;q&quot;]\n        end\n    end\n    content_type :json\n    JSON.pretty_generate(jj)\nend<\/code><\/pre>\n<p>Ce n'est qu'un exemple pour d\u00e9montrer le principe.<\/p>\n<p>Requ\u00eate&nbsp;:<\/p>\n<pre><code class=\"language-bash\">curl localhost:4567\/api\/test\/server\/whatever\/\\?q=yo<\/code><\/pre>\n<p>R\u00e9sultat&nbsp;:<\/p>\n<pre><code class=\"language-json\">{\n  &quot;meta&quot;: {\n    &quot;code&quot;: 200,\n    &quot;message&quot;: &quot;Welcome to MiniServer. Request accepted.&quot;\n  },\n  &quot;data&quot;: {\n    &quot;components&quot;: [\n      &quot;test&quot;,\n      &quot;server&quot;,\n      &quot;whatever&quot;\n    ],\n    &quot;query&quot;: &quot;yo&quot;\n  }\n}<\/code><\/pre>\n<h3>404<\/h3>\n<p>Il faut bien se pr\u00e9parer aussi un petit 404, encore une fois Sinatra nous aide avec un helper pour, cette-fois, la route&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">not_found do\n    {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 404,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer. Request not found.&#039;\n        }\n    }.to_json\nend<\/code><\/pre>\n<p><em>Ici j'ai volontairement choisi de retourner du JSON brut, non pretty-print, pour montrer que c'est encore plus simple \u00e0 faire.<\/em><\/p>\n<h3>Upload<\/h3>\n<p>Bien s\u00fbr, on peut aussi tester autre chose que <code>GET<\/code>. Par exemple, pour l'upload d'un fichier&nbsp;:<\/p>\n<pre><code class=\"language-ruby\">post &#039;\/upload&#039; do\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 200,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer API. File received.&#039;\n        }, \n        &#039;data&#039; =&gt; {\n            &#039;env&#039; =&gt; request.env\n        }\n    }\n    content_type :json\n    JSON.pretty_generate(jj)\nend<\/code><\/pre>\n<p>On fait par exemple avec notre pr\u00e9c\u00e9dent fichier de test (mais \u00e7a pourrait \u00eatre une image ou tout autre fichier)&nbsp;:<\/p>\n<pre><code class=\"language-bash\">curl -X POST -d @&quot;test.json&quot; localhost:4567\/upload<\/code><\/pre>\n<p>R\u00e9sultat&nbsp;:<\/p>\n<pre><code class=\"language-json\">{\n  &quot;meta&quot;: {\n    &quot;code&quot;: 200,\n    &quot;message&quot;: &quot;Welcome to MiniServer API. File received.&quot;\n  },\n  &quot;data&quot;: {\n    &quot;env&quot;: {\n      &quot;SERVER_SOFTWARE&quot;: &quot;thin 1.6.3 codename Protein Powder&quot;,\n      &quot;SERVER_NAME&quot;: &quot;localhost&quot;,\n      &quot;rack.input&quot;: &quot;#&lt;StringIO:0x007ff1e298b2b0&gt;&quot;,\n      &quot;rack.version&quot;: [\n        1,\n        0\n      ],\n      &quot;rack.errors&quot;: &quot;#&lt;IO:0x007ff1e20ca4f0&gt;&quot;,\n      &quot;rack.multithread&quot;: true,\n      &quot;rack.multiprocess&quot;: false,\n      &quot;rack.run_once&quot;: false,\n      &quot;REQUEST_METHOD&quot;: &quot;POST&quot;,\n      &quot;REQUEST_PATH&quot;: &quot;\/upload&quot;,\n      &quot;PATH_INFO&quot;: &quot;\/upload&quot;,\n      &quot;REQUEST_URI&quot;: &quot;\/upload&quot;,\n      &quot;HTTP_VERSION&quot;: &quot;HTTP\/1.1&quot;,\n      &quot;HTTP_USER_AGENT&quot;: &quot;curl\/7.35.0&quot;,\n      &quot;HTTP_HOST&quot;: &quot;localhost:4567&quot;,\n      &quot;HTTP_ACCEPT&quot;: &quot;*\/*&quot;,\n      &quot;CONTENT_LENGTH&quot;: &quot;49&quot;,\n      &quot;CONTENT_TYPE&quot;: &quot;application\/x-www-form-urlencoded&quot;,\n      &quot;GATEWAY_INTERFACE&quot;: &quot;CGI\/1.2&quot;,\n      &quot;SERVER_PORT&quot;: &quot;4567&quot;,\n      &quot;QUERY_STRING&quot;: &quot;&quot;,\n      &quot;SERVER_PROTOCOL&quot;: &quot;HTTP\/1.1&quot;,\n      &quot;rack.url_scheme&quot;: &quot;http&quot;,\n      &quot;SCRIPT_NAME&quot;: &quot;&quot;,\n      &quot;REMOTE_ADDR&quot;: &quot;127.0.0.1&quot;,\n      &quot;async.callback&quot;: &quot;#&lt;Method: Thin::Connection#post_process&gt;&quot;,\n      &quot;async.close&quot;: &quot;#&lt;EventMachine::DefaultDeferrable:0x007ff1e21e3558&gt;&quot;,\n      &quot;rack.request.form_input&quot;: &quot;#&lt;StringIO:0x007ff1e298b2b0&gt;&quot;,\n      &quot;rack.request.form_hash&quot;: {\n        &quot;{\\&quot;meta\\&quot;:{\\&quot;code\\&quot;:200,\\&quot;message\\&quot;:\\&quot;Test from file.\\&quot;}}&quot;: null\n      },\n      &quot;rack.request.form_vars&quot;: &quot;{\\&quot;meta\\&quot;:{\\&quot;code\\&quot;:200,\\&quot;message\\&quot;:\\&quot;Test from file.\\&quot;}}&quot;,\n      &quot;sinatra.commonlogger&quot;: true,\n      &quot;rack.logger&quot;: &quot;#&lt;Logger:0x007ff1e2872680&gt;&quot;,\n      &quot;rack.request.query_string&quot;: &quot;&quot;,\n      &quot;rack.request.query_hash&quot;: {\n      },\n      &quot;sinatra.route&quot;: &quot;POST \/upload&quot;\n    }\n  }\n}<\/code><\/pre>\n<p>Yeah, c'est plut\u00f4t complet ce que nous donne Sinatra comme infos, pas mal du tout&nbsp;!<\/p>\n<h2>Exemple complet<\/h2>\n<pre><code class=\"language-ruby\">#!\/usr\/bin\/env ruby\n# encoding: utf-8\n\nrequire &quot;sinatra&quot;\nrequire &quot;json&quot;\n\n# C&#039;est pratique aussi d&#039;avoir un log\nconfigure do\n  file = File.new(&quot;#{Dir.home}\/temp\/server.log&quot;, &#039;a+&#039;)\n  file.sync = true\n  use Rack::CommonLogger, file\nend\n\n# On peut choisir une liste de serveurs compatibles, choisir le port, et choisir le dossier racine\nset :server, %w[thin webrick]\nset :port, 4567\nset :root, File.dirname(__FILE__)\n\n# Routes\n\nget &#039;\/&#039; do\n    &quot;yo&quot;\nend\n\nget &#039;\/api&#039; do\n    content_type :json\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 200,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer API.&#039;\n        }\n    }\n    JSON.pretty_generate(jj)\nend\n\nget &#039;\/file\/*.*&#039; do\n    name = params[&quot;splat&quot;].join(&quot;.&quot;)\n    File.read(name)\nend\n\nget &#039;\/api\/*&#039; do\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 200,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer. Request accepted.&#039;\n        }\n    }\n    if params[&#039;splat&#039;].first != &quot;&quot;\n        jj[&#039;data&#039;] = {}\n        jj[&#039;data&#039;][&#039;components&#039;] = params[&#039;splat&#039;].first.split(&#039;\/&#039;)\n        if !params[&quot;q&quot;].nil?\n            jj[&#039;data&#039;][&#039;query&#039;] = params[&quot;q&quot;]\n        end\n    end\n    content_type :json\n    JSON.pretty_generate(jj)\nend\n\nget &#039;\/picture&#039; do\n    send_file &#039;\/Users\/me\/images\/bird.jpg&#039;\nend\n\npost &#039;\/upload&#039; do\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 200,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer API. File received.&#039;\n        }, \n        &#039;data&#039; =&gt; {\n            &#039;env&#039; =&gt; request.env\n        }\n    }\n    content_type :json\n    JSON.pretty_generate(jj)\nend\n\nnot_found do\n    jj = {\n        &#039;meta&#039; =&gt; {\n            &#039;code&#039; =&gt; 404,\n            &#039;message&#039; =&gt; &#039;Welcome to MiniServer. Request not found.&#039;\n        }\n    }\n    content_type :json\n    JSON.pretty_generate(jj)\nend<\/code><\/pre>\n","protected":false},"excerpt":{"rendered":"<p>On a souvent besoin de tester des requ\u00eates Internet quand on d\u00e9veloppe une application, par exemple pour v\u00e9rifier dans une app iOS que la connexion se fait bien en arri\u00e8re-plan, que le JSON re\u00e7u est bien d\u00e9cod\u00e9, etc. On serait tent\u00e9 de s&rsquo;adresser \u00e0 son serveur de prod, ou m\u00eame d&rsquo;utiliser Dropbox&#8230; mais il y&hellip; <a class=\"more-link\" href=\"https:\/\/www.aya.io\/blog\/un-mini-serveur-api-en-ruby\/\">Poursuivre la lecture <span class=\"screen-reader-text\">Un mini serveur d&rsquo;API en Ruby<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":87,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5,24,10],"tags":[],"class_list":["post-86","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dev","category-ruby","category-tuto","entry"],"_links":{"self":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts\/86","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/comments?post=86"}],"version-history":[{"count":3,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts\/86\/revisions"}],"predecessor-version":[{"id":148,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/posts\/86\/revisions\/148"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/media\/87"}],"wp:attachment":[{"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/media?parent=86"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/categories?post=86"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.aya.io\/blog\/wp-json\/wp\/v2\/tags?post=86"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}