Skip to content

Granite Framework

Granite is a business actions architecture for Rails applications.

It's a combination of user interaction (attributes and validations), context (preconditions) and permissions (authorization policies).

What problems does Granite solve

Granite leverages patterns for improving productivity when developing a growing application. Instead of bloating controller and model, you put business logic in e.g. app/managers directory.

These managers deal with a user wanting to perform some action interacting with the system. The user needs to send some extra data to accomplish the task. Some subject data is going to be managed during the action.

Business actions

The central concept of Granite is a business action. Each business action can start with a simple execute_perform! method.

Hello world

Basically it is an active model-like class (form object) defined to execute a sequence of commands. The simplest business action looks like this:

class Action < Granite::Action
  private def execute_perform!(*)
    puts 'Hello World'
  end
end

There are two ways of executing newly defined business action: using #perform or #perform! method:

[1] pry(main)> Action.new.perform!
   (0.3ms)  BEGIN
Hello World
   (0.1ms)  COMMIT
=> true
[2] pry(main)> Action.new.perform
   (0.2ms)  BEGIN
Hello World
   (0.2ms)  COMMIT
=> true

As you can see from log, every action execution is wrapped inside a DB transaction. The main difference between these methods is: #perform! raises exception in case of errors and #perform simply returns false

Performer

Every BA has a performer which can be assigned via .as class method before BA creation.

MyAction.as(Admin.first).new(params)

Performer can be any Ruby object. By default performer is nil.

Attributes

The next step is defining action attributes. There are several types of them and they are provided by active_data gem:

class Action < Granite::Action
  attribute :name, String
  collection :ids, Integer

  private def execute_perform!(*)
    puts "Hello #{name}! We have the following ids: #{ids}'
  end
end

For detailed information on the available types and usage examples, check out ActiveData documentation.

The attributes behave pretty much as they do with ActiveData objects, except for represents:

Represents

In ActiveData objects, when a model attribute is exposed through represents and the AD object changes, the exposed attribute is updated right away, and Granite Actions update the represented attribute before_validation.

Associations

Granite actions can also define several associations:

class CreateBook < Granite::Action
  attribute :name, String
  references_one :author
  embeds_many :reviews
end

For more information on the associations available and usage examples, see ActiveData documentation.

NestedActions

Some business actions call other actions as part of their own action. For cases like that we should define memoizable method that returns instance of subaction.

memoize def subaction
  MySubactionClass.new
end

Subactions will validate their data and check preconditions when they're performed. This however should not be relied on and it's better to check preconditions of subaction when precondtions of main action are checked and validate subaction when main action is validated. For this we use:

precondition embedded: :subaction
validates :subaction, nested: true

Subject

Subject definition does three things: defines references_one association, aliases its methods to common names (subject and subject_id) and modifies action initializer, providing ability to pass subject as the first argument and restricting subject-less action initialization.

class Action < Granite::Action
  subject :user

  private def execute_perform!(*); end
end
[1] pry(main)> Action.new # => ArgumentError
[2] pry(main)> Action.new(User.first)
=> #<Action user: #<ReferencesOne #<User id: 1...>, user_id: 1>
[3] pry(main)> Action.new(1)
=> #<Action user: #<ReferencesOne #<User id: 1...>, user_id: 1>
[4] pry(main)> Action.new(user: User.first)
=> #<Action user: #<ReferencesOne #<User id: 1...>, user_id: 1>
[5] pry(main)> Action.new(subject: User.first)
=> #<Action user: #<ReferencesOne #<User id: 1...>, user_id: 1>
[6] pry(main)> Action.new(user_id: 1)
=> #<Action user: #<ReferencesOne #<User id: 1...>, user_id: 1>
[7] pry(main)> Action.new(id: 1)
=> #<Action user: #<ReferencesOne #<User id: 1...>, user_id: 1>

As you can see #user is aliased to #subject and #user_id is aliased to #id. Also subject call takes any combination of references_one possible options.

Policies, preconditions, validations

The main question is how to choose suitable construction. Here are simple rules:

  1. If condition is dependent on any of user provided attribute values except subject - it is a validation.
  2. If condition depends on subject or any value found depending on subject - it is a precondition.
  3. Otherwise if it is related to performer - it is a policy.

Policies

Performing restrictions for the performer:

class Action < Granite::Action
  allow_if { performer.present? }
  allow_self # equal to allow_if { performer == subject }
end

Policies support strategies. If default AnyStrategy doesn't fit your needs you can use AlwaysAllowStrategy, RequiredPerformerStrategy or write your own. You can use new strategy like that:

class Action < Granite::Action
  self._policies_strategy = MyCustomStrategy
end

Preconditions

This is a subject-related prevalidation, working in the same way as validations with blocks, but decline_with method is used instead of errors.add:

precondition do
  decline_with(:inactive) unless subject.active?
end

In case you have subactions which you perform inside your action you can check subaction preconditions by simple embedding:

precondition embedded: :my_custom_action

You can specify conditions when precondition block should be executed with :if (and only :if) statement.

precondition if: -> { subject.active? } do
  decline_with(:too_young) if subject.age < 30
end

Validations

You are able to use any of ActiveModel-provided validations.

Context validations

Context validations (see the note about the context argument) are supported and embraced by Granite. You can specify the on: key with any validation to declare a context in which the validation should be executed. Such validations will be triggered only when the provided context was specified explicitly.

To specify a context with the built-in ActiveModel methods valid? and invalid?, simply provide the context as the first argument.

To specify a context with perform, perform!, or try_perform!, pass the name of the context as a keyword argument context:.

You should use context validations when a single action could be triggered in different scenarios (e.g. by a staff member and a user) and different validation behavior is required.

Consider this simplified business action for updating a portfolio of a user:

class BA::User::UpdatePortfolio < Granite::Action
  subject :user

  represents :full_name, of: :subject

  validates :full_name, presence: true, on: :user

  private def execute_perform!(*)
    # ...
  end
end

To run a business action without context you can simply send the perform! message to the action. It won't require full_name to be present. If you want a validation to be executed in this scope you can add context argument to perform call: perform!(context: :user).

Exceptions handling

Granite has built-in mechanism for exceptions handling (similar to rescue_from known from ActionController). You are able to register handlers for any exception type, like this:

class Action < Granite::Action
  handle_exception ThirdPartyLib::APIError do |error|
    decline_with(:third_party_lib_failed)
  end

  private def execute_perform!(*)
    ThirdPartyLib.api_call
  end
end

Adding errors to action object is important, because each time handled exception is raised, Granite::Action::ValidationError is raised. Validation exception will have the same backtrace as original error. Prefer this way over custom exception handling in private methods.

I18n

There are special I18n rules working in action. If I18n identifier is prefixed with . (t('.foobar')) - then translations lookup happens in following order:

granite_action.#{granite_action_name}.foobar
granite_action.granite/action.foobar
foobar

Note that rules are different for I18n lookup inside a projector context.

Generator

You can use granite generator to generate a starting point for your action. You have to pass name and path of action as first argument. Basic usage is: rails g granite SUBJECT/ACTION [PROJECTOR]. You can use -C or --collection option to generate collection action where subject is not known when initializing action. You can pass a second argument to generator to specify projector name.

rails g granite user/create

  create  apq/actions/ba/user/create.rb
  create  apq/actions/ba/user/business_action.rb
  create  spec/apq/actions/ba/user/create_spec.rb

rails g granite user/create -C

  create  apq/actions/ba/user/create.rb
  create  spec/apq/actions/ba/user/create_spec.rb

rails g granite user/create simple

  create  apq/actions/ba/user/create/simple
  create  apq/actions/ba/user/create.rb
  create  apq/actions/ba/user/business_action.rb
  create  spec/apq/actions/ba/user/create_spec.rb