Skip to content

Instantly share code, notes, and snippets.

@jodosha
Last active Dec 9, 2020
Embed
What would you like to do?
Full stack Lotus application example

Lotus

The Lotus gem wasn't released yet, but all the core frameworks are out at the version 0.1.0. This is a guide to build a full stack Lotus application today.

Installation

Clone this gist:

% git clone https://gist.github.com/9830002.git lotus

Setup:

% cd lotus && bash setup.sh

Run the specs:

% bundle exec rake

Architecture

This guide has two files to look at:

  1. lotus.rb
  2. application.rb

Lotus

The code in this file is a simplfied version of the gem that will be released in the near future.

Lotus' frameworks are known to have few conventions and they are agnostic by design. This characteristic let them to be used in already existing projects.

However, I want Lotus apps to be different. They will take small decisions for the developers, in order to help in their day to day work.

For this reason, Lotus gem brings some behind the scenes enhancements of their capabilities. It will inject glue code to make all the components to work together.

One case is Lotus::View.load!: I don't expect developers to deal with this low level details, Lotus will do this job for you.

This is the meaning of the FullStackPatch module, it adds some behaviors to Lotus::Action.

Another convention that you will see is the matching between action and view names (see RenderingPolicy). Given a HomeController::Index, Lotus expects to find Home::Index view. This mechanism may not match your thinking, that's why a Lotus::Application let to inject this policy.

Application

What you see here is the code of a Lotus application. Generally, all these classes are split across files, but here I kept them together to emphatize the matching parts.

HomeController::Index is a typical example of Lotus::Controller. When called, it sets the value of @planet, it will be available accesible using #planet or #exposures.

The most important aspect of Home::Index is #greet. The implementation of this method uses a concrete method #salutation with something that cames from the action: #planet.

That information can be accessed here because Action#to_rendering returns a context that matches Lotus::View needs (see Lotus::View::Rendering#render).

If you try:

action = HomeController::Index.new
action.call({'HTTP_ACCEPT' => 'text/html'})

action.to_rendering # => { planet: 'World', format: :html }

That output is exactly what Home::Index.render expects.

The rest of the file deals with loading mechanisms and a compatibility with Rack::Builder.

Please notice that Application constant is assigned as Capybara.app in spec/spec_helper.rb. This makes possible to run features tests with RSpec.

Feedback

If you have any question or feedback, please leave a comment here or ping me on Twitter: @jodosha.

Happy hacking!

require 'lotus/router'
require 'lotus/controller'
require 'lotus/view'
require_relative 'lotus'
class HomeController
include Lotus::Controller
action 'Index' do
expose :planet
def call(params)
@planet = 'World'
end
end
end
module Home
class Index
include Lotus::View
def salutation
'Hello'
end
def greet
"#{ salutation }, #{ planet }!"
end
end
end
Lotus::View.root = __dir__ + '/templates'
Lotus::View.load!
router = Lotus::Router.new do
get '/', to: 'home#index'
end
Application = Rack::Builder.new do
run Lotus::Application.new(router)
end.to_app
source 'https://rubygems.org'
gem 'rake'
gem 'lotus-router'
gem 'lotus-controller'
gem 'lotus-view'
group :test do
gem 'rspec'
gem 'capybara'
end
require 'spec_helper'
feature 'Home page' do
it 'successfully visits the home page' do
visit '/'
expect(page.body).to match('Hello, World!')
end
end
module FullStackPatch
def response
[ super, self ].flatten
end
def format
Rack::Mime::MIME_TYPES.invert[content_type].gsub(/\A\./, '').to_sym
end
def to_rendering
exposures.merge(format: format)
end
end
Lotus::Action::Rack.class_eval do
prepend FullStackPatch
end
module Lotus
class Application
def initialize(router, renderer = RenderingPolicy.new)
@router = router
@renderer = renderer
end
def call(env)
_call(env).tap do |response|
response[2] = Array(@renderer.render(response))
end
end
private
def _call(env)
env['HTTP_ACCEPT'] ||= 'text/html'
@router.call(env)
end
end
class RenderingPolicy
def render(response)
if render?(response)
action = response.pop
view = view_for(action)
view.render(action.to_rendering)
end
end
private
def render?(response)
response.last.respond_to?(:to_rendering)
end
def view_for(action)
Object.const_get(action.class.name.gsub(/Controller/, ''))
end
end
end
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
task default: :spec
task :server do
exec 'bundle exec rackup application.rb'
end
#!/bin/bash
mkdir -p spec/features
mkdir -p templates/home
mv spec_helper.rb spec
mv home_page_spec.rb spec/features
mv index.html.erb templates/home
bundle
# This script will self destruct in 3, 2, 1.. poof!
rm -- "$0"
$:.unshift __dir__ + '/..'
require 'rubygems'
require 'bundler'
Bundler.require(:default, :test)
require 'rspec'
require 'capybara/rspec'
require 'application'
module RSpec
module FeatureExampleGroup
def self.included(group)
group.metadata[:type] = :feature
Capybara.app = Application
end
end
end
RSpec.configure do |c|
def c.escaped_path(*parts)
Regexp.compile(parts.join('[\\\/]') + '[\\\/]')
end
c.color = true
c.include RSpec::FeatureExampleGroup, type: :feature, example_group: {
file_path: c.escaped_path(%w[spec features])
}
c.include Capybara::DSL, type: :feature
end
@haazime
Copy link

haazime commented Sep 3, 2014

I got this error too.

$ bundle exec rake
~/lotus/application.rb:32:in `<top (required)>': undefined method `root=' for Lotus::View:Module (NoMethodError)

Edit application.rb like this, and work fine!

#Lotus::View.root = __dir__ + '/templates'
Lotus::View.configure do
  root __dir__ + '/templates'
end
Lotus::View.load!

@yondermon
Copy link

yondermon commented Apr 30, 2015

Lotus gem is awesome, but since it stick together all parts of lotus microframeworks, I can't figure out how to exclude views from some app (API, for example) and keep it for others.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment