├── rails_root ├── .gitignore ├── lib │ ├── request.rb │ ├── response.rb │ ├── entities │ │ └── entity.rb │ ├── interactors │ │ └── interactor.rb │ ├── templates │ │ └── rails │ │ │ └── controller │ │ │ └── controller.rb │ └── generators │ │ ├── entity │ │ └── entity_generator.rb │ │ └── interactor │ │ └── interactor_generator.rb ├── spec │ ├── spec_helper.rb │ └── support │ │ ├── interactor_shared_examples.rb │ │ └── entity_shared_examples.rb └── config │ └── database.example.yml ├── README.md ├── LICENSE └── rails-clean-architecture.rb /rails_root/.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle 2 | /config/database.yml 3 | /log/*.log 4 | /tmp 5 | -------------------------------------------------------------------------------- /rails_root/lib/request.rb: -------------------------------------------------------------------------------- 1 | require 'hashie' 2 | 3 | class Request < Hashie::Mash 4 | end 5 | -------------------------------------------------------------------------------- /rails_root/lib/response.rb: -------------------------------------------------------------------------------- 1 | require 'hashie' 2 | 3 | class Response < Hashie::Mash 4 | end 5 | -------------------------------------------------------------------------------- /rails_root/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'byebug' 2 | require 'support/entity_shared_examples.rb' 3 | require 'support/interactor_shared_examples.rb' 4 | -------------------------------------------------------------------------------- /rails_root/lib/entities/entity.rb: -------------------------------------------------------------------------------- 1 | require 'active_model' 2 | 3 | class Entity 4 | include ActiveModel::Validations 5 | attr_accessor :id 6 | 7 | def initialize(attr = {}) 8 | attr.each do |name, value| 9 | send("#{name}=", value) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /rails_root/spec/support/interactor_shared_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples 'an interactor' do 4 | context "#initialize" do 5 | 6 | it "takes request data" do 7 | interactor = described_class.new(data: 1) 8 | expect(interactor.request).to eql(data: 1) 9 | end 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /rails_root/lib/interactors/interactor.rb: -------------------------------------------------------------------------------- 1 | require 'response' 2 | 3 | class Interactor 4 | attr_reader :request, :response 5 | 6 | def initialize(request = {}) 7 | @request = request 8 | @response = Response.new 9 | @response.interactor = self.class.to_s 10 | end 11 | 12 | def call 13 | @response 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /rails_root/config/database.example.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | host: localhost 4 | pool: 5 5 | username: postgres 6 | password: secret 7 | 8 | development: 9 | <<: *default 10 | database: <%= @app_name %>_development 11 | 12 | test: 13 | <<: *default 14 | database: <%= @app_name %>_test 15 | 16 | production: 17 | <<: *default 18 | database: <%= @app_name %> 19 | -------------------------------------------------------------------------------- /rails_root/spec/support/entity_shared_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'an entity' do 2 | context "#initialize" do 3 | it "can have id assigned" do 4 | entity = described_class.new(id: 1) 5 | expect(entity.id).to eql(1) 6 | end 7 | it "can't have non-accessible attributes assigned" do 8 | expect { described_class.new(non_attrib: true) }.to raise_error(NoMethodError) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /rails_root/lib/templates/rails/controller/controller.rb: -------------------------------------------------------------------------------- 1 | require 'tilt/handlebars' 2 | 3 | <% if namespaced? -%> 4 | require_dependency "<%= namespaced_path %>/application_controller" 5 | 6 | <% end -%> 7 | <% module_namespacing do -%> 8 | class <%= class_name %>Controller < ApplicationController 9 | <% actions.each do |action| -%> 10 | def <%= action %> 11 | template = Tilt.new('app/views/<%= file_name %>/<%= action %>.html.hbs') 12 | render text: template.render 13 | end 14 | <%= "\n" unless action == actions.last -%> 15 | <% end -%> 16 | end 17 | <% end -%> 18 | -------------------------------------------------------------------------------- /rails_root/lib/generators/entity/entity_generator.rb: -------------------------------------------------------------------------------- 1 | class EntityGenerator < Rails::Generators::NamedBase 2 | desc "This generator creates an entity file in lib/entities" 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def create_entity_file 6 | create_file "lib/entities/#{file_name}.rb", <<-FILE 7 | class #{class_name} < Entity 8 | end 9 | FILE 10 | 11 | create_file "spec/entities/#{file_name}_spec.rb", <<-FILE 12 | require 'entities/entity.rb' 13 | require 'entities/#{file_name}.rb' 14 | require 'spec_helper.rb' 15 | 16 | describe #{class_name} do 17 | 18 | it_behaves_like 'an entity' 19 | 20 | end 21 | FILE 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rails Clean Architecture 2 | ======================== 3 | 4 | [![Join the chat at https://gitter.im/reedlaw/rails-clean-architecture](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/reedlaw/rails-clean-architecture?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | *[Clean Architecture](http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) template for Rails apps* 7 | 8 | Instructions 9 | ------------ 10 | 11 | ``` 12 | git clone git@github.com:reedlaw/rails-clean-architecture.git 13 | rails new APP_NAME --skip-action-view --skip-sprockets --skip-spring --skip-javascript --skip-test-unit --skip-bundle -m rails-clean-architecture/rails-clean-architecture.rb 14 | ``` 15 | -------------------------------------------------------------------------------- /rails_root/lib/generators/interactor/interactor_generator.rb: -------------------------------------------------------------------------------- 1 | class InteractorGenerator < Rails::Generators::NamedBase 2 | desc "This generator creates an interactor file in lib/interactors" 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def create_interactor_file 6 | create_file "lib/interactors/#{file_name}.rb", <<-FILE 7 | class #{class_name} < Interactor 8 | def call 9 | raise UnauthorizedAccess if not authorized? 10 | super 11 | end 12 | 13 | def authorized? 14 | # TODO: define method for permission to call this interactor 15 | end 16 | end 17 | FILE 18 | 19 | create_file "spec/interactors/#{file_name}_spec.rb", <<-FILE 20 | require 'interactors/interactor.rb' 21 | require 'interactors/#{file_name}.rb' 22 | require 'spec_helper.rb' 23 | 24 | describe #{class_name} do 25 | 26 | it_behaves_like 'an interactor' 27 | 28 | end 29 | FILE 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Reed G. Law 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /rails-clean-architecture.rb: -------------------------------------------------------------------------------- 1 | gem 'hashie' 2 | gem 'pg' 3 | gem 'tilt' 4 | gem 'tilt-handlebars' 5 | gem 'unicorn' 6 | 7 | gem_group :development, :test do 8 | gem 'byebug' 9 | gem 'rspec-rails' 10 | end 11 | 12 | gsub_file 'Gemfile', /^gem\s+["']sqlite3["'].*\n/, '' 13 | gsub_file 'Gemfile', /^gem\s+["']jbuilder["'].*\n/, '' 14 | gsub_file 'Gemfile', /^gem\s+["']sdoc["'].*\n/, '' 15 | gsub_file 'Gemfile', /^#.*\n/, '' 16 | gsub_file 'Gemfile', /^$\n/, '' 17 | gsub_file 'Gemfile', /"/, '\'' 18 | 19 | environment <<-CODE 20 | config.generators do |g| 21 | g.orm :active_record 22 | g.stylesheets false 23 | g.javascripts false 24 | g.controller :clean_controller 25 | g.view_specs false 26 | g.helper_specs false 27 | end 28 | CODE 29 | 30 | # Remove assets/views 31 | run 'rm -r app/assets app/models app/views lib/assets' 32 | 33 | def source_paths 34 | Array(super) + 35 | [File.join(File.expand_path(File.dirname(__FILE__)),'rails_root')] 36 | end 37 | 38 | remove_file '.gitignore' 39 | copy_file '.gitignore' 40 | 41 | inside 'config' do 42 | remove_file 'database.yml' 43 | template 'database.example.yml' 44 | run "ln -s database.example.yml database.yml" 45 | end 46 | 47 | inside 'lib' do 48 | inside 'entities' do 49 | copy_file 'entity.rb' 50 | end 51 | inside 'generators' do 52 | inside 'entity' do 53 | copy_file 'entity_generator.rb' 54 | end 55 | inside 'interactor' do 56 | copy_file 'interactor_generator.rb' 57 | end 58 | end 59 | inside 'interactors' do 60 | copy_file 'interactor.rb' 61 | end 62 | copy_file 'request.rb' 63 | copy_file 'response.rb' 64 | inside 'templates' do 65 | inside 'rails' do 66 | inside 'controller' do 67 | copy_file 'controller.rb' 68 | end 69 | end 70 | end 71 | end 72 | 73 | inside 'spec' do 74 | copy_file 'spec_helper.rb' 75 | inside 'support' do 76 | copy_file 'entity_shared_examples.rb' 77 | copy_file 'interactor_shared_examples.rb' 78 | end 79 | end 80 | 81 | puts 82 | puts 'Clean Architecture Rails app generated!' 83 | puts 84 | puts 'Now you should:' 85 | puts 86 | puts '1. cd ' + @app_name 87 | puts '2. bundle' 88 | puts '3. Check config/database.yml' 89 | puts '4. Generate entities with "rails generate entity *name*"' 90 | puts '7. Generate interactors with "rails generate interactor *name*"' 91 | puts '8. Add tests and production code and test with rspec' 92 | puts '9. Add routes, controller actions, and handlebars templates' 93 | --------------------------------------------------------------------------------