├── tmp └── .gitignore ├── .gitignore ├── models └── post.rb ├── spec ├── spec.opts ├── spec_helper.rb ├── models │ └── person_spec.rb └── app_spec.rb ├── concepts └── post │ ├── operation │ ├── dry_error_messages.yml │ ├── update.rb │ └── create.rb │ ├── cell │ ├── show.rb │ └── new.rb │ └── view │ ├── show.slim │ └── new.slim ├── Rakefile ├── public └── app.css ├── gemgem ├── cell │ └── layout.rb └── view │ └── layout.slim ├── Gemfile ├── README.md ├── config ├── migrations.rb └── init.rb ├── app.rb ├── MIT-LICENSE └── Gemfile.lock /tmp/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | -------------------------------------------------------------------------------- /models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < Sequel::Model 2 | end -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --colour 2 | --format progress 3 | --loadby mtime 4 | --reverse 5 | -------------------------------------------------------------------------------- /concepts/post/operation/dry_error_messages.yml: -------------------------------------------------------------------------------- 1 | en: 2 | errors: 3 | unique?: "%{name} not unique" 4 | -------------------------------------------------------------------------------- /concepts/post/cell/show.rb: -------------------------------------------------------------------------------- 1 | module Post::Cell 2 | class Show < Trailblazer::Cell 3 | property :title 4 | property :content 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /concepts/post/view/show.slim: -------------------------------------------------------------------------------- 1 | a href="/posts/#{model.id}/edit" 2 | | Edit 3 | 4 | h1 5 | = title 6 | 7 | .content 8 | = content 9 | 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "Run those specs" 2 | task :spec do 3 | require 'spec/rake/spectask' 4 | 5 | Spec::Rake::SpecTask.new do |t| 6 | t.spec_files = FileList['spec/**/*_spec.rb'] 7 | end 8 | end -------------------------------------------------------------------------------- /concepts/post/cell/new.rb: -------------------------------------------------------------------------------- 1 | module Post::Cell 2 | class New < Trailblazer::Cell 3 | private 4 | def url 5 | options[:url] || raise("no action URL!") 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | body { background-color: #fff; color: #333; } 2 | 3 | body, p, ol, ul, td { 4 | font-family: verdana, arial, helvetica, sans-serif; 5 | font-size: 13px; 6 | line-height: 18px; 7 | } 8 | 9 | -------------------------------------------------------------------------------- /gemgem/cell/layout.rb: -------------------------------------------------------------------------------- 1 | module Gemgem 2 | module Cell 3 | class Layout < Trailblazer::Cell 4 | self.view_paths = ["."] 5 | 6 | def content 7 | @options[:content] 8 | end 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /concepts/post/operation/update.rb: -------------------------------------------------------------------------------- 1 | require_relative "create" 2 | 3 | class Post::Update < Post::Create 4 | contract do 5 | property :url_slug, readonly: true 6 | end 7 | 8 | def model!(params) 9 | Post[params[:id]] 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '..', 'app.rb') 2 | 3 | require 'rack/test' 4 | require 'ruby-debug' 5 | require 'spec' 6 | require 'spec/autorun' 7 | require 'spec/interop/test' 8 | 9 | # set test environment 10 | set :environment, :test 11 | set :run, false 12 | set :raise_errors, true 13 | set :logging, false 14 | 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem "sinatra" 4 | gem "sinatra-sequel" # persistence 5 | gem "sqlite3" 6 | gem "trailblazer", "1.1.0.rc1" # operation 7 | # gem "reform", "2.1.0.rc1" # validation 8 | gem "reform", path: "../reform" # validation 9 | gem "dry-validation", ">= 0.4.0" 10 | gem "cells" # presentation 11 | gem "cells-slim", ">= 0.0.4" 12 | 13 | 14 | gem "trailblazer-cells", path: "../trailblazer-cells" 15 | 16 | gem "rerun" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sinatra With Trailblazer 2 | 3 | _Cause We Can._ 4 | 5 | ## Stack 6 | 7 | The following gems are used in this project. 8 | 9 | | Gem | Layer | 10 | |---|---| 11 | | Sinatra | Infrastructure, Routing | 12 | | Trailblazer | Business | 13 | | Reform | Validation | 14 | | Dry-validation | Validation | 15 | | Cells | Presentation | 16 | | Slim | Presentation | 17 | 18 | ## Run 19 | 20 | In development, start the server with 21 | 22 | ```ruby 23 | rerun "bundle exec ruby app.rb" 24 | ``` 25 | 26 | And browse to `http://localhost:4567/posts/new`. -------------------------------------------------------------------------------- /config/migrations.rb: -------------------------------------------------------------------------------- 1 | # Migrations will run automatically. The DSL like wrapper syntax is courtesy 2 | # of sinatra-sequel 3 | # 4 | # For details on sequel's schema modifications, check out: 5 | # http://sequel.rubyforge.org/rdoc/files/doc/schema_rdoc.html 6 | 7 | migration "create the posts table" do 8 | database.create_table :posts do 9 | primary_key :id 10 | string :title 11 | string :content 12 | string :url_slug 13 | end 14 | end 15 | 16 | migration "add URL slug to posts" do 17 | database.add_column :posts, :url_slug, String 18 | end 19 | -------------------------------------------------------------------------------- /concepts/post/view/new.slim: -------------------------------------------------------------------------------- 1 | h1 New Post! 2 | 3 | form action="#{url}" method="post" 4 | .row 5 | input type="text" name="title" value=model.contract.title placeholder="Title" 6 | .row 7 | input type="text" name="url_slug" value=model.contract.url_slug disabled=model.contract.options_for(:url_slug)[:readonly] placeholder="URL slug" 8 | .row 9 | textarea name="content" placeholder="And your story..." rows="9" 10 | =model.contract.content 11 | .row 12 | input type="submit" class="large button expand" 13 | 14 | = model.contract.errors.messages.inspect 15 | -------------------------------------------------------------------------------- /concepts/post/operation/create.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../../models/post" 2 | 3 | class Post::Create < Trailblazer::Operation 4 | contract do 5 | property :title 6 | property :url_slug 7 | property :content 8 | 9 | include Reform::Form::Dry::Validations 10 | 11 | validation :default do 12 | key(:title, &:filled?) 13 | key(:url_slug) { |slug| slug.format?(/^[\w-]+$/) && slug.unique? } 14 | 15 | def unique?(value) 16 | form.model.class[url_slug: value].nil? 17 | end 18 | 19 | configure { |config| 20 | config.messages_file = 'concepts/post/operation/dry_error_messages.yml' 21 | } 22 | end 23 | end 24 | 25 | def model!(*) 26 | Post.new 27 | end 28 | 29 | def process(params) 30 | validate(params) do 31 | contract.save 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require "sinatra" 2 | require_relative "config/init" 3 | 4 | get "/posts/new" do 5 | op = Post::Create.present({}) 6 | Post::Cell::New.(op, url: "/posts", layout: Gemgem::Cell::Layout).() 7 | end 8 | 9 | post "/posts" do 10 | op = Post::Create.run(params) do |op| 11 | redirect "/posts/#{op.model.id}" 12 | end 13 | 14 | Post::Cell::New.(op, url: "/posts", layout: Gemgem::Cell::Layout).() 15 | end 16 | 17 | get "/posts/:id" do 18 | op = Post::Update.present(params) 19 | 20 | Post::Cell::Show.(op.model, url: "/posts", layout: Gemgem::Cell::Layout).() 21 | end 22 | 23 | get "/posts/:id/edit" do 24 | op = Post::Update.present(params) 25 | Post::Cell::New.(op, url: "/posts/#{op.model.id}", layout: Gemgem::Cell::Layout).() 26 | end 27 | 28 | post "/posts/:id" do 29 | op = Post::Update.run(params) do |op| 30 | redirect "/posts/#{op.model.id}" 31 | end 32 | 33 | Post::Cell::Show.(op.model, layout: Gemgem::Cell::Layout).() 34 | end 35 | -------------------------------------------------------------------------------- /config/init.rb: -------------------------------------------------------------------------------- 1 | require "sinatra/sequel" 2 | require "sqlite3" 3 | 4 | configure :development do 5 | set :database, "sqlite://tmp/development.sqlite" 6 | end 7 | 8 | configure :test do 9 | set :database, "sqlite3::memory:" 10 | end 11 | 12 | require_relative "migrations" 13 | 14 | Sequel::Model.strict_param_setting = false 15 | # Dir["models/**/*.rb"].each{|model| 16 | # require model 17 | # } 18 | 19 | require "trailblazer/operation" 20 | require "reform/form/dry" 21 | require "trailblazer/cells" 22 | require "cells-slim" 23 | 24 | Trailblazer::Cell.class_eval do 25 | include Cell::Slim 26 | self.view_paths = ["concepts"] # DISCUSS: is that the right place? 27 | end 28 | 29 | # TODO: use trailblazer-loader. 30 | require_relative "../concepts/post/operation/create.rb" 31 | require_relative "../concepts/post/operation/update.rb" 32 | require_relative "../concepts/post/cell/new.rb" 33 | require_relative "../concepts/post/cell/show.rb" 34 | 35 | require_relative "../gemgem/cell/layout.rb" 36 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Original parts Copyright (c) 2010 Robert Graff. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /spec/models/person_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe Person do 4 | before :each do 5 | Person.delete 6 | end 7 | describe "#rand" do 8 | describe "with no records" do 9 | it "should return nil" do 10 | Person.rand.should be_nil 11 | end 12 | end 13 | describe "with one record" do 14 | before :each do 15 | @person = Person.new(:name => "Maggie").save 16 | end 17 | it "should return the one record" do 18 | Person.rand.should == @person 19 | end 20 | end 21 | describe "with two records" do 22 | before :each do 23 | @person = Person.new(:name => "Maggie").save 24 | @another = Person.new(:name => "Audrey").save 25 | end 26 | it "should return one or the other" do 27 | [@person, @another].should include(Person.rand) 28 | end 29 | end 30 | end 31 | 32 | describe "validations" do 33 | it "should require a name" do 34 | Person.new().should_not be_valid 35 | Person.new(:name => '').should_not be_valid 36 | Person.new(:name => "Maggie").should be_valid 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | bacon (1.2.0) 5 | declarative (0.0.5) 6 | uber (>= 0.0.15) 7 | disposable (0.2.1) 8 | declarative (~> 0.0.4) 9 | representable (>= 2.4.0, <= 3.1.0) 10 | uber 11 | rack (1.6.4) 12 | rack-protection (1.5.3) 13 | rack 14 | reform (2.1.0.rc1) 15 | disposable (>= 0.2.1, < 0.3.0) 16 | representable (>= 2.4.0, < 3.1.0) 17 | uber (~> 0.0.11) 18 | representable (3.0.0) 19 | declarative (~> 0.0.5) 20 | uber (~> 0.0.15) 21 | sequel (4.29.0) 22 | sinatra (1.4.6) 23 | rack (~> 1.4) 24 | rack-protection (~> 1.4) 25 | tilt (>= 1.3, < 3) 26 | sinatra-sequel (0.9.0) 27 | bacon 28 | sequel (>= 3.2.0) 29 | sinatra (>= 0.9.4) 30 | sqlite3 (1.3.11) 31 | tilt (2.0.1) 32 | trailblazer (1.1.0.rc1) 33 | declarative 34 | reform (>= 2.0.0, < 3.0.0) 35 | uber (>= 0.0.15) 36 | uber (0.0.15) 37 | 38 | PLATFORMS 39 | ruby 40 | 41 | DEPENDENCIES 42 | reform (= 2.1.0.rc1) 43 | sinatra 44 | sinatra-sequel 45 | sqlite3 46 | trailblazer (= 1.1.0.rc1) 47 | 48 | BUNDLED WITH 49 | 1.10.6 50 | -------------------------------------------------------------------------------- /gemgem/view/layout.slim: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 |