03- Rails Basics
Last updated 09 May 2017 trailblazer v2.0Now that we’ve learned what operations do and how Trailblazer provides convenient macro steps to ease your life as a software engineer, it’s time to check out how to use operations, contracts, and cells in Rails.
Where’s the EXAMPLE CODE?
Setup
In this example we will use Trailblazer operations with Reform form objects to validate and process incoming data.
Here’s the Gemfile
.
source "https://rubygems.org"
gem "rails", "~> 5.1"
gem "activerecord"
gem "sqlite3"
gem "simple_form"
# gem "formular", github: "trailblazer/formular"
gem "dry-validation"
gem "trailblazer", ">= 2.0.3"
gem "trailblazer-rails"
gem "trailblazer-cells"
gem "cells-rails"
gem "cells-slim"
group :test do
gem "rspec-rails"
gem "capybara"
end
The trailblazer-rails
gem makes the integration a walk in the park. It pulls and invokes the trailblazer-loader
gem automatically for you via a Railtie. All Trailblazer files are eager-loaded.
In Trailblazer, we don’t believe that an ever-changing runtime environment is a good idea. Code that is, maybe, loaded, in a certain order, maybe, is a source of many production problems. Even in development mode we want an environment as close to production as possible.
This is why trailblazer-loader
always loads all TRB files at server startup. The speed decrease is about 2 seconds and is ignorable, since the automatic reloading with Rails still works.
The traiblazer-rails
gem also adds one single method #run
to the ApplicationController
which we’ll discover soon.
File Structure
You can always discover a Trailblazer application in Rails by the app/concepts
directory.
├── app
│ ├── concepts
│ │ ├── blog_post
│ │ │ ├── cell
│ │ │ │ ├── edit.rb
│ │ │ │ ├── index.rb
│ │ │ │ ├── item.rb
│ │ │ │ ├── new.rb
│ │ │ │ └── show.rb
│ │ │ ├── contract
│ │ │ │ ├── create.rb
│ │ │ │ └── edit.rb
│ │ │ ├── operation
│ │ │ │ ├── create.rb
│ │ │ │ ├── delete.rb
│ │ │ │ ├── index.rb
│ │ │ │ ├── show.rb
│ │ │ │ └── update.rb
│ │ │ └── view
│ │ │ ├── edit.slim
│ │ │ ├── index.slim
│ │ │ ├── item.slim
│ │ │ ├── new.slim
│ │ │ └── show.slim
│ │ └── user
│ │ ├── contract
│ │ │ └── create.rb
│ │ └── operation
│ │ └── create.rb
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── blog_posts_controller.rb
│ └── models
│ ├── blog_post.rb
│ └── user.rb
This is where files are structured by concept, and then by technology. What is very different to Rails has proven to be highly intuitive and emphasizes the modularity TRB brings.
For example, all classes and views related to the “blog post” concept are located in app/concepts/blog_post
. The different abstractions are represented with their own directories, such as blog_post/operation
or blog_post/contract
.
Keep in mind that it is also possible to use nested concepts as in app/concepts/admin/ui/post
.
Also, in Trailblazer we decided that all file and class names are singular which means you don’t have to think about whether or not something should be plural (it is still possible to use plural names, e.g. app/concepts/invoices/..
).
Your controllers and models, unless desired differently, are still organized the Rails Way, allowing TRB to be used in existing projects for refactoring.
Presentation Operation
Since we already covered the essential mechanics in chapter 02, we can jump directly into the first problem: how do we render a form to create a blog post?
At first, we need a presentation operation that creates an empty BlogPost
for us and sets up a Reform object which can then be rendered in a view. This operation per convention is named BlogPost::Create::Present
and sits in app/concepts/blog_post/operation/create.rb
.
class BlogPost::Create < Trailblazer::Operation
class Present < Trailblazer::Operation
step Model(BlogPost, :new)
step Contract::Build( constant: BlogPost::Contract::Create )
end
# ...
end
Those are all steps we’ve discussed in chapter 02. Create a new model, and use Contract::Build
to instantiate a Reform form that decorates the model.
It’s totally up to you whether or not you want a separate file for Present
operations, or if you want to name them New
and Edit
. The convention shown here is in use in hundreds of applications and has evolved as a best-practice over the last years.
Contract
The interesting part in the Present
operation is the :constant
option: it references the BlogPost::Contract::Create
class, which itself lives in app/concepts/blog_post/contract/create.rb
.
require "reform"
require "reform/form/dry"
module BlogPost::Contract
class Create < Reform::Form
include Dry
property :title
property :body
validation do
required(:title).filled
required(:body).maybe(min_size?: 9)
end
end
end
Contracts can be pure dry-validation
schemas or Reform objects that can in turn use dry-validation
or ActiveModel::Validations
as their validation engine. Using a Reform object, whatsoever, will allow rendering that form in a view.
Contract Rendering
We now have the form and operation in place and are ready to hook that into the BlogPostsController
’s new
action.
class BlogPostsController < ApplicationController
def new
run BlogPost::Create::Present
render cell(BlogPost::Cell::New, @form), layout: false
end
The run
method invokes the operation, optionally passes dependencies such as the current_user
into the operation’s call
, and then sets some default variables such as @model
and @form
for you.
The instance variables in the controller are only set for your convenience and could be retrieved via the result object, too. → API
After running the operation and retrieving the contract instance, it is now time to render a view with a form, that we can actually fill out and publish our blog post. This happens via render
and by invoking a cell. The cell’s job is rendering the form, so we need to pass the @form
object to it.
BTW, the Cells gem and the rendering layer it brings is completely optional. If you want, you can keep using ActionView rendering along with operations.
Form Cell
The BlogPost::Cell::New
cell is responsible for rendering this view. We will discuss its internals later, but for a quick preview, here’s the cell class.
module BlogPost::Cell
class New < Trailblazer::Cell
include ActionView::RecordIdentifier
include ActionView::Helpers::FormOptionsHelper
include SimpleForm::ActionViewExtensions::FormHelper
end
end
The include
s are necessary to import all helpers we need in the view.
The first line module BlogPost::Cell
is crucial as it creates the module constant Cell
in the BlogPost
namespace. It has to be in one single line, otherwise you will get strange constant errors due to a never-fixed bug in Ruby.
As all we do is rendering the view, there’s no real code yet. Speaking of views, here is the cell’s view in app/concepts/blog_post/view/new.slim
.
.row.new
h1 New Post
= simple_form_for model do |f|
.row
.col-sm-12
= f.input :title, placeholder: "Title", label: false
.row
.col-sm-12
= f.input :body, placeholder: "Say it out loud!", label: false
.row
.col-sm-12
= f.submit 'Create Post'
Enough code to render the blog form. It looks a bit sad without any layout, but we’ll come to that shortly.
Submitting this very form will POST it to /blog_posts/
, which is the next controller action we have to implement.
Form Processing
Again, we run an operation. This time it’s BlogPost::Create
.
Can you see how controller actions map to operations? This is because in Rails apps, actions correspond to specific application functions (“create blog post”, “search user”, “add comment”), and since the business logic should be encapsulated in operations, you will always find controller actions simply dispatching to one operation.
However, this doesn’t mean you couldn’t use operations for all kinds of smaller tasks, or in background jobs, or as console commands, too.
Here’s the controller action dispatching the operation.
def create
run BlogPost::Create do |result|
return redirect_to blog_posts_path
end
render cell(BlogPost::Cell::New, @form), layout: false
end
A nice detail about run
is: it executes an optional block when the operation was run successfully. This means we can redirect to the index page in case of a successful blog post create. Otherwise, we re-render the form cell.
Please note that there’s a return
in the block, causing the controller’s execution to stop. If you forget this, the rest of the create
method will be executed, too.
Let’s check out the BlogPost::Create
operation in detail, now.
Create
While the operation we implemented earlier only creates an unpersisted model, the BlogPost::Create
operation also processes the submitted form data and physically persists the model in case of validity.
To understand how the operation knows whether or not it was run successful, and how this entire workflow is implemented, we should have a look at the code in app/concepts/blog_post/operation/create.rb
.
class BlogPost::Create < Trailblazer::Operation
class Present < Trailblazer::Operation
step Model(BlogPost, :new)
step Contract::Build( constant: BlogPost::Contract::Create )
end
step Nested( Present )
step Contract::Validate( key: :blog_post )
step Contract::Persist( )
step :notify!
def notify!(options, model:, **)
options["result.notify"] = Rails.logger.info("New blog post #{model.title}.")
end
end
Don’t get confused by the nested Present
class in Create
! This is only Ruby’s way of namespacing and doesn’t leak any logic or state into the Create
operation.
The Create
operation definition starts with the step Nested ( .. )
statement. This is where we’re reusing the existing Present
operation to create the model and contract for it. After that’s done, we run validations, persist the data to the model (in case the validation is happy) and send a notification after it.
The Nested
step macro runs the Present
operation and copies the key/value pairs from its result object into Create
our result object. Of course, this all happens at run-time.
We wrote enough code to have a fully working setup to create, validate and persist blog posts along with post-processing logic such as sending out notifications, which would usually happen in an ActiveRecord callback.
While an invalid form submission will re-render the form, sane data causes us to get redirect to /blog_posts/
, aka the index action.
Index
The controller index
method, again, simply dispatches to the BlogPost::Index
operation and then uses its cell to render the list.
def index
run BlogPost::Index
render cell(BlogPost::Cell::Index, result["model"]), layout: false
end
We’re going to discuss the cell in another chapter. Here’s the operation found in app/concepts/blog_post/operation/index.rb
.
class BlogPost::Index < Trailblazer::Operation
step :model!
def model!(options, *)
options["model"] = ::BlogPost.all.reverse_order
end
end
In line with what we did in Create::Present
, the Index
operation’s job is to retrieve a list of posts and expose them via the result object’s "model"
field.
Remember that this is the place where pagination, filtering, or even delegating to Query
objects might happen. So far, we didn’t see the need to introduce framework-based query objects, but with increasing usage this might be an optional feature in a future version.
Also, the name "model"
- despite it being an array - is purely conventional.
This array is then passed into the cell and renders an index list. In that list, clicking a link will direct you to a URL such as /blog_posts/1
, which corresponds to a show
action.
Show
The show
action in the controller now doesn’t look anything special, anymore.
def show
run BlogPost::Show
render cell(BlogPost::Cell::Show, result["model"]), layout: false
end
Given that we’ve just learned about the Index
operation, the Show
class in app/concepts/blog_post/operation/index.rb
is almost boring.
class BlogPost::Show < Trailblazer::Operation
step Model(BlogPost, :find_by)
end
Trailblazer already ships with the Model
macro to retrieve the model and assign it to the options
object.
We use find_by
, which will either return the model matching the :id
field, or nil
. This has the advantage that there won’t be an evil exception breaking our flow, but the operation will automatically jump to the error track if it can’t find a model.
A colorless, but functional view of a particular blog post is the reward for our hard work.
Admittedly, this looks quite bare-bones, but we can read the blog post, and have links to edit and delete it.
When clicking Edit we will be redirect to /blog_posts/1/edit
. This is just another controller action dispatching to the BlogPost::Update::Present
operation.
Edit
When editing an existing blog post, the BlogPostsController#edit
action is invoked.
def edit
run BlogPost::Update::Present
render cell(BlogPost::Cell::Edit, @form), layout: false
end
The processing part of the update flow is implemented via the #update
action.
def update
run BlogPost::Update do |result|
flash[:notice] = "#{result["model"].title} has been saved"
return redirect_to blog_post_path(result["model"].id)
end
render cell(BlogPost::Cell::Edit, @form), layout: false
end
Again, the cells views will be discussed in chapter 05. Let’s focus on the business logic here.
Update
The following code file are the BlogPost::Update::Present
and BlogPost::Update
operations used in the two controller actions just introduced. Just as their Create
counterpart, we put them in app/concepts/blog_post/update.rb
.
class BlogPost::Update < Trailblazer::Operation
class Present < Trailblazer::Operation
step Model(BlogPost, :find_by)
step Contract::Build( constant: BlogPost::Contract::Create )
end
step Nested(Present)
step Contract::Validate( key: :blog_post )
step Contract::Persist()
end
Note that in the Model
step, we use the :find_by
method, just as we did for Show. Another interesting fact here’s that the Create
contract is reused. At the moment, we don’t need differing validations for the editing logic, which allows us to lazily point to that existing class.
The last missing piece for our simple CRUD example is deleting: we want to be able to trash articles that no longer fit.
Delete
The controller action #destroy
is responsible for triggering the respective delete logic.
def destroy
run BlogPost::Delete
flash[:alert] = "Post deleted"
redirect_to blog_posts_path
end
Since we don’t have anything that could go wrong, yet, we don’t need the block for run
. The operation in app/concepts/blog_post/operation/delete.rb
is very simple, too.
class BlogPost::Delete < Trailblazer::Operation
step Model(BlogPost, :find_by)
step :delete!
def delete!(options, model:, **)
model.destroy
end
end
The Model
macro helps with finding the right blog post instance, and a custom delete!
step actually deletes the model using ActiveRecord’s destroy
method.
Summary
Building a simple CRUD component for a model is very easy with Trailblazer. In this chapter, we really only focused on the business code, and we will learn about Cells in chapter 05.
However, and this is a terrible thing to do, we’ve totally neglected testing! Testing with Trailblazer is incredibly simple and much more straight-forward as compared to Rails and its quite fragmented testing style. We’ll discover the world of testing in the next chapter, and only once we’re finished you can sit back and be proud of your work.