= 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 Rodauth::Auth subclass instance. # r is the Roda::RodaRequest subclass 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