internals.rdoc

doc/internals.rdoc
Last Update: 2017-04-14 09:49:49 -0700

Rodauth Internals

Rodauth's implementation heavily uses metaprogramming in order to DRY up the codebase, which can be a little intimiting to developers who are not familiar with the codebase. This guide explains how Rodauth is built, which should make the internals easier to understand.

Object Model

First, let's talk about the basic parts of Rodauth.

Rodauth::Auth

Rodauth::Auth is the core of rodauth. If a user calls rodauth inside their Roda application, they get a Rodauth::Auth subclass instance. Rodauth's configuration DSL is designed to build a Rodauth::Auth subclass appropriate to the application, by loading only the features that are needed, and overriding defaults as appropriate.

Rodauth::Configuration

Inside the block you pass to plugin :rodauth, self is an instance of this class. This class is mostly empty, as most of Rodauth is implemented as separate features, and the configuration for each feature is loaded as a separate module into this instance.

Rodauth::Feature

Each of the parts of rodauth that you can use is going to be a separate feature. Rodauth::Feature is a Module subclass, and every feature you load is included in the Rodauth::Auth subclass used by the Roda application. Rodauth::Feature has many methods designed to make building Rodauth features easier by defining methods in the Rodauth::Feature instance.

Rodauth::FeatureConfiguration

Just as each feature is a module included in the Rodauth::Auth subclass for the application, each feature also contains a configuration module that is an instance of Rodauth::FeatureConfiguration (also a module subclass). For each feature you load into the Rodauth configuration, the Rodauth::Configuration instance is extended with the feature's Rodauth::FeatureConfiguration instance, which is what makes the feature's configuration methods available inside the plugin :rodauth block. This is why you need to enable the features in Rodauth before configuring them.

Object Model Example

Here's some commented output hopefully showing the relation between the different parts

Roda.plugin :rodauth do
  self                       # => #<Rodauth::Configuration> (instance)
  auth                       # => Rodauth::Auth subclass

  singleton_class.ancestors  # => [#<Class:#<Rodauth::Configuration>> (singleton class of self),
                             #     Rodauth::FeatureConfiguration::Base (instance of Rodauth::FeatureConfiguration),
                             #     Rodauth::Configuration,
                             #     ...]
  auth.ancestors             # => [Rodauth::Auth subclass,
                             #     Rodauth::Base (instance of Rodauth::Feature),
                             #     Rodauth::Auth,
                             #     ...]

  enable :login

  singleton_class.ancestors  # => [#<Class:#<Rodauth::Configuration>> (singleton class of self),
                             #     Rodauth::FeatureConfiguration::Login (instance of Rodauth::FeatureConfiguration),
                             #     Rodauth::FeatureConfiguration::Base (instance of Rodauth::FeatureConfiguration),
                             #     Rodauth::Configuration,
                             #     ...]
  auth.ancestors             # => [Rodauth::Auth subclass,
                             #     Rodauth::Login (instance of Rodauth::Feature),
                             #     Rodauth::Base (instance of Rodauth::Feature),
                             #     Rodauth::Auth,
                             #     ...]
end

Roda.rodauth                 # => Rodauth::Auth subclass
Roda.rodauth.ancestors       # => [Rodauth::Auth subclass,
                             #     Rodauth::Login (instance of Rodauth::Feature),
                             #     Rodauth::Base (instance of Rodauth::Feature),
                             #     Rodauth::Auth,
                             #     ...]

Roda.route do |r|
  rodauth                    # => Rodauth::Auth subclass instance
end

Feature Creation Example

Here's a heavily commented example showing what is going on inside a Rodauth feature.

module Rodauth
  # Feature.define takes a symbol, specifying the name of the feature. This
  # is the same symbol you would pass to enable when loading the feature into
  # the Rodauth configuration.  Feature is a module subclass, and Feature.define
  # is a class method that creates an instance of Feature (a module) and executes
  # the block in the context of the Feature instance.
  #
  # The second argument is optional, and sets the Feature instance and related
  # FeatureConfiguration instance to a constant in the Rodauth namespace, which
  # makes it easier to locate via inspect.
  Feature.define(:foo, :Foo) do
    # Inside this block, self is an instance of Feature.  As this instance of
    # Feature will be included in the Rodauth::Auth subclass instance if
    # the feature is loaded into the rodauth configuration, methods you define
    # in this block (via def or define_method) will be callable on any
    # rodauth object if this feature is loaded into the rodauth configuration.

    # Feature has many instance methods that define methods in the Feature
    # instance.  This is one of those methods, which sets the text of the notice
    # flash, shown after successful submission of the form. It's basically
    # equivalent to executing this code in the feature:
    #
    #   def foo_notice_flash
    #     "It worked!"
    #   end
    #
    # while also adding a method to the configuration which does:
    #
    #   def foo_notice_flash(v=nil, &block)
    #     block ||= proc{v}
    #     @auth.class_eval do
    #       define_method(:foo_notice_flash, &block)
    #     end
    #   end
    #
    # This is what easily allows you to modify any part of Rodauth during
    # configuration.  The Rodauth::Auth subclass has the default behavior
    # added via a method in an included module (the Feature instance), and the
    # Rodauth::Configuration instance has a method that when called defines
    # a method in the Rodauth::Auth subclass itself, which will take precedence
    # over the default method, which defined in the included Feature instance.
    notice_flash "It worked!"

    # The rest of these method calls are fairly similar to notice_flash.
    # This defines the foo_error_flash method, for the error flash message to
    # show if the form submission wasn't successful.
    error_flash "There was an error"

    # This defines the foo_view method to use template 'foo.str' in the templates
    # folder, and set the title of the page to 'Foo'.
    view 'foo', 'Foo'

    # This defines the foo_additional_form_tags method, which would generally be called
    # inside the foo.str template.
    additional_form_tags

    # This defines the foo_button method, for the text to use on the submit button
    # for the form in foo.str.
    button 'Submit'

    # This defines the foo_redirect method, for where to redirect after successful submission
    # of the form.
    redirect

    # This defines the before_foo method, called before performing the foo action.
    before

    # This defines the after_foo method, called after successfully performing the foo action.
    after

    # This defines a loaded_templates method that calls super and adds 'foo' as one of the
    # templates.  This is necessary for precompilation of templates to work.
    loaded_templates ['foo']

    # auth_value_method is a generic method that takes two arguments, a method to define
    # and a default value.  It is similar to the methods above, except that it allows
    # arbitrary method names.  The notice_flash, error_flash, button, and additional_form_tags
    # methods are actually defined in terms of this method.
    #
    # So this particular method defines a foo_error_status method that will return 401 by
    # default, but also adds a configuration method that allows you to override the default.
    auth_value_method :foo_error_status, 401

    # This is similar to auth_value_method, but it only adds the configuration method.
    # Using this should only be done if you have defining the method in the feature
    # separately (see below).
    auth_value_methods :foo_bar

    # This is similar to auth_value_methods, but it changes the configuration method so that
    # a block is required and you cannot provide an argument.  This is used for the cases
    # where a statically defined value would never make sense, such as when any correct
    # behavior would depend on accessing request-specific information.
    auth_methods :foo

    # route defines a route used for the feature.  This is the code that will be executed
    # if a user goes to /foo in the Roda app.
    route do |r|
      # Inside the block, you are in the context of the Roda instance, just as you would
      # be inside a Roda route block.  r is the RodaRequest instance, just as it would
      # be for a Roda route block.

      # route adds a before_foo_route method that by default does nothing. It also
      # adds a configuration method that you can call to set behavior that will be
      # executed before routing.
      before_foo_route

      # Just like in Roda, r.get is called for GET requests
      r.get do
        # This will render a view to the user, using the foo.erb template from the
        # templates directory (unless the user has overridden it), inside the Roda
        # application's layout.
        foo_view
      end

      # Just like in Roda, r.post is called for GET requests
      r.post do
        # This is called before performing the foo action
        before_foo

        # This assumes foo returns false or nil on failure, or otherwise on
        # success.
        if foo 
          # In general, Rodauth only calls after_foo if foo is successful.
          after_foo

          # Successful form submission will usually set the notice flash,
          # the redirect to the appropriate page.
          set_notice_flash foo_notice_flash
          redirect foo_redirect
        else
          # Unsucessful form subsmission will usually set the error flash,
          # the redisplay the page so that the submission can be fixed.
          set_error_flash foo_error_flash
          foo_view
        end
      end
    end

    # This is the default behavior for the foo method, if a user doesn't
    # call the foo method inside the configuration block.
    def foo
      # Do Something
    end

    # This is the default behavior for the foo_bar method, if a user doesn't
    # call the foo_bar method inside the configuration block.
    def foo_bar
      42
    end
  end
end