0015-CodeReview-tropy2.rb

あなたの Ruby コードを添削します 【第 4 回】 Tropy で解説した、添削後のソースコード tropy.rb です。

#
# tropy.rb - Tropy in Ruby, Version 2
#
# Copyright (C) 2005,2006 by Hiroshi Yuki.
# http://www.hyuki.com/tropy/
#
# 2006-07-07 modified by Minero Aoki
#
# You can redistribute and/or modify it under the same terms as Ruby.
#

require 'pstore'
require 'cgi'
require 'erb'
require 'nkf'
require 'stringio'

unless Object.method_defined?(:funcall)
  class Object
    alias funcall __send__
  end
end

class CGI
  def get(key)
    a = params()[key]
    a ? a[0] : nil
  end
end

module Tropy

  class Application
    def initialize(db, manager)
      @db = db
      @screenmanager = manager
    end

    def cgi_main
      screen = handle(CGI.new)
      print screen.http_response
    end

    # FIXME: not implemented yet
    #def fcgi_main
    #end

    def handle(req)
      cmd, *args = parse_request(req)
      funcall("handle_#{cmd}", *args)
    rescue => err
      return error_response(err)
    end

    private

    def parse_request(req)
      # For backward compatibility
      req.params.each_key do |k|
        case k
        when /\A(\d{8})\z/  then return "view", $1
        when /\Ae(\d{8})\z/ then return "edit", $1
        when /\Aw(\d{8})\z/ then return "write", $1, req.params['msg'][0]
        when /\Ac\z/        then return "create"
        end
      end
      if req.params.key?('id')
        case req.get('cmd')
        when 'view', nil then return 'view', req.get('id')
        when 'edit'      then return 'edit', req.get('id')
        when 'save'      then return 'save', req.get('id'), req.get('text')
        when 'create'    then return 'create'
        end
      end
      return "drift"
    end

    def error_response(err)
      ErrorScreen.new(err)
    end

    def handle_view(id)
      page = @db[id]
      return handle_create() unless page
      @screenmanager.view_page_screen(id, page)
    end

    def handle_drift
      id, page = *@db.random
      return handle_create() unless id
      @screenmanager.view_page_screen(id, page)
    end

    def handle_create
      id, page = *@db.create
      @screenmanager.edit_page_screen(id, page)
    end

    def handle_edit(id)
      page = @db[id] || Page.empty
      @screenmanager.edit_page_screen(id, page)
    end

    def handle_save(id, src)
      if src.to_s.strip.empty?
        @db.delete id.to_s
        @screenmanager.redirect
      else
        @db[id] = @db.parse_page(src)
        @screenmanager.redirect_page(id)
      end
    end
  end


  class URLMapper
    def initialize(baseurl)
      @url = baseurl
    end

    attr_reader :url

    def drift
      @url
    end

    def view(id)
      "#{@url}?#{id}"
    end

    def edit(id)
      "#{@url}?e#{id}"
    end

    def create
      "#{@url}?c"
    end

    def write(id)
      "#{@url}?w#{id}"
    end
  end


  class ScreenManager
    def initialize(h)
      @urlmapper = URLMapper.new(h[:baseurl])
      @params = Params.new(@urlmapper,
                           TemplateRepository.new(h[:templatedir]),
                           h[:theme])
    end

    def view_page_screen(id, page)
      ViewPageScreen.new(@params, id, page)
    end

    def edit_page_screen(id, page)
      EditPageScreen.new(@params, id, page)
    end

    def redirect_top
      RedirectScreen.new(@params, @urlmapper.url)
    end

    def redirect_page(id)
      RedirectScreen.new(@params, @urlmapper.view(id))
    end

    class Params
      def initialize(umap, tmpl, theme)
        @urlmapper = umap
        @template_repository = tmpl
        @theme = theme
      end

      attr_reader :urlmapper
      attr_reader :template_repository

      def css_url
        "#{@theme}/style.css"
      end
    end
  end

  class TemplateRepository
    def initialize(prefix)
      @prefix = prefix
    end

    def load(id)
      File.read("#{@prefix}/#{id}")
    end
  end

  class Screen
    def http_response
      body = body()
      out = StringIO.new
      out.puts "Content-Type: #{content_type()}"
      out.puts "Content-Length: #{body.length}"
      out.puts
      out.puts body
      out.string
    end

    private

    def escape_html(s)
      CGI.escapeHTML(s)
    end
  end

  class ErrorScreen < Screen
    def initialize(err)
      @error = err
    end

    def content_type
      'text/html'
    end

    def body
      <<-EndHTML
<html>
<head><title>Error</title></head>
<body>
<h1>Error</h1>
<pre>#{escape_html(@error.message)} (#{escape_html(@error.class.name)})
#{@error.backtrace.map {|s| escape_html(s) }.join("\n")}</pre>
</body>
</html>
      EndHTML
    end
  end

  class TemplateScreen < Screen
    def initialize(params)
      @params = params
      @urlmapper = params.urlmapper
      @template_repository = params.template_repository
    end

    private

    def run_template(id)
      erb = ERB.new(@template_repository.load(id))
      erb.filename = id
      erb.result(binding())
    end

    alias h escape_html
  end

  class PageBoundScreen < TemplateScreen
    def initialize(params, id, page)
      super params
      @id = id
      @page = page
    end

    def content_type
      "text/html; charset=#{@page.encoding}"
    end
  end

  class ViewPageScreen < PageBoundScreen
    def editable?
      true
    end

    def body
      run_template('view')
    end
  end

  class EditPageScreen < PageBoundScreen
    def editable?
      false
    end

    def body
      run_template('edit')
    end
  end

  class RedirectScreen < TemplateScreen
    def initialize(params, desturl)
      super params
      @desturl = desturl
    end

    def content_type
      "text/html"
    end

    def body
      run_template('thanks')
    end
  end


  class Database
    def initialize(path, encoding)
      @pstore = PStore.new(path)
      @encoding = encoding
    end

    def parse_page(src)
      Page.parse(to_local(src), @encoding)
    end

    def to_local(str)
      case @encoding
      when 'shift_jis'
        NKF.nkf("-m0 -s", str)
      when 'euc-jp'
        NKF.nkf("-m0 -e", str)
      else
        raise "unsupported encoding: #{@encoding.inspect}"
      end
    end
    private :to_local

    def ids
      @pstore.transaction(true) {|ps| ps.roots }
    end

    def [](id)
      @pstore.transaction(true) {|store|
        store[id]
      }
    end

    def []=(id, page)
      @pstore.transaction {|store|
        store[id.to_s] = page
      }
    end

    def random
      @pstore.transaction(true) {|store|
        ids = store.roots
        id = ids[rand(ids.size)]
        return id, store[id]
      }
    end

    def create
      @pstore.transaction(true) {|store|
        id = sprintf("%08d", rand(10 ** 8))
        if store.root?(id)
          return id, store[id]
        else
          return id, Page.empty
        end
      }
    end

    def delete(id)
      @pstore.delete(id)
    end
  end

  class Page
    def Page.parse(src, encoding = nil)
      title, *body = src.to_a
      new(title.strip, body.join(''), encoding)
    end

    def Page.empty
      new("New Page", "")
    end

    def initialize(title, body, encoding = nil)
      @title = title
      @body = body
      @encoding = encoding
    end

    attr_reader :title
    attr_reader :body
    attr_reader :encoding

    def source
      "#{@title}\n#{@body}"
    end

    def body_html
      body = CGI.escapeHTML(@body).gsub(/\n/, "<br>")
      "<p>#{body}</p>"
    end
  end

end