Ruby
Article

Decoupling with Lotus

By Robert Qualls

cover

There’s no question that the term “DSL-Oriented-Programming” could be applied to Rails.

According to the Lotus website, Lotus is a web framework that aims to make Ruby web development more object-oriented. Lotus features encapsulation of MVC layers and no monkey-patching.

We won’t be making an app in Lotus, but we’ll be looking at its constituents to see what it offers.

Rails is magical for a reason. When we see that Lotus takes away a lot of that magic, it’s easy to be skeptical. So, try to keep an open mind.

How It’s Different

On the surface, Lotus initially looks like another “me-too” framework that accomplishes things just like Rails. But as we’ll see, Lotus also supports a less opinionated, more object-oriented approach to building apps.

Instead of one framework, Lotus is currently composed of several gems:

  • Lotus::Router
  • Lotus::Utils
  • Lotus::Validations
  • Lotus::Model
  • Lotus::View
  • Lotus::Controller
  • Lotus::Assets (not officially part of framework yet)

The gems are designed to be as independent as possible, so if you like, you can just use Lotus::Router for routing and Mongoid or Sequel for models.

Keep in mind that Lotus is early in development, so some of this is likely to change.

Lotus::Router

In Rails, it’s easy to forget that everything goes through Rack underneath the surface. Lotus, on the other hand, lives just a couple houses down from Rack.

$ gem install lotus-router --version 0.2.0
$ gem install rack --version 1.6.0

Lotus lets you place a different Rack app at each route. So you could have a sinatra app persisting to mongoid at ‘/dashboard’ and a camping app persisting to PostgreSQL at ‘/posts’.

Here’s a very basic app that uses Lotus::Router.

require 'lotus/router'
require 'rack'

@router = Lotus::Router.new
@router.get '/', to: ->(env) { [200, {}, ['Hello from Lotus']] }

def app(env)
  @router.call(env)
end

Rack::Handler::WEBrick.run method(:app)

env is the hash that contains information about the request and the server. It’s the lingua franca of the Rack middleware system.

If you like, Lotus::Router does provide a very familiar DSL.

Lotus::Router.new do
  get '/', to: ->(env) { [200, {}, ['Hello World']] }
  get '/some-rack-app',  to: SomeRackApp.new
  get '/posts',          to: 'posts#index'
  get '/posts/:id',      to: 'posts#show'

  mount Api::RackApp, at: '/api'

  namespace 'admin' do
    get '/users', to: AdminUsers::Index
  end

  resources 'users' do
    member do
      patch '/award'
    end

    collection do
      get '/search'
    end
  end
end

Lotus::Model

$ gem install lotus-model -- version 0.2.0

The way lotus handles models is probably the least magical thing about it.
Right now the persistent data situation is broken up into:

  • Lotus::Model::Entity – defines attributes (for objects, not tables)
  • Lotus::Model::Repository – mediates between entities and persistence layer (this is where you call CRUD methods)
  • Lotus::Model::Mapper – keeps entities separate from database details
  • Lotus::Model::Adapter – implements persistence logic
  • Lotus::Model::Adapters::SomeAdapter::Query – query implementation for an
    adapter
  • Lotus::Model::Adapters::SomeAdapter::Command – gets passed the query in
    the adapter
  • Lotus::Model::Adapters::SomeAdapter::Collection – insertion/manipulation
    implementation for an adapter

So, about 50 hours of Gregg Pollack talking and pointing. Here’s what the system
looks like in practice:

require 'lotus/model'

class Post
  include Lotus::Entity
  attributes :title, :content
end

class PostRepository
  include Lotus::Repository
end

Lotus::Model.configure do
  adapter type: :sql, uri: 'postgres://localhost/database'

  mapping do
    collection :posts do
      entity      Post
      respository PostRepository

      attribute :id,      Integer
      attribute :title,   String
      attribute :content, String
    end
  end
end

Lotus::Model.load!

post = Post.new(title: 'First Post', content: 'This is my first post.')
post = PostRepository.create(post)

puts post.id # => 1

p = PostRepository.find(post.id)
p == post # => true

Obvious question: what about the database schema? Migrations, how do they work?

Right now, you’re on your own. The latest information regarding migrations is a trello
card
marked as “Planned”. In gitter, the Sequel gem is mentioned as a way of going about migrations, although you could use it for the entire model as well.

Lotus::Validations

I you haven’t, install the gem.

$ gem install lotus-validations --version 0.2.2

Lotus validations work like ActiveRecord in that a class specifies attributes and their associated validations. Unlike in ActiveRecord, Lotus validations are separate from persistence logic. This makes it easy to use them for a wider variety of things.

require 'lotus/validations'

class User
  include Lotus::Validations

  attribute :name, format: /\A[a-zA-z]+\z/
end

robert = User.new(name: 'Robert')
puts robert.valid?  # => true

invalid = User.new(name: '!Robert')
puts invalid.valid? # => false

Note that since the validations library does not deal with persistence, uniqueness validations are not implemented. According to the Lotus philosophy, uniqueness should be enforced in the database itself (MongoDB example).

Lotus::View

The Lotus view layer is based on the principle of separation between views and templates. Lotus views are objects, so they can be tested.

$ gem install lotus-view --version 0.3.0

You might be wondering what a view object would do. Lotus views take on some of the responsibilities typically handled by controllers in Rails, like rendering and data-sharing.

Here’s what using a Lotus view looks like:

require 'lotus/view'

module Posts
  class Index
    include Lotus::View
  end

  class XMLIndex < Index
    format :xml 
  end
end

Lotus::View.configure do
  root 'app/templates'
end

Lotus::View.load!

posts = PostRepository.all

# Uses Posts::Index and "posts/index.html.erb"
Posts::Index.render(format: :html, posts: posts)

# Uses Posts::XMLIndex and "posts/index.xml.erb"
Posts::Index.render(format: :xml, posts: posts)

Lotus::Controller

In Lotus, a controller is nothing more than a plain Ruby module that contains a group of actions.

$ gem install lotus-controller --version 0.3.1

In Rails, an action is a method on a controller. In Lotus, an Action is an
object.

require "lotus/controller"

class Show
  include Lotus::Action

  def call(params)
    self.status = 210
    self.body = 'Hello World'
    self.headers.merge!({ 'X-Some-Custom-Header' => 'TRUDAT'})
  end
end

show = Show.new
puts show.call({ some_param: 5}).inspect
# => [210, {"X-Some-Custom-Header"=>"TRUDAT", "Content-Type"=>"application/octet-stream; charset=utf-8"}, ["Hello World"]]

The main action method to be concerned with is #call which returns a serialized Rack::Response.

Because actions are objects, we can use dependency injection to create generic actions. The consequence is it’s easier to create testable actions.

require "lotus/controller"

class Show
  include Lotus::Action

  def initialize(repository = Post)
    @repository = repository
  end

  def call(params)
    @whatever = @repository.find params[:id]
  end
end

show = Show.new(MemoryPostRepository)
show.call({ id: 5 })

Lotus lets you supply before callbacks like Rails’ before_action. Except now you’re doing it in the action instead of the controller. This might seem to defeat the purpose, but you still want reusable methods in your actions in case you want to add them to other actions. For example, maybe you have an authentication method that you want to add to an action just by including a mixin and setting it in before.

require "lotus/controller"
require "your_project/mixins/authentication

class Show
  include Lotus::Action
  include YourProject::Mixins::Authentication

  before { authenticate! }

  def call(params)
  end
end

In a Rails controller, actions share data with views by setting instance variables and sharing context. Lotus accomplishes this with the expose DSL command. By default, all Lotus actions expose #params and #errors.

require "lotus/controller"

class Show
  include Lotus::Action

  expose :post

  def call(params)
    @post = Post.find params[:id]
  end
end

action = Show.new
action.call({ id: 5 })

puts action.post.id    # => 5
puts action.exposures  # => { post: <a Post object> }

For security, Lotus actions provide whitelisting of allowed parameters.

require 'lotus/controller'

class Upload
  include Lotus::Action

  params do
    param :title
    param :content
  end

  def call(params)
    puts params[:title]       # => "The Foo in The Bar"
    puts params[:l33texploit] # => nil
  end
end

Note: You do not do any rendering in Lotus actions. Rendering occurs in views as shown in the previous section. Lotus controllers are just pure HTTP endpoints for the router.

Lotus::Utils

$ gem install lotus-utils --version 0.3.3

If you have used Ruby on Rails, you have used ActiveSupport. It’s the module that provides syntactic sugar like this:

28.days.ago

Currently, Lotus::Utils is not a Lotus-equivalent of Rails’ ActiveSupport. It’s mostly a collection of various data type enhancements. Something to note is the lack of monkey-patching.

Here’s what you get at the moment:

  • Lotus::Utils::String – lets you #underscore and #demodulize
  • Lotus::Utils::PathPrefix – subclasses Lotus::Utils::String
  • Lotus::Utils::Class – lets you load classes by pattern name from a given
    namespace
  • Lotus::Utils::Hash – wraps Hash and adds some methods like
    #symbolize! and #stringify!.
  • Lotus::Utils::Attributes – sets/gets a Lotus hash where all the keys are
    coerced to strings (think ActiveSupport::HashWithIndifferentAccess), performs a deep duplication with #to_h
  • Lotus::Utils::LoadPaths – “A collection of loading paths” (unsure what
    this really does for us, please leave a comment if you know)
  • Lotus::Utils::Kernel – contains type coercion methods
  • Lotus::Utils::ClassAttributes – “inheritable class level variable
    accessors”
  • Lotus::Utils::Callback – stores and runs procs
  • Lotus::Utils::Deprecation – used to print deprecation messages

These different components in the lotus-utils gem will need to be required separately (except in a couple of situations where one depends on another).

require 'lotus/utils/string'

puts Lotus::Utils::String.new('MyClass').underscore
# => 'my_class'
puts Lotus::Utils::String.new('Lotus::Utils::Hash').demodulize
# => 'Hash'
puts Lotus::Utils::String.new('my_class').classify
# => 'MyClass'

Note that it does not look like the goal is to replace ActiveSupport.

Here is a snippet of conversation on gitter where someone asks the maintainer about adding ActiveSupport-style calculations:

lotus_chat

So, if you were hoping to have an ActiveSupport that you can load in piecemeal instead of autoloading monkey-patches, it looks like you will have to wait.

Lotus::Assets

lotus-assets is not official just yet, but a version 0.0.0 gem is up. Right now the trello roadmap lists “Assets helpers” and “Development precompilers (CoffeeScript and SASS)” as in
development.

Conclusion

Lotus certainly makes some interesting points. It probably had many of you at “no monkey patching”. On the other hand, the magic in Rails is what brings the boys to the Yard. Does it really need to be fixed by purists? You decide. At the very least, if the lack of testability or reusability in Rails frustrates you, this is the project you want to be working on.

Luca Guidi, the creator of Lotus, put out this post at the beginning of the year to discuss Lotus’ roadmap. It’s definitely worth a read, so check it out.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.