MagicResource

MagicResource is a Rails plugin which introduces the resource-based ideology for WEB applications development.

Getting Started

Add to your Gemfile:

  gem 'magic-resource', git: '[email protected]:anadea/magic-resource.git', tag: 'v0.0.3'

Run the bundle command to install it.

Run the generator:

rails generate magic_resource:install

Include to controller, typically to ApplicationController:

class ApplicationController < ActionController::Base
  include MagicResource::Controller

Resource abstraction

It is a quite naturally when we affecting to something, we always affecting to specific object or to the scope of alike objects. Just like the calling the instance method or class method. Even when we want to do something like turn off all electrical devices in the house, it's better and more natural to trigger the turn_off_the_devices method on House.belongs_to(me) object, rather than to implement inplace logic which will find all the devices, and do something with them.

Lets think like that when building the RoR application. Any time when the browser makes the request to the server, it affects the specific object, or to the scope of alike object. In RoR terms, each call to the server affects to specific Model instance, or to the scope of such Model.

Each Model has resource_name, typically that is the model's class name, underscored and pluralized. For Property model that will be :properties.

Then, lets add the resource_context when we affecting to the Model. It is just a Symbol identity. It should have just a logical sense, but not tied to any terms from the application like user roles, application areas, menu items etc.

For example, lets say that admin users should have an access to all crud actions for the Property model, and guest users should have acccess to show and filterable index actions only. It's better to say that Property model can be affected under :crud and :search contexts, rather than :admin and :guest ones. Then, if admin role will be renamed to superadmin, if crud actions will be available for manager as well etc - :crud context name will not loose the sense, rather than :admin context.

The affecting to the Model is always has some context, it can't be blank. Lets say that default context for any Model is :crud.

Each resource context means the separated controller and the special folder for templates. To finish the picture, lets add the single localization file, then say:

Resource is the container-like abstraction, which includes the Model, the contexts to be applied when affecting the Model, the scope of one-per-context Controllers, the scope of templates to be rendered with the Model, and single localization file.

Sources structure

Controllers

Controllers should be placed to app/controllers/#{resource_name}/#{resource_context}_controller.rb and named accordingly. For Property model and :crud context it should be placed to app/controllers/properties/crud_controller.rb and contains something like:

module Properties
    class CrudController < ApplicationController
    end
end

Models

Nothing specific, place and name them as always.

Views

There are the general folder for resource templates, named app/views/#{resource_name}/. In the root of this folter should be placed the templates which you normally want to name shared. In folters like app/views/#{resource_name}/#{resource_context}/ should be placed the context-specific templates.

The typical scope of templates for Property model under :crud context should looks like:

  • app/views/properties/crud/edit.html.haml
  • app/views/properties/crud/index.html.haml
  • app/views/properties/crud/new.html.haml
  • app/views/properties/crud/show.html.haml
  • app/views/properties/_form_attrs.html.haml
  • app/views/properties/_list_item.html.haml
  • app/views/properties/_form_search_attrs.html.haml
  • app/views/properties/_view.html.haml

Locales

The single locale file should be placed to config/locales/en/#{resource_name}.yml. For Property model it should be placed to config/locales/en/properties.yml and contains something like:

en:
    properties:
        button_edit: 'Edit Property'

Magic

When we speaking about the magic, we mean some smart and powerful instruments. Just like:

    redirect_to user

But, uncontrolled usage of such things is not good:

    redirect_to my_favorite_variable

Where we go? Lets guess that to users#show action, and we going to rename the action or controller - how we can find all places in the sources which redirects to users#show in this way?

Lets define, that the magic is good when it:

  • invariantly predictable
  • searchable in the sources
  • can be banned partially or totally in whole project

General rules

Lets separate the controller's action and templates to two group - collection and member, dependent when we working with the single Model instance of with Model's scope.

Set the resource object

In collection controller actions call resources= method:

def index
    self.resources = Property.all
end

In member controller actions call resource= method:

def show
    self.resource = Property.find(params[:id])
end

Controller will set the resource context automatically by controller's class name.

Get the resource object

In 'collection' controller actions and templates call 'resources' method:

resources.size

In 'member' controller actions and templates call 'resource' method:

resource.full_name

Magic patterns

The full pattern looks like #{resource_name}::#{resource_context}#{separator}#{identifier}. Dependent on the magic method, it changes the separator and identifier sense:

# the way to controller's action
r.path_to 'users::search#show'

# the template path
r.render 'users::search/view'

# the translation key
r.t 'users::search.button_search'

When the context name is skipped, it means the inheritance of current context for current object, or apllying the default context to other object:

r.path_to 'users#show' # inherit the current context

r(resource).path_to 'users#show' # the same as
r(resource).path_to 'users::crud#show'

MagicResource will check is the object's type matching to specified resource name:

r(Property).path_to 'users#show' # raise exception

For current object is possible to skip the resource name:

r.path_to '#index'

For current object is possible to specify just identifier:

r.path_to :show

Access to the magic

Typically you want to inherit the current resource object and resource_context for the magic. To do this, in controller or template call r method. You are able to use short patterns in magic calls, as well as combined and full patterns:

r.path_to :show
r.path_to 'properties#show'
r.path_to '::search#show'
r.path_to 'properties::search#show'

Sometimes, you want to make a magic with some other object. To do this, in controller or template call r method with object as parameter. Once the other object is specified, you are forced to use only full patterns in magic calls:

r(resource.user).path_to 'users#show'
r(resource.user).path_to 'users::search#show'

When you pass the current resource as parameter, it anyway means that the other object is specified - you loose the current context and must use the full patterns in magic calls:

r(resource).path_to 'properties#show'
r(resource).path_to 'properties::search#show'

When you don't have the specific object, pass the class as parameter. It again means the other object rules:

r(User).path_to 'users#index'

You can pass the form object as parameter, in this case the object inside it will be a the other object:

= r.form_for :update do |f|
  = f.simple_fields_for :user do |uf|
    = r(ff).render 'users/form_attrs', f: ff

Magic methods

r.context?

Check is the resource context is in the args list:

r.context?(:crud, :search) # true

r.t

The call looks like:

r.t :button_edit
r(user).t 'users::crud.button_edit', {interpolation_option: 'foo'}

It will search the translation in next order:

  • users.crud.button_edit
  • users.button_edit
  • resources.crud.button_edit
  • resources.button_edit

The method will not accept dotted keys like .my.dotted.key. But why do you want it actually? '.my_dotted_key' has the same count of chars, and it's better to separate the locales section by white space rather than to produce the one more subtree for nothing.

r.render

The call looks like:

r.render :form_attrs, f: f
r(user).render 'users::crud/form_attrs', f: f

It will search the template in next order:

  • users/crud/_form_attrs
  • users/_form_attrs
  • resources/crud/_form_attrs
  • resources/_form_attrs

The method will apply the specified object and context as current inside the target template.

The method will not accept the subfolder /form/attrs templates just like r.t method.

The accepts the form object as second param, and passes it as f local parameter:

= r.form_for :update do |f|
  = r.render :form_attrs, f

It's good to render the other resource template, rather than mass calling to it's attributes or magic methods:

that is bad:
= r(resource.user).link_to 'users#show', resource.user.name
= resource.user.role
= resource.user.last_logged_in

that is good:
= r(resource.user).render 'users#view'

and then in 'app/views/users/_view'
= r.link_to :show, resource.name
= resource.role
= resource.last_logged_in

In general, when you see in your partial the massive definition of full magic pattern - it's time to move this part to other resource's partial.

r.render_collection

The call looks like:

r.render_collection :list_item
r(users).render_collection 'users::crud/list_item'

Just calls r.render for collection items, with current context inheritance in first sample.

r.render_collection_build

The call looks like:

r.render_collection_build :new_form
r(users).render_collection_build 'users::crud/new_form'

Calls the r.render for 'object.soft_build' object (look at stokarenko/association-soft-build gem), with current context inheritance in first sample.

r.path_to

The call looks like:

r.path_to :show
r(user).path_to 'users::crud#show'

Returns the URL to specific controller/action.

To pass the URL parameters call it in hashable way:

r.path_to show: {foo: :bar}
r(user).path_to 'users::crud#show' => {foo: :bar}

r.redirect_to

Can be used only in controller.

The call looks like:

r.redirect_to :show
r(user).redirect_to 'users::crud#show'

Redirects to specific controller/action.

To pass the URL parameters call it in hashable way:

r.redirect_to show: {foo: :bar}
r(user).redirect_to 'users::crud#show' => {foo: :bar}

The call looks like:

r.link_to :edit
r(user).link_to 'users::crud#edit', 'Special edit button'

The method will build the link html tag to specific controller/action.

It will accept the URL parameters in hashable way, just like r.path_to do.

It will automatically apply the HTTP verb for the link.

It will use :button_#{action} as the label param by default, then...

It will apply r.t method if the label param is a Symbol.

It will use string label param for label as it is.

It will automatically set :confirm param to true for :destroy actions, then

It will set :confirm param to :confirm_destroy if it is true, then

It will apply r.t method for confirmation message if :confirm param if is a Symbol.

It will apply :confirm string param as confirmation message as it is.

It will accept the block to be used as label parameter.

The call looks like:

r.link_to_if true, :edit
r(user).link_to_if false, 'users::crud#edit'

Works just like regular link_to_if but uses r.link_to on success.

It will accept the URL parameters in hashable way, just like r.path_to do.

It will accept the block to be used as label parameter.

The call looks like:

r.link_to_unless false, :edit
r(user).link_to_unless true, 'users::crud#edit'

Works just like regular link_to_unless but uses r.link_to on success.

It will accept the URL parameters in hashable way, just like r.path_to do.

It will accept the block to be used as label parameter.

r.form_for

The call looks like:

r.form_for :update do |f|
end

r(user).form_for 'users::crud#update' do |f|
end

Build the form to specific controller/action.

It will accept the URL parameters in hashable way, just like r.path_to do.

It automatically set the HTTP verb.

It uses the default :simple form helper prefix, so the form will be built with simple_form_for helper.

It takes the second param as the form helper prefix to be used.

It supports Ransack, just pass Ransack::Search object as second param.

It takes the logical #save action, which will be transformed to #create or #update actions dependent on the object's persistency status.

r.content_for

This method is too much magical, and disabled by default. Activate it in configuration if you really need it.

The usage looks like:

- r(resource.user).content_for 'users:header_class', 'strong'
- r(resource.user).content_for 'users:header_prefix' do
    some super prefix

= r(resource.user).render 'users::crud/view'

Then, in app/views/users/crud/_view template:

.header{class: r.content(:header_class) || :default}
    = r.content(:header_prefix)
    the body

The same can be done by locals passing to render, but sometimes we need to forward such locals deeply and deeply to other render calls. Looks like r.content_for method is a better solution for that...

Configuration

MagicResource generator will prepare the config/initializers/magic-resource.rb config file. Please take a look on it:

MagicResource.setup do |config|
  ## Uncoment the line to change the context to be used as default.
  ## Default value is `:crud`.
  # config.default_context = :manage

  ## Uncoment the line to change default form helper prefix.
  ## Default value is `:simple`.
  # config.default_form_type = :default

  ## Change the method to adjust the level or resource magic.
  ## possible methods are `no_magic`, `try_magic?` and `full_magic!`
  ## Thechnically such methods just setting all following parameters to
  ## `:by_exception`, `:by_warning` and `false` respectively.
  config.no_magic

  ############################
  #### For all next parameters
  #### the correct values are: `[false, :by_exception, :by_warning]`.

  ## Will require to specify the resource name in helper's pattern when resource is "other".
  ## Can be useful for fast development or experiments. But typically it's better
  ## to switch it off later and define the resource names everywhere,
  ## because the magic becames to be not predictable and searchable.
  # config.force_resource_name_definition = :by_exception

  ## Will require the correct resource assignation in controller
  ## Means the situation when inside Users::CrudController you trying to do
  ## self.resource = Property.first
  # config.assert_resource_name_in_controller = :by_exception

  ## Will disable content_for helper.
  ## It is too much magical...
  ## But in some cases it is the better than other solutions.
  ## Try to avoid it.
  ## Activate it if you really need it.
  # config.disable_content_for_helper = :by_exception

end

Changes

v0.0.3

  • Added the possibility to pass URL params to routable helpers.
  • Not ActiveModel-like classes can be a resource.
  • r method will extract the object and use it as resource when form object received.
  • r.render accepts the form object as second param, and passes it as f local parameter.
  • Added r.link_to_unless helper.
  • r.link_to-like helpers accept the block as label.
  • Added r.redirect_to controller helper.

TODO

  • Implement resourcable Coccon, and make it less painful.
  • Leave r object in controller as it was set there (?).
  • Apply the passing of translation interpolation params as Hash (?).
  • Separate the helpers for controller and view.
  • Resource controllers.
  • Automatic preload in controllers, bases on statistic of association calls in templates.
  • inverse_of as default in ActiveModel (?).
  • Resource tests (?).
  • ActiveRecord translations.
  • cancancan integration.
  • Single-file navigation for whole application (?).
  • Routes definition helper.
  • Implement resources default templates, and configuration to turn them off.