Gemsavvy
How to build a Rails app
in 48 hours without using the RailsWay™
Project
- 4 remote developers
- 48 hours
- Rails app
Setup
Outcome
- Working project
- > 100 Pull Request
- > 250 commits
- > 60 issues closed
- Only 4 hours w/o commits
- 8th (over 128 projects)
Share your jewels with your community
Caution
This talk is the exact opposite of the RailsWay™.
If you really like the RailsWay™ and have heart disease, this talk might be dangerous for you.
Layers
Model
Service
Query
Validation
Controller
Context
Presenter
Template
Helper
Do
- Define relationships
- Represent data
- Store data
Don't do
- Define queries / scopes
- Define validations
- Define business actions
- Define callbacks
Models
Queries
module Gempackages
class StatsQuery < BaseQuery
@model = Gempackage
module Scopes
def maximum_usage(threshold)
group(:id)
.having('count(*) <= ?', threshold)
.select('count(*) as usage_count, gempackages.*')
end
def maximum_stargazers(threshold)
where('github_stars <= ?', threshold)
end
def minimum_stargazers(threshold)
where('github_stars >= ?', threshold)
end
def sort_by_top_stargazers
order(github_stars: :desc)
end
end
end
end
Queries
Good
Bad
- Don't clutter model
- Still reusable
- Clear interface for user
- Bad performance
Gempackages::StatsQuery
.all(survey.gempackages)
.minimum_stargazers(Settings.gempackages.outsiders_stargazers_min_threshold)
.maximum_stargazers(Settings.gempackages.outsiders_stargazers_max_threshold)
.merge(not_much_used_gems)
.sort_by_top_stargazers
.limit(Settings.gempackages.outsiders_max_threshold)
Forms
class GemfileForm < Reform::Form
property :owner_name
property :document, virtual: true
validates :owner_name, presence: true
validates :document, presence: true
validates :document, file_size: { less_than: 2.megabytes },
file_content_type: { allow: 'application/octet-stream' }
validate :document_is_gemfile
private
def document_is_gemfile
return if document.nil?
Bundler::Definition.build(document.tempfile, nil, nil)
rescue
errors.add(:document, 'must be a Gemfile')
end
end
Forms
Good
Bad
- Don't clutter model
- Can be context dependent
- Can't perform DB validation
gemfile = Gemfile.new
form = GemfileForm.new(gemfile)
fail Errors::ValidationError.new({ form: form }) unless form.validate(params)
form.sync
Services
module Gemfiles
class CreateService < BaseService
# `attr_reader`s and `initialize`...
def call
fail Gemfiles::ClosedSurveyError.new(survey) if survey.closed?
fail Errors::ValidationError.new({ form: form }) unless form.validate(params)
form.sync
gemfile.assign_attributes attributes
gemfile.save!
perform_job(Gemfiles::ImportJob, gemfile.id)
gemfile
end
private
# helper methods
end
end
Services
Good
Bad
- Don't clutter model
- Can return anything
- Can easily handle pre/post actions
- no idea ¯\_(ツ)_/¯
def foo
gemfile_creator = Gemfiles::CreateService.new(GemfileForm, survey.id, params[:gemfile])
gemfile = gemfile_creator.call
rescue Errors::ValidationError => exception
# do some stuff
end
Presenters
class SurveyPresenter
private
attr_reader :survey
public
def initialize(survey)
@survey = survey
end
def period
opening_on = I18n.l(survey.created_at.to_date, format: :long)
closing_on = I18n.l(survey.closing_on, format: :long)
"#{opening_on} - #{closing_on}"
end
def when_is_open(&block)
block.call unless survey.closed?
end
end
Presenters
Good
Bad
- Don't clutter model
- Remove the conditional from the template
- Easily testable
- no idea ¯\_(ツ)_/¯
survey = ::Surveys::FindByCodeService.new(group.id, params[:id]).call
@survey_presenter = SurveyPresenter.new(survey)
Contexts
module Surveys
class ShowContext
private
attr_reader :survey, :stats
public
delegate :logo_url, to: :group, prefix: true
delegate :when_is_open, to: :survey_presenter
def initialize(survey, stats)
@survey = survey
@stats = stats
end
private
def survey_presenter
@survey_presenter ||= SurveyPresenter.new(survey)
end
end
end
(should be named views!)
Contexts
Good
Bad
- Send only one variable to the template
- Easily testable
- Can be hard to decide where a method belong
def show
survey = ::Surveys::FindByCodeService.new(group.id, params[:id]).call
stats = ::Surveys::GenerateStatsService.new(survey.id).call
@context = ::Surveys::ShowContext.new(survey, stats)
end
Controllers
module Groups
class SurveysController < Groups::BaseController
before_action :authorize!, only: [:new, :create, :edit, :update]
def create
survey = ::Surveys::CreateService.new(SurveyForm, group.id, params[:survey]).call
flash[:notice] = "Survey #{survey.name} has been succesfully created"
redirect_to group_path(group, token: params[:token])
rescue Errors::ValidationError => exception
@context = ::Surveys::ActionContext.new(group, exception.context[:form])
flash.now[:alert] = 'We were not able to create your survey'
render :new
end
def show
stats = ::Surveys::GenerateStatsService.new(survey.id).call
@context = ::Surveys::ShowContext.new(survey, stats)
end
# ... other actions ...
private
def survey
@survey ||= ::Surveys::FindByCodeService.new(group.id, params[:id]).call
end
end
end
Controllers
Do
Don't do
- Ask/Send data to services
- Give data to contexts
- Handle user request flow (redirections, errors,...)
- Initialize/Save models
- Execute queries/scopes
- Manipulate data
- Validate data
Helpers
module MaterializeHelper
Flash = Struct.new(:title, :classes)
FLASH_TYPES = {
"alert" => MaterializeHelper::Flash.new("Oooops!", "error"),
"notice" => MaterializeHelper::Flash.new("Well done!", "success")
}
def flash_type_to_class_names(type)
FLASH_TYPES[type].classes
end
def flash_type_to_title(type)
FLASH_TYPES[type].title
end
end
Helpers
Do
Don't do
- format purely template related content
- format business objects
- compute data on business objects
That's all folks!
Website:
Source code:
http://gemsavvy.tips
http://github.com/ruby-nord/gemsavvy
The project is Open Source,
don't hesitate to come by and contribute.
Gemsavvy
By Kevin Disneur
Gemsavvy
How to build a Rails app in 48 hours without the RailsWay
- 2,275