rubysinatrarackup

Sinatra: modular routes class not recognizing helper methods


I am building an API in Sinatra, and have converted to a modular style. However, I am having an issue with method calls inside the routes files not being recognized.

I have simplified the app so the post is shorter, but the basic problem is that if I GET /test in WorkoutHandler - it cannot recognize methods in WardenStrategies or LoginHelper unless I also include those files in the Handler (they are already included in app.rb). However once I do that, the methods declared in the gems they use are unrecognized. All of these are registered in app.rb, and are required in my Rackup file.

Here is my app.rb file

require 'sinatra/base'
require 'sinatra/activerecord'

class WorkoutApp < Sinatra::Base

  register Sinatra::ActiveRecordExtension
  register WardenStrategies

  use WorkoutHandler

  helpers LoginHelper
  helpers HashHelpers

  use Rack::Session::Cookie
  use Warden::Manager do |manager|
    manager.default_strategies :password
    manager.intercept_401 = false
    manager.failure_app = WorkoutApp
    manager.serialize_into_session(&:id)
    manager.serialize_from_session { |id| User.find(id) }
  end
  set :database_file, '../config/database.yml'

  Warden::Manager.before_failure do |env, _opts|
    env['REQUEST_METHOD'] = 'POST'
  end
end

Here are the config.ru, Handler, and Helper files. (Handlers are just the name I use for Controller/Route files.

module LoginHelper
  def warden_handler
    env['warden']
  end

  def current_user
    warden_handler.user
  end

  def check_authentication
    return if warden_handler.authenticated?
    body 'User not authenticated'
    halt 401
  end
end

I was under the assumption that helpers, use, and register only need to be declared in app.rb (since I require all files in my rackup file and run the app from there). However the check_authentication method isn't recognized unless I register the helper here as well.

class WorkoutHandler < Sinatra::Base
  helpers LoginHelper                   # Is there a way to not need this?
  register WardenStrategies             # Or this?

  get '/test' do
    check_authentication
  end
end

module WardenStrategies
  Warden::Strategies.add(:password) do
    def valid?
      params['email'] || params['password']
    end

    def authenticate!
      user = User.find_by(email: params['email'])
      if user && user.authenticate(params['password'])
        success!(user)
      else
        fail!('Could not log in')
      end
    end
  end
end

require 'rubygems'
require 'bundler'

Bundler.require     #Shouldn't this give my Handler access to gem methods?

ENV['APP_NAME'] = 'workout'

require_all 'app'

run WorkoutApp

As of the time of this writing, I am getting the following error when running specs:

NoMethodError: undefined method `authenticated?' for nil:NilClass

This means that env doesn't have a :warden key/value (which I confirmed in console). The authenticated? method is in the Warden gem directory, so I guess I could require 'warden' in each Handler file - I don't think that's the way modular Sinatra apps are supposed to be constructed.

I've read all the blog posts and book chapters I can find on modular Sinatra apps, and I can't seem to debug my issue. From what I understand, needing to re-register (include) helpers in all files that use them is extraneous. I thought that by extending Sinatra::Base, I would have access to all declared classes in app.rb.

Any help would be appreciated. Thanks!


Solution

  • The short answer to your question is that every time you subclass Sinatra::Base, it creates a completely independent Sinatra application. Just because multiple classes subclass Sinatra::Base doesn't mean they all inherit each others' attributes. This is why it's called a "modular" style!

    If you have some common functionality to be shared between different Sinatra applications, you can either create a mixin and extend/include it in every application (which is basically what you're doing now with helpers and register), or you can create an intermediate class that your applications subclass. For example:

    my_base_app.rb:

    require 'sinatra/base'
    require 'warden'
    
    require_relative 'login_helper'
    
    class MyBaseApp < Sinatra::Base
      register WardenStrategies
    
      helpers LoginHelper
    end
    

    workout_app.rb:

    require 'sinatra/activerecord'
    require 'warden'
    
    require_relative 'my_base_app'
    require_relative 'hash_helpers'
    require_relative 'workout_handler'
    
    class WorkoutApp < MyBaseApp
      register Sinatra::ActiveRecordExtension
    
      use WorkoutHandler
    
      helpers HashHelpers
    
      use Rack::Session::Cookie
      use Warden::Manager do |manager|
        # yadda yadda...
      end
    end
    

    workout_handler.rb:

    require_relative 'my_base_app'
    
    class WorkoutHandler < MyBaseApp
      get '/test' do
        check_authentication
      end
    end
    

    As far as your question "Shouldn't this give my Handler access to gem methods?" that depends on how your Gemfile is laid out. If you have something like this:

    gem 'sinatra', :require => 'sinatra/base'
    gem 'sinatra-activerecord', :require => 'sinatra/activerecord'
    gem 'warden'
    

    Then yes, doing Bundle.require in your rackup script would eliminate the need to do individual require statements in your class files.