├── db ├── app.db └── initial_data.sql ├── app ├── views │ └── home │ │ └── index.erb ├── models │ └── user.rb └── controllers │ ├── application_controller.rb │ ├── home_controller.rb │ └── echo_controller.rb ├── Gemfile ├── config.ru ├── Rakefile ├── test ├── test_helper.rb ├── connection_adapter_test.rb ├── application_test.rb ├── user_test.rb ├── filters_test.rb └── rendering_test.rb ├── lib ├── boot.rb ├── action_controller.rb ├── filters.rb ├── connection_adapter.rb ├── active_record.rb ├── application.rb └── rendering.rb ├── super_module.rb └── Gemfile.lock /db/app.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/owningrails/patterns/HEAD/db/app.db -------------------------------------------------------------------------------- /app/views/home/index.erb: -------------------------------------------------------------------------------- 1 |

Hello from a view!

2 |

@message = <%= @message %>

-------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | validates :name, :presence => true 3 | end -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | 3 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'activemodel', '4.2.4' 4 | gem 'activesupport', '4.2.4' 5 | gem 'rack' 6 | gem 'rake' 7 | gem 'sqlite3' 8 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | def index 3 | render :index # renders app/views/home/index.erb 4 | end 5 | end -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # Start with: shotgun 2 | # Under Windows: rackup (CTRL+C and restart on each change) 3 | 4 | require ::File.expand_path("../lib/boot", __FILE__) 5 | require "application" 6 | 7 | run Application.new -------------------------------------------------------------------------------- /app/controllers/echo_controller.rb: -------------------------------------------------------------------------------- 1 | class EchoController < ApplicationController 2 | def index 3 | # response.write "You said: " + request.params["text"] 4 | render text: "You said: " + params["text"] 5 | end 6 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << 'lib' 5 | t.libs << 'test' 6 | t.pattern = 'test/**/*_test.rb' 7 | t.verbose = true 8 | end 9 | 10 | task :default => :test -------------------------------------------------------------------------------- /db/initial_data.sql: -------------------------------------------------------------------------------- 1 | create table users ( 2 | id int, 3 | name varchar(30) 4 | ); 5 | 6 | insert into users values (1, "Marc"); 7 | insert into users values (2, "Greg"); 8 | insert into users values (3, "Dan"); 9 | insert into users values (4, "Dave"); 10 | insert into users values (5, "Bob"); -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative "../lib/boot" 2 | 3 | # Load ActiveSupport testing stuff 4 | require 'active_support' 5 | require 'active_support/testing/autorun' 6 | require 'active_support/test_case' 7 | 8 | # Remove warning from ActiveSupport 9 | I18n.enforce_available_locales = true 10 | 11 | ActiveSupport.test_order = :random 12 | -------------------------------------------------------------------------------- /lib/boot.rb: -------------------------------------------------------------------------------- 1 | # Boot the framework. Similar to Rails' `config/boot.rb`. 2 | 3 | # Activate Bundler. Set up gems listed in the Gemfile. 4 | require 'bundler/setup' 5 | 6 | # Add our framework code (lib/) to the load path. 7 | $LOAD_PATH.unshift "lib" 8 | 9 | # Add all directories of app/ to the load path. 10 | Dir["app/*"].each do |path| 11 | $LOAD_PATH << path 12 | end 13 | -------------------------------------------------------------------------------- /super_module.rb: -------------------------------------------------------------------------------- 1 | class Parent 2 | def say 3 | puts "In Parent" 4 | end 5 | end 6 | 7 | module A 8 | def say 9 | puts "In A" 10 | super 11 | end 12 | end 13 | 14 | module B 15 | def say 16 | puts "In B" 17 | super 18 | end 19 | end 20 | 21 | # class Parent 22 | # prepend A, B 23 | # end 24 | 25 | class Child < Parent 26 | include A, B 27 | end 28 | 29 | # puts Child.ancestors 30 | Child.new.say -------------------------------------------------------------------------------- /test/connection_adapter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "connection_adapter" 3 | 4 | class ConnectionAdapterTest < ActiveSupport::TestCase 5 | def setup 6 | @adapter = SqliteAdapter.new 7 | end 8 | 9 | def test_execute 10 | row = @adapter.execute("SELECT * FROM users").first 11 | assert_equal({ id: 1, name: "Marc", 0 => 1, 1 => "Marc" }, row) 12 | end 13 | 14 | def test_columns 15 | assert_equal [:id, :name], @adapter.columns("users") 16 | end 17 | end -------------------------------------------------------------------------------- /lib/action_controller.rb: -------------------------------------------------------------------------------- 1 | require "filters" 2 | require "rendering" 3 | 4 | module ActionController 5 | class Metal 6 | attr_accessor :request, :response 7 | 8 | def process(action) 9 | # action == "index" 10 | # `send "index"` same as `index` 11 | send action # calls index 12 | end 13 | 14 | def params 15 | request.params 16 | end 17 | end 18 | 19 | # class Child < Parent 20 | class Base < Metal 21 | include Filters 22 | include Rendering 23 | end 24 | end -------------------------------------------------------------------------------- /lib/filters.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/concern' 2 | 3 | module Filters 4 | extend ActiveSupport::Concern 5 | 6 | class_methods do 7 | def before_action(method) 8 | before_actions << method 9 | end 10 | 11 | def before_actions 12 | @before_actions ||= [] 13 | end 14 | 15 | def after_action(method) 16 | after_actions << method 17 | end 18 | 19 | def after_actions 20 | @after_actions ||= [] 21 | end 22 | end 23 | 24 | def process(action) 25 | self.class.before_actions.each { |method| send method } 26 | super 27 | self.class.after_actions.each { |method| send method } 28 | end 29 | end -------------------------------------------------------------------------------- /test/application_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "application" 3 | 4 | class ApplicationTest < ActiveSupport::TestCase 5 | def setup 6 | @app = Application.new 7 | end 8 | 9 | def test_routing 10 | assert_equal ["home", "index"], @app.route("/home/index") 11 | assert_equal ["home", "index"], @app.route("/home") 12 | assert_equal ["home", "index"], @app.route("/") 13 | assert_equal ["users", "index"], @app.route("/users") 14 | assert_equal ["users", "show"], @app.route("/users/show") 15 | end 16 | 17 | def test_load_controller_class 18 | klass = @app.load_controller_class("home") 19 | assert_equal HomeController, klass 20 | end 21 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activemodel (4.2.4) 5 | activesupport (= 4.2.4) 6 | builder (~> 3.1) 7 | activesupport (4.2.4) 8 | i18n (~> 0.7) 9 | json (~> 1.7, >= 1.7.7) 10 | minitest (~> 5.1) 11 | thread_safe (~> 0.3, >= 0.3.4) 12 | tzinfo (~> 1.1) 13 | builder (3.2.2) 14 | i18n (0.7.0) 15 | json (1.8.3) 16 | minitest (5.8.2) 17 | rack (1.6.4) 18 | rake (10.4.2) 19 | sqlite3 (1.3.11) 20 | thread_safe (0.3.5) 21 | tzinfo (1.2.2) 22 | thread_safe (~> 0.1) 23 | 24 | PLATFORMS 25 | ruby 26 | 27 | DEPENDENCIES 28 | activemodel (= 4.2.4) 29 | activesupport (= 4.2.4) 30 | rack 31 | rake 32 | sqlite3 33 | -------------------------------------------------------------------------------- /lib/connection_adapter.rb: -------------------------------------------------------------------------------- 1 | class SqliteAdapter 2 | def initialize 3 | require "sqlite3" 4 | @db = SQLite3::Database.new(File.dirname(__FILE__) + "/../db/app.db", results_as_hash: true) 5 | end 6 | 7 | # Execute an SQL query and return the results as a hash, eg.: { id: 1, name: "Marc" }. 8 | def execute(sql) 9 | @db.execute(sql).each do |row| 10 | row.keys.each { |key| row[(key.to_sym rescue key) || key] = row.delete(key) } # Symbolize keys 11 | end 12 | end 13 | 14 | # Return the column names of a table. 15 | def columns(table_name) 16 | @db.table_info(table_name).map { |info| info["name"].to_sym } 17 | end 18 | end 19 | 20 | class MysqlAdapter 21 | def execute(sql) 22 | # Here we'd implement executing the query in MySQL. 23 | end 24 | end -------------------------------------------------------------------------------- /test/user_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "active_record" 3 | require "user" 4 | 5 | class UserTest < ActiveSupport::TestCase 6 | def test_initialize_with_attributes 7 | user = User.new(id: 1, name: "Marc") 8 | assert_equal 1, user.id 9 | assert_equal "Marc", user.name 10 | end 11 | 12 | def test_find 13 | user = User.find(1) 14 | assert_kind_of User, user 15 | assert_equal 1, user.id 16 | end 17 | 18 | def test_all 19 | user = User.all.first 20 | assert_kind_of User, user 21 | assert_equal 1, user.id 22 | end 23 | 24 | def test_table_name 25 | assert_equal "users", User.table_name 26 | end 27 | 28 | def test_valid 29 | user = User.new 30 | assert ! user.valid? 31 | assert_equal ["can't be blank"], user.errors[:name] 32 | end 33 | end -------------------------------------------------------------------------------- /test/filters_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "action_controller" 3 | require "application_controller" 4 | 5 | class FiltersTestController < ApplicationController 6 | before_action :before 7 | after_action :after 8 | 9 | def initialize(out) 10 | @out = out 11 | end 12 | 13 | def before 14 | @out << :before 15 | end 16 | 17 | def after 18 | @out << :after 19 | end 20 | 21 | def index 22 | @out << :index 23 | end 24 | end 25 | 26 | class FiltersTest < ActiveSupport::TestCase 27 | def test_filters 28 | out = [] 29 | FiltersTestController.new(out).process(:index) 30 | 31 | # assert_equal [:before, 32 | # :index], out 33 | 34 | # With after_action 35 | assert_equal [:before, 36 | :index, 37 | :after], out 38 | end 39 | end -------------------------------------------------------------------------------- /test/rendering_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "action_controller" 3 | require "application_controller" 4 | require "home_controller" 5 | 6 | class RenderingTest < ActiveSupport::TestCase 7 | def setup 8 | @controller = HomeController.new 9 | end 10 | 11 | def test_template_path 12 | assert_equal "app/views/home/index.erb", @controller.template_path("index") 13 | end 14 | 15 | def test_controller_name 16 | assert_equal "home", @controller.controller_name 17 | end 18 | 19 | def test_render_text 20 | assert_equal "some text", @controller.render_to_string(text: "some text") 21 | end 22 | 23 | def test_render_action_as_hash 24 | assert_match /^

/, @controller.render_to_string(action: :index) 25 | end 26 | 27 | def test_render_action_as_symbol 28 | assert_match /^

/, @controller.render_to_string(:index) 29 | end 30 | end -------------------------------------------------------------------------------- /lib/active_record.rb: -------------------------------------------------------------------------------- 1 | require "connection_adapter" 2 | require "active_model" 3 | 4 | module ActiveRecord 5 | class Base 6 | include ActiveModel::Validations 7 | 8 | @@connection = SqliteAdapter.new 9 | 10 | def initialize(attributes={}) 11 | @attributes = attributes 12 | end 13 | 14 | def method_missing(name, *args) 15 | if @@connection.columns(self.class.table_name).include?(name) 16 | @attributes[name] 17 | else 18 | super 19 | end 20 | end 21 | 22 | def self.find(id) 23 | find_by_sql("SELECT * FROM #{table_name} WHERE id = #{id.to_i} LIMIT 1").first 24 | end 25 | 26 | def self.all 27 | find_by_sql("SELECT * FROM #{table_name}") 28 | end 29 | 30 | def self.find_by_sql(sql) 31 | @@connection.execute(sql).map do |attributes| 32 | new(attributes) 33 | end 34 | end 35 | 36 | def self.table_name 37 | name.downcase + "s" # => "users" 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /lib/application.rb: -------------------------------------------------------------------------------- 1 | require "action_controller" 2 | require "application_controller" 3 | 4 | class Application 5 | def call(env) 6 | request = Rack::Request.new(env) 7 | response = Rack::Response.new 8 | 9 | controller_name, action_name = route(request.path_info) 10 | 11 | controller_class = load_controller_class(controller_name) # HomeController 12 | controller = controller_class.new 13 | controller.request = request 14 | controller.response = response 15 | controller.process(action_name) 16 | 17 | response.finish 18 | end 19 | 20 | def route(path) 21 | # path = "/home/index" => ["home", "index"] 22 | _, controller, action = path.split("/") # => ["", "home", "index"] 23 | [controller || "home", action || "index"] 24 | end 25 | 26 | def load_controller_class(name) 27 | # name => "home" => "HomeController" => HomeController 28 | require "#{name}_controller" 29 | Object.const_get name.capitalize + "Controller" # HomeController 30 | end 31 | end -------------------------------------------------------------------------------- /lib/rendering.rb: -------------------------------------------------------------------------------- 1 | require "erb" 2 | # To render an ERB template: 3 | # 4 | # ERB.new(File.read(erb_file_path)).result(binding) 5 | 6 | module Rendering 7 | def render(action_or_options) 8 | response.write render_to_string(action_or_options) 9 | end 10 | 11 | def render_to_string(action_or_options) 12 | if action_or_options.is_a? Symbol 13 | options = { action: action_or_options } 14 | else 15 | options = action_or_options 16 | end 17 | 18 | if options[:text] 19 | options[:text] 20 | elsif options[:action] 21 | render_template template_path(options[:action]) 22 | else 23 | raise ArgumentError, "Nothing to render" 24 | end 25 | end 26 | 27 | def template_path(action) 28 | "app/views/#{controller_name}/#{action}.erb" 29 | end 30 | 31 | def controller_name 32 | self.class.name[/^(\w+)Controller/, 1].downcase # HomeController => "home" 33 | end 34 | 35 | private 36 | def render_template(path) 37 | ERB.new(File.read(path)).result(binding) 38 | end 39 | end --------------------------------------------------------------------------------