= 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 # => # (instance)
auth # => Rodauth::Auth subclass
singleton_class.ancestors # => [#> (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 # => [#> (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