Exploration de l'API Standard de Crystal-lang PART 4 (Class)
Tags: crystal
Published: 2023-07-14 12:36:51

---
title: Exploration de l'API Standard de Crystal-lang PART 4 (Class)
published: true
description: Exploration de l'API Standard de Crystal-lang PART 4 (Class)
tags: crystal
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-07-14 10:29 +0000
---
# Introduction 
Dans cette quatrième partie, nous allons encapsuler la partie serveur HTTP dans une classe dédiée.

# C'est partie.
## Etats des lieux

Notre fichier principal ressemble à ça

```crystal
require "http/server"
require "./singleton"
require "./config"
require "./repository"

# Define a route for the todo list page.
server = HTTP::Server.new() do |context|
  request = context.request

  case request.path
  when "/"
    items = Repository.new(Config::TODO_FILE_PATH).all
    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
          Repository.new(Config::TODO_FILE_PATH).add(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

server.bind_tcp Config::BIND_IP, Config::PORT
if Config.environment != "test"
  server.listen
end
```

Dans un premier temps, notre but va être d'isoler la partie serveur dans un objet.

## Vision objet

Notre serveur est défini par :
- ce qu'il écoute : des requêtes au protocol HTTP sur une connexion TCP_IP (IP, PORT)
- ce qu'il renvoie.

Nous allons traiter la première partie.

## Passons aux tests.

Quand nous initialisons le serveur HTTP, nous avons besoin de définir l'ip et le port d'écoute. Nous avons aussi besoin que cela fonctionne sans cette saisie. Créons le fichier `./spec/server_spec.cr`

```crystal
require "./spec_helper"

describe Server do
  describe "#new" do
    it "set default ip at 0.0.0.0" do
      Server.new.bind_ip.should eq "0.0.0.0"
    end
    it "set default port at 3000" do
      Server.new.port.should eq 3000
    end
    it "set bind_ip with argument value" do
      Server.new(bind_ip: "254.43.35.34").bind_ip.should eq "254.43.35.34"
    end 
    it "set port with argument value" do
      Server.new(port: 4567).port.should eq 4567
    end   
    it "set ip and port with argument value" do
      server = Server.new(bind_ip: "123.123.123.123", port: 9876)
      server.bind_ip.should eq "123.123.123.123"
      server.port.should eq 9876
    end     
  end 
end 
```

Ces tests sont simples mais permettent de montrer la syntaxe de Crystal sur les signatures de méthodes. En passant, dans la signature, le nom des variables de celle-ci, je me permets de laisser le choix de l'implémentation pour plus tard.
En revanche, cela impose d'avoir des valeurs par défaut dans notre implémentation.

# Implémentation de notre objet `Server`

L'ip et le port d'écoute n'ont pas besoin d'être changé après l'initialisation. Je propose donc de ne définir que des `getter`.
La partie serveur n'a pas à être exposé en dehors de la classe à mon humble avis.
L'implémentation donne alors :

```crystal
class Server
  getter bind_ip : String
  getter port : Int32
 
  def initialize(@bind_ip = "0.0.0.0", @port = 3000)
    @server = HTTP::Server.new() do |context|
      request = context.request
 
      case request.path
      ...
      end
    end
  end

  def run
    @server.bind_tcp @bind_ip, @port
    @server.listen
  end
end
```

`getter` est une macro qui va définir la méthode `def bind_ip ...`

Si nous avions mis un `setter`, cela aurait défini la méthode `def bind_ip=`. Le la macro `property` réalise les deux.

Ensuite, dans la signature de la méthode `initialize`, nous pouvons mettre directement les variables de classe en lieu et place de :
```crystal
def initialize(bind_ip)
  @bind_ip = bind_ip
end
```

et pour définir une valeur par défaut, nous pouvons le faire dans la signature comme je l'ai fait ici.

**Note importante** , si une variable de classe est obligatoire (ne peut être nulle), le compilateur vérifiera que cette variable est bien renseignée à la fin de la méthode `initialize`. Si ce n'est pas le cas, une erreur de compilation apparaîtra.

# Conclusion
Cette partie est courte mais je voulais revenir sur l'initialisation d'un objet et l'importance de la signature de la méthode `initialize`

Dans la prochaine partie, nous allons nous attaquer à la partie routage. Pour cela, nous allons avoir besoin d'utiliser les REGEX.