DEV Community

David YOTEAU
David YOTEAU

Posted on • Edited on

Exploration de l'API Standard de Crystal-lang PART 1 (HTTP::Server, DIR, ECR, File)

Introduction Générale

Cette article est le premier d'une série. Cette série vise à explorer l'API standard de Crystal-lang à travers la réalisation d'une application.

L'application cible est un gestionnaire de la méthode "Getting Things Done" avec une partie web, une api HTTP et une CLI.

Cette article

Dans cette article en particulier, nous allons créer une page d'ajout de tâches et une persistance en fichier. Nous allons utiliser :

  • HTTP::Server pour exposer le service
  • ECR pour concevoir l'écran
  • File pour la persistance
  • Dir pour la gestion de l’arborescence

C'est parti

Créons des tâches dans un fichier.

Pour l'instant, la structure d'une tâche est une chaine de caractères, chaque ligne dans le fichier sera une tâche.

Définissons une constante pour le chemin vers le fichier de persistance.

TODO_FILE_PATH = File.join(Dir.current, "todo.txt")
Enter fullscreen mode Exit fullscreen mode

Dir.current renvoie le chemin courant de l’exécutable.

File.join permet de créer le chemin en y ajoutant le nom du fichier voulu. Cette méthode prend autant d'élément que vous le souhaitez et va créer un chemin système valide en fonction de votre OS.

Maintenant que nous avons le chemin, nous allons écrire la méthode d'ajout de tâche au fichier.

def add_task(item : String)
  file = File.open(TODO_FILE_PATH, "a")
  file.puts item
  file.close
end
Enter fullscreen mode Exit fullscreen mode

Nous pouvons aussi l'écrire sous cette forme

def add_task(item : String)
  File.open(TODO_FILE_PATH, "a") do |file|
    file.puts item
  end
end
Enter fullscreen mode Exit fullscreen mode

Dans le deuxième cas, le File.close est implicitement exécuté à la fin du bloc File.open.

File.open va ouvrir le fichier avec le mode passé en argument. Par défaut, c'est le mode "r" pour "read". Vous avez aussi :

  • w pour write, créer ou remplace le fichier avec le contenu.
  • rw pour read-write, ouvre le fichier et remplace le contenu.
  • a pour append, ajoute le contenu à la fin du fichier.

Nous souhaitons ajouter une tâche, le choix se porte sur le mode "a".

File.puts envoie, à la fin du fichier, la chaîne de caractère passée en argument.

Avec ce que nous venons de voir, ajoutons une méthode pour récupérer la liste de toutes les tâches.

def task_list : Array(String)
  File.read_lines(TODO_FILE_PATH)
end
Enter fullscreen mode Exit fullscreen mode

File.read_lines a l'avantage d'ouvrir le fichier, lire le contenu et le transforme en tableau puis refermer le fichier.

Nous en avons fini avec la persistance pour cette article. Passons à la partie mise en page.

Créons un écran html.

Pour faire un template et embarquer du code crystal, l'API standard fournit ECR.

Dans son mode d'usage le plus simple, il faut :

  • définir des variables
  • exécuter un ECR.render avec le fichier template en argument. (Toutes les variables du même scope que le ECR.render seront transmises)

Dans notre cas, nous avons besoin de la liste des tâches (déjà réalisé ci-dessus) et du template. Nous sommes sur du web, il s'agit d'un template HTML.

  <!DOCTYPE html>
  <html>
    <head>
      <title>Todo List</title>
    </head>
    <body>
      <h1>Todo List</h1>
      <ul>
        <% items.each do |item| %>
          <li><%= item %></li>
        <% end %>
      </ul>
    </body>
  </html>
Enter fullscreen mode Exit fullscreen mode

Je vous passe la partie HTML réduit à sa plus simple expression.

Pour ce qui nous intéresse, nous pouvons voir l'usage des balise <% %> et <%= %> pour l'insertion de code crystal. Ici, un simple parcours de la liste des items pour ensuite les afficher en liste HTML.

Voyons comment créer un serveur HTTP pour afficher cette page

Le serveur http.

Crystal-lang vient avec un serveur http dans son API standard.

le code de la documentation est

require "http/server"

server = HTTP::Server.new do |context|
  context.response.content_type = "text/plain"
  context.response.print "Hello world!"
end

address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen
Enter fullscreen mode Exit fullscreen mode

Nous voyons que nous avons un HTTP::Context qui est à disposition. Cette objet contient la requête et permet de définir la réponse. A minima, nous devons définir le content-type et le body.

Une fois la gestion du contexte établi, nous pouvons configurer l'ip et le port d'écoute puis lancer l’écoute.

dans notre cas, nous voulons afficher notre page quand la requête est sur le chemin "/".

require "http/server"

server = HTTP::Server.new do |context|
  request = context.request

  case request.path
  when "/"
    items = task_list
    context.response.content_type = "text/html"
    context.response.print(ECR.render "src/template.ecr")
  else
    context.response.status_code = 404
    context.response.print("Not found")
  end
end

address = server.bind_tcp 8080
puts "Listening on http://#{address}"
server.listen
Enter fullscreen mode Exit fullscreen mode

En lançant l'application, vous aurez votre liste de tâches. Pour l'exemple, je vous conseille de créer un fichier "todo.txt" à la racine de votre projet et d'y ajouter des lignes.

Passons à la possibilité d'ajouter des tâches via notre IHM.

Ajout du formulaire

reprenons notre template et ajoutons un formulaire simple d'envoi du texte de la tâche.

  <!DOCTYPE html>
  <html>
    <head>
      <title>Todo List</title>
    </head>
    <body>
      <h1>Todo List</h1>
      <ul>
        <% items.each do |item| %>
          <li><%= item %></li>
        <% end %> 
      </ul>
      <form method="post" action="/add">
        <label for="item">New item:</label>
        <input type="text" id="item" name="item">
        <button type="submit">Add</button>
      </form>
    </body>
  </html>
Enter fullscreen mode Exit fullscreen mode

L'ajout d'une tâche passe par une requête POST sur le chemin "/add".

Reprenons notre serveur pour en ajouter la gestion de cet appel.

...
  server = HTTP::Server.new() do |context|
    request = context.request

    case request.path
    when "/" 
      items = task_list
      context.response.content_type = "text/html"
      context.response.print(ECR.render "src/template.ecr")
    when "/add"
      if request.method == "POST"
        body = request.body
        if body.nil?
          context.response.status_code = 400 
          context.response.print("Body is required")
        else
          params = body.gets_to_end
          uri_params = URI::Params.parse(params)
          item = uri_params.fetch("item",nil)
          if item.nil?
            context.response.status_code = 400 
            context.response.print("Body is required")
          else
            add_task(item)
          end 
          context.response.status_code = 302 
          context.response.headers["Location"] = "/" 
        end 
      else
        context.response.status_code = 405 
        context.response.headers["Allow"] = "POST"
        context.response.print("Method not allowed")
      end 
    else
      context.response.status_code = 404 
      context.response.print("Not found")
    end 
  end
...
Enter fullscreen mode Exit fullscreen mode

Oui, je sais, beaucoup de bloc if et case imbriqué. Ce code est là pour gérer les différentes erreurs sans utiliser de lib externes.

La nouveauté de ce bloc est URI::params.parse qui récupère les objet du formulaire à partir de la lecture du body de la requête.

Voici ce qui termine ce premier article.

Conclusion.

Nous avons vu File, Dir, HTTP::Server et ECR. Dans la suite, nous allons voir comment tester tout ça.

Prochaine API : Spec

Note :

repository gitlab

Top comments (0)