Ch1 Zero to “It Works!”

Gem的檔案結構

$ bundle gem rulers
    create rulers/Gemfile
    create rulers/Rakefile
    create rulers/LICENSE.txt
    create rulers/README.md
    create rulers/.gitignore
    create rulers/rulers.gemspec # !!
    create rulers/lib/rulers.rb  # !!
    create rulers/lib/rulers/version.rb
Initializating git repo in src/rulers

rulers/rulers.gemspec放的是gem的資訊與dependency

rulers/lib/rulers.rb就是主程式

dependency分成development與runtime

# rulers.gemspec

gem.add_development_dependency "rspec"
gem.add_runtime_dependency "rest-client"

rack進入點 與 rack app回傳值的資料結構

# best_quotes/config.ru
run proc {
    [200, {'Content-Type' => 'text/html'}, ["Hello, world!"]]
}

狀態值,header,資料

rack app 的 interface

# rulers/lib/rulers.rb
require "rulers/version"
module Rulers
    class Application
        def call(env)
            [200, {'Content-Type' => 'text/html'}, ["Hello from Ruby on Rulers!"]]
        end
    end
end

要有call的method,吃的是來自rack的env

new是編譯期的args,call是runtime的args

# best_quotes/config.ru
require './config/application'
run BestQuotes::Application.new

Ch2 Your First Controller

# rulers/lib/rulers.rb
require "rulers/version"
require "rulers/routing"
module Rulers
class Application
   def call(env)
     klass, act = get_controller_and_action(env)
     controller = klass.new(env)
     text = controller.send(act)
     [200, {'Content-Type' => 'text/html'}, [text]]
   end
    def get_controller_and_action(env)
     _, cont, action, after = env["PATH_INFO"].split('/', 4)
     cont = cont.capitalize # "People"
     cont += "Controller" # "PeopleController"
     [Object.const_get(cont), action]
   end
end
class Controller
   def initialize(env)
     @env = env
   end
   def env
     @env
   end
end
end

觀察

  • get_controller_and_action :: env -> (Class, Symbol)
  • Controller :: env -> obj(一堆action)

Ch3 Rails Automatic Loading

const_missing

class Object
    def self.const_missing(c)
    require "./bobo"
    Bobo
    end
end
Bobo.new.print_bobo

CamelCase and snake_case

# rulers/lib/rulers/util.rb
module Rulers
    def self.to_underscore(string)
        string.gsub(/::/, '/').
        gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
        gsub(/([a-z\d])([A-Z])/,'\1_\2').
        tr("-", "_").
        downcase
    end
end

auto-load

# rulers/lib/rulers/dependencies.rb
class Object
    def self.const_missing(c)
        require Rulers.to_underscore(c.to_s)
        Object.const_get(c)
    end
end

# best_quotes/config/application.rb
require "rulers"
$LOAD_PATH << File.join(File.dirname(__FILE__),"..", "app","controllers") #require要的
# --> No more require here! <--
module BestQuotes
    class Application < Rulers::Application
    end
end

Ch4 Rendering Views

erb

# some_directory/erb_test.rb
require "erubis"
template = <<TEMPLATE
    Hello! This is a template.
    It has <%= whatever %>.
TEMPLATE

eruby = Erubis::Eruby.new(template)
puts eruby.src
puts "=========="
puts eruby.result(:whatever => "ponies!")

_buf = '';
_buf << 'Hello!   This is a template. It has ';
_buf << ( whatever ).to_s; _buf << '.';
_buf.to_s

==========

Hello! This is a template.
It has ponies!.

render

# rulers/lib/rulers/controller.rb (excerpt)
def render(view_name, locals = {})
    filename = File.join "app", "views","#{view_name}.html.erb"
    template = File.read filename
    eruby = Erubis::Eruby.new(template)
    eruby.result locals.merge(:env => env)
end

locals是controller的資料 env是來自rack的資料

觀察

  • parse -> proc having lots of string -> eval -> string

Ch5 Basic Models

skip

Ch6 Request, Response

Request

module Rulers
    class Controller
        def request
            @request ||= Rack::Request.new(@env)
        end
        def params
            request.params
        end
    end
end

env 得到 Request 再從中得到 params

Response

module Rulers
    class Controller
        #private
        def response(text, status = 200, headers = {})
            raise "Already responded!" if @response
            a = [text].flatten
            @response = Rack::Response.new(a, status, headers)
        end
        def render(view_name, locals = {})
            filename = File.join "app", "views","#{view_name}.html.erb"
            template = File.read filename
            eruby = Erubis::Eruby.new(template)
            eruby.result locals.merge(:env => env)
        end
        #public
        def get_response # Only for Rulers
            @response
        end
        def render_response(*args)
            response(render(*args))
        end
    end
end

response產生three tuple,就是rack app真正要回傳的東西

Ch7 The Littlest ORM

Migration 之後,DB做事之前

# best_quotes/mini_migration.rb
require "sqlite3"
conn = SQLite3::Database.new "test.db"
conn.execute <<SQL
create table my_table (
 id INTEGER PRIMARY KEY,
 posted INTEGER,
 title VARCHAR(30),
 body VARCHAR(32000));
SQL

Model

# rulers/lib/rulers/sqlite_model.rb
require "sqlite3"
require "rulers/util"
DB = SQLite3::Database.new "test.db"
module Rulers
    module Model
        class SQLite
            def initialize(data = nil)
                @hash = data
            end
            # table's meta data
            def self.table
                Rulers.to_underscore name
            end
            def self.schema
                return @schema if @schema
                @schema = {}
                DB.table_info(table) do |row|
                    @schema[row["name"]] = row["type"]
                end
                @schema
            end
            # ruby val -> sql's ds in ruby string
            def self.to_sql(val)
                case val
                    when Numeric
                        val.to_s
                    when String
                        "'#{val}'"
                    else
                        raise "Can't change #{val.class} to SQL!"
                end
            end
            # 產生新model for a row
            def self.create(values)
                values.delete "id"
                keys = schema.keys - ["id"]
                vals = keys.map do |key|
                    values[key] ? to_sql(values[key]) : "null"
                end
                DB.execute <<SQL
                    INSERT INTO #{table} (#{keys.join ","})
                    VALUES (#{vals.join ","});
                SQL
                data = Hash[keys.zip vals]
                sql = "SELECT last_insert_rowid();"
                data["id"] = DB.execute(sql)[0][0]
                self.new data
            end
            def self.find(id)
                row = DB.execute <<SQL
                    select #{schema.keys.join ","} from #{table}
                    where id = #{id};
                SQL
                data = Hash[schema.keys.zip row[0]]
                self.new data
            end
            # operate on DB directly
            def self.count
                DB.execute(<<SQL)[0][0]
                    SELECT COUNT(*) FROM #{table}
                SQL
            end
            def save!
                unless @hash["id"]
                    self.class.create
                    return true
                end
                fields = @hash.map do |k, v|
                "#{k}=#{self.class.to_sql(v)}"
                end.join ","
                DB.execute <<SQL
                    UPDATE #{self.class.table}
                    SET #{fields}
                    WHERE id = #{@hash["id"]}
                SQL
                true
            end
            def save
                self.save! rescue false
            end
            # operate on model's hash (no DB business)
            def [](name)
                @hash[name.to_s]
            end
            def []=(name, value)
                @hash[name.to_s] = value
            end
        end
    end
end

method_missing & define_method

Model的accessor!!

class MyClass
    ["foo", "bar"].each do |method|
        define_method(method) {
            puts "Obj #{method}"
        }
    end
end
myobj = MyClass.new
myobj.foo
myobj.bar

When you call a method that doesnʼt exist on an object, its method called “method_missing” will be called to tell it so. If you override method_missing to check for column names, you can instead just return the column value from method_missing, which will return it from the original call.

Ch8 Rack Middleware

rack app的interface(type signature) & 如何疊app

# sample_dir/config.ru
class Canadianize
    def initialize(app, arg = "")
        @app = app # next rack app
        @arg = arg # args
    end
    def call(env) # env -> [ status, headers, content ]
        status, headers, content = @app.call(env)
        content[0] += @arg + ", eh?" 
        [ status, headers, content ]
    end
end

use Canadianize, ", simple"
run proc {
    [200, {'Content-Type' => 'text/html'}, ["Hello, world"]]
}

map

# sample_dir/config.ru
require "rack/lobster"
use Rack::ContentType
map "/lobster" do
    use Rack::ShowExceptions
    run Rack::Lobster.new
end
map "/lobster/but_not" do
    run proc {
        [200, {}, ["Really not a lobster"]]
    }
end
run proc {
    [200, {}, ["Not a lobster"]]
}

If thereʼs could be two that match, the longer path is checked first.

Ch9 Real Routing

Controller Actions are Rack Apps

Before

module Rulers
 class Application
    def call(env)
        if env['PATH_INFO'] == '/favicon.ico'
            return [404, {'Content-Type' => 'text/html'}, []]
        end
        klass, act = get_controller_and_action(env)

        controller = klass.new(env) # get env from the previous line, so this line should be eliminated

        text = controller.send(act) # should not expose how to execute an action
        if controller.get_response # this is controller(rack app)'s responsibility
            st, hd, rs = controller.get_response.to_a
            [st, hd, [rs.body].flatten]
        else
            [200, {'Content-Type' => 'text/html'}, [text]]
        end
        # rack_app = klass.action(act)

        # rack_app.call(env)
    end
 end
end

After

# rulers/lib/rulers.rb
module Rulers
 class Application
    def call(env)
        if env['PATH_INFO'] == '/favicon.ico'
            return [404, {'Content-Type' => 'text/html'}, []]
        end
        klass, act = get_controller_and_action(env)
        rack_app = klass.action(act)
        rack_app.call(env)
    end
 end
end

# rulers/lib/rulers/controller.rb
module Rulers
    class Controller
        include Rulers::Model
        def initialize(env)
            @env = env
            @routing_params = {} # Add this line!
        end
        def dispatch(action, routing_params = {})
            @routing_params = routing_params
            text = self.send(action)
            if get_response
                st, hd, rs = get_response.to_a
                [st, hd, [rs].flatten]
            else
                [200, {'Content-Type' => 'text/html'}, [text].flatten]
            end
        end
        def self.action(act, rp = {})
            proc { |e| self.new(e).dispatch(act, rp) }
        end
        def params
            request.params.merge @routing_params
        end
    end
end

Route 也就是 match

Usuge

# best_quotes/config.ru
require "./config/application"
app = BestQuotes::Application.new
use Rack::ContentType
app.route do
    match "", "quotes#index"
    match "sub-app", proc { [200, {}, ["Hello, sub-app!"]] }
    # default routes
    match ":controller/:id/:action"
    match ":controller/:id", :default => { "action" => "show" }
    match ":controller", :default => { "action" => "index" }
end
run app

match & routeObject

# rulers/lib/rulers/routing.rb
class RouteObject
    def initialize
        @rules = []
    end
    def match(url, *args)
        options = {}
        options = args.pop if args[-1].is_a?(Hash)
        options[:default] ||= {}

        dest = nil
        dest = args.pop if args.size > 0
        raise "Too many args!" if args.size > 0
        parts = url.split("/")
        parts.select! { |p| !p.empty? }

        vars = []
        regexp_parts = parts.map do |part|
            if part[0] == ":"
                vars << part[1..-1]
                "([a-zA-Z0-9]+)"
            elsif part[0] == "*"
                vars << part[1..-1]
                "(.*)"
            else
                part
            end
        end
        regexp = regexp_parts.join("/")
        @rules.push({
            :regexp => Regexp.new("^/#{regexp}$"),
            :vars => vars,
            :dest => dest,
            :options => options,
        })
    end
    def check_url(url)
        @rules.each do |r|
            m = r[:regexp].match(url)
            if m
                options = r[:options]
                params = options[:default].dup
                r[:vars].each_with_index do |v, i|
                params[v] = m.captures[i]
            end
                dest = nil
                if r[:dest]
                    return get_dest(r[:dest], params)
                else
                    controller = params["controller"]
                    action = params["action"]
                    return get_dest("#{controller}" +
                    "##{action}", params)
                end
            end
        end
        nil
    end
    def get_dest(dest, routing_params = {})
        return dest if dest.respond_to?(:call)
        if dest =~ /^([^#]+)#([^#]+)$/
            name = $1.capitalize
            cont = Object.const_get("#{name}Controller")
            return cont.action($2, routing_params)
        end
        raise "No destination: #{dest.inspect}!"
    end
end
# And now, the Application:
module Rulers
 class Application
    def route(&block)
        @route_obj ||= RouteObject.new
        @route_obj.instance_eval(&block)
    end
    def get_rack_app(env)
        raise "No routes!" unless @route_obj
        @route_obj.check_url env["PATH_INFO"]
    end
 end
end

Conclusion

from env to res

env -> router -> Controller -----------------------> (Real)Controller -> res
                    / \               / \
                     |                 |
                    env       req -> parmas
                              / \     / \
                               |       |
                              env    router
                                      / \
                                       |
                                      env

基本上這本書的九個把Rails的核心精神都帶到了,這裡是縮減版的rails與上面的很像,不過某些部分多了一些包裝。

Appendix: Unobtrusive JavaScript

JavaScript 不再需要與 HTML 混在一起

原本是

<input type="text" name="date" onchange="validateDate()"/>

變成

<input type="text" name="date" id="date" />
window.addEventListener("DOMContentLoaded"functionevent{
    document.getElementById('date').addEventListener("change"validateDate);
});

先找出要的DOM,之後再JS中設定該DOM。

找出要的DOM,可透過id或class,甚至是DOM的traversal

原本但是找到之後,要怎麼儲存這個DOM的狀態??

原本都是在JS中來保存,但這樣就會變成DOM的狀態與程式邏輯的狀態會混在一起。

所以之後有了data attributes!!