DEV Community

David YOTEAU
David YOTEAU

Posted on

Exploration de l'API Standard de Crystal-lang PART 4 (Class)

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

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
Enter fullscreen mode Exit fullscreen mode

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

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 
Enter fullscreen mode Exit fullscreen mode

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 :

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
Enter fullscreen mode Exit fullscreen mode

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 :

def initialize(bind_ip)
  @bind_ip = bind_ip
end
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)