Search code examples
rubysinatrametaprogramming

How to use DSL metaprogramming with Sinatra


I'm trying to work on a DSL to manage different locales within the same route, like get "/test". The

This is an exercise to learn how to extend Sinatra, therefore Rack::Locale or a similar tool is not a valid answer.

Based on the body of the request JSON body, assuming I receive JSON as POST or PUT, I want to respond with the specific locale.

I currently have a barebones script, of what I think I need:

class Locale
  attr_reader :locale_id
  attr_reader :described_class

  alias :current_locale :locale_id

  def initialize(locale_id, &block)
    @locale_id = locale_id
    instance_eval &block
  end

end

def locale(locale_id, &block)
  Locale.new(locale_id, &block)
end

I am missing the capability to respond based on the locale in the request.body JSON I receive as input, and the class here has something else I do not yet see that is needed or is missing.

An example of how this would get used would be:

get '/' do 
   locale 'cs-CS' do 
     "Czech"
     #or db query or string
   end 
   locale 'en-UK' do 
     "British english"
     #or db query or string
   end
end

Therefore to try to clarify even more clearly I will try with a TDD approach:

As User when I send a JSON that contains: "locale": "cs-CS" the result is Czech.


Solution

  • Have you read Extending The DSL and the Conditions section of the README?

    Right now, you're not really extending the DSL. I'd redesign it slightly, because it looks like you'd want to match on a case statement but that would mean creating lots of classes or an ugly matching statement. But, Sinatra already has some really nice ways to match on routes and conditions. So, something like this would be more idiomatic:

    post '/', :locale => "Czech" do
      "Czech"
    end
    
    post '/', :locale => "British English" do
      "British"
    end
    

    or

    post '/', :locale => "en-GB" do
      "cs-CS"
    end
    
    post '/', :locale => "cs-CS" do
      "cs-CS"
    end
    

    How to do this? First, you'll need a filter to transform the JSON coming in:

    before do
      if request.media_type == "application/json"
        request.body.rewind
        @json = JSON.parse request.body.read
        @locale = @json["locale"] && Locales[@json["locale"]]
      end
    end
    

    and then you'll need a condition to check against:

    set(:locale) {|value|
      condition {
        !!@locale && (@locale == value || @json["locale"] == value)
      }
    }
    

    All together (app.rb):

    require 'sinatra'
    
    Locales = {
      'cs-CS' => "Czech",
      'en-GB' => "British English"
    }
    
    before do
      if request.media_type == "application/json"
        request.body.rewind
        @json = JSON.parse request.body.read
        @locale = @json["locale"] && Locales[@json["locale"]]
      end
    end
    
    
    set(:locale) {|value|
      condition {
        !!@locale && (@locale == value || @json["locale"] == value)
      }
    }
    
    
    post '/', :locale => "en-GB" do
      "cs-CS"
    end
    
    
    post '/', :locale => "cs-CS" do
      "cs-CS"
    end
    

    That works but it won't work as an extension. So, relying the docs I posted at the top:

    require 'sinatra/base'
    
    module Sinatra
      module Localiser
    
        Locales = {
          'cs-CS' => "Czech",
          'en-GB' => "British English"
        }
    
        def localise!(locales=Locales)
          before do
            if request.media_type == "application/json"
              request.body.rewind
              @json = JSON.parse request.body.read
              @locale = @json["locale"] && locales[@json["locale"]]
            end
          end
    
          set(:locale) {|value|
            condition {
              !!@locale && (@locale == value || @json["locale"] == value)
            }
          }
        end
      end
      register Localiser
    end
    

    Now it will extend the DSL. For example:

    require "sinatra/localiser"
    class Localised < Sinatra::Base
      register Sinatra::Localiser
    
      localise!
    
    
      post '/', :locale => "Czech" do
        "Czech"
      end
    
    
      post '/', :locale => "British English" do
        "British"
      end
    
    
      ["get","post"].each{|verb|
        send verb, "/*" do
          "ELSE"
        end
      }
    
      run! if app_file == $0
    end
    

    Hopefully that helps clarify a few things for you.