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")
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
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
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
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>
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
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
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>
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
...
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
Top comments (0)