Today is frank, the great jazz master. Frank Sinatra would have been born 100 years ago. It is a good day to translate this article.

It’s raining in Hangzhou at the moment, cold and damp. With jazz swaying in my ears, I shut the cat in the balcony, turned on the computer and started talking nonsense.

(You can skip the text and go to the full code at the end.)

The original link: robots.thoughtbot.com/lets-build-…

Build a Sinatra

Sinatra is a Domain-specific language for rapidly developing Web applications based on Ruby. After using it for a few small projects, I decided to check it out.

What is Sinatra made of?

At the heart of Sinatra is Rack. I wrote an article about Rack, which is worth reading if you’re a little confused about how Rack works. Sinatra provides a powerful DSL on top of Rack. Here’s an example:

get "/hello" do [200, {}, "Hello from Sinatra!"]  end post "/hello" do [200, {}, "Hello from a post-Sinatra world!"]  endCopy the code

When this code executes, we send a GET to /hello and see Hello from Sinatra! ; Sending a POST request to /hello will see Hello from a post-Sinatra world! . But at this point, any other requests will return 404.

structure

Sinatra’s source code, we worked together to refine a Sinatra like structure.

We will create an inheritable and extensible class based on Sinatra. It keeps the request routing table (GET/Hello handleHello) and calls handleHello when a GET/Hello request is received. In fact, it handles all requests pretty well. When a request is received, it traverses the routing table and returns a 404 if there is no suitable request.

OK, let’s do it.

Just call it Nancy and don’t ask me why.

The first thing to do is create a class that has a GET method that captures the path of the request and finds the corresponding function.


# nancy.rb

require "rack"

module Nancy

 class Base

 def initialize

 @routes = {}

 end

 attr_reader :routes

 def get(path, &handler)

 route("GET", path, &handler)

 end

 private

 def route(verb, path, &handler)

 @routes[verb] ||= {}

 @routes[verb][path] = handler

 end

 end

end
Copy the code

The route function takes a verb (HTTP request method name), a path, and a callback method and stores it in a Hash structure. This design keeps POST/Hello and GET/Hello from getting messed up.

Then add some test code below:

nancy = Nancy::Base.new

nancy.get "/hello" do
  [200, {}, ["Nancy says hello"]]
end

puts nancy.routesCopy the code

Nancy uses nancy.get instead of Sinatra’s get.

If we execute the program at this point, we see:


{ "GET" =\> { "/hello" =\> \#\ } }
Copy the code

This returns the result that our routing table works fine.

Nancy who introduced Rack

Now let’s add the Rack call method to Nancy to make it the smallest Rack program. Here’s the code from my other Rack article:

# nancy.rb
def call(env)
  @request = Rack::Request.new(env)
  verb = @request.request_method
  requested_path = @request.path_info

  handler = @routes[verb][requested_path]

  handler.call
endCopy the code

First, we GET the request method (HTTP/GET, etc.) and path (/the/path) from the env environment variable parameters of Rack’s request, then route the corresponding callback method from the table and invoke it based on that information. The callback method returns a fixed structure containing the status code, HTTP headers, and returned content that Rack requires for its Call to be returned to the user via Rack.

Let’s add a callback like this to Nancy::Base:

nancy = Nancy::Base.new

nancy.get "/hello" do
  [200, {}, ["Nancy says hello"]]
end

# This line is new!
Rack::Handler::WEBrick.run nancy, Port: 9292Copy the code

The Rack App is now up and running. We use WEBrick as the server, which is built into Ruby.

nancy = Nancy::Base.new

nancy.get "/hello" do
  [200, {}, ["Nancy says hello"]]
end

# This line is new!
Rack::Handler::WEBrick.run nancy, Port: 9292Copy the code

Execute ruby Nancy. Rb, visit http://localhost:9292/hello, all work very well. Note that Nancy does not automatically reload, and any changes you make must be restarted for them to take effect. Ctrl+C stops it in terminal.

Error handling

Access to the path in the routing table it can work normally, but the path of the visit are not present in the routing table such as http://localhost:9292/bad, you can only see the Rack to return to the default Error message, a hostile Internal Server Error page. Let’s look at how to customize an error message.

We need to modify the call method:

def call(env)
   @request = Rack::Request.new(env)
   verb = @request.request_method
   requested_path = @request.path_info

-  handler = @routes[verb][requested_path]
-
-  handler.call
+  handler = @routes.fetch(verb, {}).fetch(requested_path, nil)

+  if handler
+    handler.call
+  else
+    [404, {}, ["Oops! No route for #{verb} #{requested_path}"]]
+  end
 endCopy the code

Now, requesting a path not defined in the routing table returns a 404 status code with an error message.

Get more information from the HTTP request

Nancy. Get now only gets the path, but for it to work it needs to get more information, such as the parameters of the request, etc. The environment variables for the Request are encapsulated in the params of Rack::Request.

Let’s add a new method to Nancy::Base params:

module Nancy class Base # # ... other methods.... # def params @request.params end end endCopy the code

You can access the params method for callback processing that requires this request information.

Visit the params

Take a look at the params instance method you just added.

Modify the call callback part of the code:

if handler - handler.call + instance_eval(&handler) else [404, {}, ["Oops!  Couldn't find #{verb} #{requested_path}"]] endCopy the code

There are a few tricks here that are confusing. Why instance_eval instead of Call?

  • Handler is a lambda without context

  • If we call the lambda using call, it won’t have access to Nancy::Base’s instance methods.

  • Using instance_eval instead of call, Nancy::Base instance information is injected, and it can access Nancy::Base instance variables and methods (context).

So, now we can access params in handler block. Give it a try:

nancy.get "/" do
  [200, {}, ["Your params are #{params.inspect}"]]
endCopy the code

Go to http://localhost:9292/? Foo =bar&hello=goodbye, all information about the request is printed out.

Support for arbitrary HTTP methods

So far, Nancy. Get handles GET requests normally. But that’s not enough. We need to support more HTTP methods. The code that supports them is similar to get:

# nancy.rb
def post(path, &handler)
  route("POST", path, &handler)
end

def put(path, &handler)
  route("PUT", path, &handler)
end

def patch(path, &handler)
  route("PATCH", path, &handler)
end

def delete(path, &handler)
  route("DELETE", path, &handler)
endCopy the code

Usually in POST and PUT requests, we want to access the request body. Now that we have access to Nancy::Base instance methods and variables in the callback, it’s good to make @Request visible:

attr_reader :requestCopy the code

Accessing requrest instance variables in a callback:

nancy.post "/" do
  [200, {}, request.body]
endCopy the code

Access tests:

$ curl --data "body is hello" localhost:9292
body is helloCopy the code

Modernization process

Let’s make the following optimizations:

  1. Use the params instance method instead of calling Request. params directly

def params
  request.params
endCopy the code
   if handler
-    instance_eval(&handler)
+    result = instance_eval(&handler)
+    if result.class == String
+      [200, {}, [result]]
+    else
+      result
+    end
   else
     [404, {}, ["Oops! Couldn't find #{verb} #{requested_path}"]]
   endCopy the code

This makes handling callbacks much easier:

nancy.get "/hello" do
  "Nancy says hello!"
endCopy the code

Continue to optimize Nancy::Application using the proxy pattern

With Sinatra, we use get and POST for elegant, powerful and intuitive request processing. How does it do that? Let’s think about Nancy’s structure. When it executes, we call Nancy:: base. new to get a new instance, then add the function that handles path, and execute. So, if you have a singleton, you can achieve the effect of Sinatra by adding the path handling method from the file to the singleton and executing it. (Translator’s note: The translation of this paragraph has nothing to do with the original; it is pure fiction. If confused, please refer to the original text)

It’s time to consider optimizing Nancy. Get to get. Add Nancy::Base singleton:

module Nancy
  class Base
    # methods...
  end

  Application = Base.new
endCopy the code

Add callback:

nancy_application = Nancy::Application

nancy_application.get "/hello" do
  "Nancy::Application says hello"
end

# Use `nancy_application,` not `nancy`
Rack::Handler::WEBrick.run nancy_application, Port: 9292
Copy the code

Add agent (source code from Sinatra) :

module Nancy
  module Delegator
    def self.delegate(*methods, to:)
      Array(methods).each do |method_name|
        define_method(method_name) do |*args, &block|
          to.send(method_name, *args, &block)
        end

        private method_name
      end
    end

    delegate :get, :patch, :put, :post, :delete, :head, to: Application
  end
endCopy the code

Introduce Nancy::Delegate to the Nancy module:

include Nancy::DelegatorCopy the code

Nancy::Delegator provides a set of proxy methods such as GET, Patch, POST, etc. When these methods are invoked in the Nancy::Application, it follows the steed to find the methods of the proxy. We achieved the same effect as Sinatra.

Now you can delete the code that creates Nancy::Base:: New and Nancy_application. Nancy’s usage has gotten infinitely closer to Sinatra:

t "/bare-get" do
  "Whoa, it works!"
end

post "/" do
  request.body.read
end

Rack::Handler::WEBrick.run Nancy::Application, Port: 9292Copy the code

Calls can also be made using rackup:

# config.ru
require "./nancy"

run Nancy::ApplicationCopy the code

The complete code for Nancy:

# nancy.rb require "rack" module Nancy class Base def initialize @routes = {} end attr_reader :routes def get(path, &handler) route("GET", path, &handler) end def post(path, &handler) route("POST", path, &handler) end def put(path, &handler) route("PUT", path, &handler) end def patch(path, &handler) route("PATCH", path, &handler) end def delete(path, &handler) route("DELETE", path, &handler) end def head(path, &handler) route("HEAD", path, &handler) end def call(env) @request = Rack::Request.new(env) verb = @request.request_method requested_path = @request.path_info handler = @routes.fetch(verb, {}).fetch(requested_path, nil) if handler result = instance_eval(&handler) if result.class == String [200, {}, [result]] else result end else [404, {}, ["Oops!  No route for #{verb} #{requested_path}"]] end end attr_reader :request private def route(verb, path, &handler) @routes[verb] ||= {} @routes[verb][path] = handler end def params @request.params end end Application = Base.new module Delegator def self.delegate(*methods, to:) Array(methods).each do |method_name| define_method(method_name) do |*args, &block| to.send(method_name, *args, &block) end private method_name end end delegate :get, :patch, :put, :post, :delete, :head, to: Application end end include Nancy::DelegatorCopy the code

Nancy’s usage code:

# app.rb
# run with `ruby app.rb`
require "./nancy"

get "/" do
  "Hey there!"
end

Rack::Handler::WEBrick.run Nancy::Application, Port: 9292Copy the code

Let’s review what happened:

  • N(T)an(I) C (r)y Sinatra (don’t ask why)

  • Implement a Web App from Rack

  • Simplify Nancy. Get to get

  • Support subclassing Nancy::Base for richer customization.

Extracurricular reading

Sinatra’s code is almost entirely in base.rb. The code density is a bit high, and it will be easier to understand after reading this article. From the call! It’s a good place to start. Then there is the Response class, which is a subclass of Rack::Response, where the information returned by the request is encapsulated. And Sinatra is class-based, Nancy is object based, and some of the sample methods in Nancy are implemented as class methods in Sinatra, so that’s another thing to be aware of.