Skip to content

Instantly share code, notes, and snippets.

@jodosha
Last active December 9, 2020 15:09
Show Gist options
  • Star 53 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jodosha/9830002 to your computer and use it in GitHub Desktop.
Save jodosha/9830002 to your computer and use it in GitHub Desktop.
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
@christhekeele
Copy link

One tweak for 404s to render properly.

We don't want to throw away the default rack response body if we can't generate our own. At the least, returning nil as the body will through a Rack error since it doesn't respond to each Another option is for the default to be an empty array.

@aderyabin
Copy link

I have this output

[2014-03-28 23:50:38] ERROR Rack::Lint::LintError: Response body must respond to each
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/rack-1.5.2/lib/rack/lint.rb:20:in `assert'
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/rack-1.5.2/lib/rack/lint.rb:643:in `each'
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/rack-1.5.2/lib/rack/body_proxy.rb:31:in `each'
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/gems/2.1.0/gems/rack-1.5.2/lib/rack/handler/webrick.rb:72:in `service'
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/2.1.0/webrick/httpserver.rb:138:in `service'
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/2.1.0/webrick/httpserver.rb:94:in `run'
    /Users/aderyabin/.rbenv/versions/2.1.1/lib/ruby/2.1.0/webrick/server.rb:295:in `block in start_thread'

@aderyabin
Copy link

Please add this lines to Rakefile to start server using rake server:

task :server do
  exec 'bundle exec rackup application.rb'
end

@mdespuits
Copy link

So will much of this functionality be running behind the scenes when the lotus gem is released? I would hate to have to copy all of this over again for new apps.

@jodosha
Copy link
Author

jodosha commented Apr 7, 2014

@mattdbridges of course. That lotus.rb file will be released with the Lotus gem (not opensourced yet).
Also, I'm planning to ship it with an application generator.

@sidonath
Copy link

The ordering in the default SUFFIX in Lotus::Router made me think that the recommended structure would be something like:

module Home
  class Controller
    include Lotus::Controller

    action 'Index' do ... end
  end

  class Index
    include Lotus::View
  end
end

Which reminded me of that alternative file-structure where the MVC parts are grouped by resources (I can't find a link to the article in which I originally found this idea):

app/
  user/
    user_controller.rb
    user_views.rb
    user_model.rb
    templates/
      index.html.haml

Would something like this be easily supported by Lotus?

And I can't help but wonder about naming collision for singleton routes between container module for views and the model itself (e.g. a post model would be in Post class, but Post would also be a container module for view classes, right?

@jodosha
Copy link
Author

jodosha commented Apr 15, 2014

@sidonath bingo! That suffix is there for that specific reason.
However, I'm still evaluating if keep that option in Lotus or not, because of the name collisions.

For now, I suggest a Rails structure of the project.

@vovkats
Copy link

vovkats commented Aug 5, 2014

Maybe I'm doing something wrong, but I got an error application.rb: 32: in <top (required)> ': undefined method root =' for Lotus :: View: Module (NoMethodError). If I remove this line the application will launch

@hzm-s
Copy link

hzm-s 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

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