Report abuse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
require 'net/http'
require 'rack'
require 'rack/cache'

module Nanoc3::Extra

  # CHiCk is a caching HTTP client that uses Rack::Cache.
  module CHiCk

    # CHiCk::Client provides a simple API for issuing HTTP requests.
    class Client

      DEFAULT_OPTIONS = {
        :cache => {
          :metastore   => 'file:tmp/rack/cache.meta', 
          :entitystore => 'file:tmp/rack/cache.body'
        },
        :cache_controller => {
          :max_age => 60
        }
      }

      def initialize(options={})
        # Get options
        @options = DEFAULT_OPTIONS.merge(options)
        @options[:cache] = DEFAULT_OPTIONS[:cache].merge(@options[:cache])
      end

      def get(url, additional_headers={})
        # FIXME use additional headers or remove support for additional headers
        # Build app
        options = @options
        @app ||= Rack::Builder.new {
          use Rack::Cache, options[:cache]
          use Nanoc3::Extra::CHiCk::CacheController, options[:cache_controller]
          run Nanoc3::Extra::CHiCk::RackClient
        }

        # Build environment for request
        env = Rack::MockRequest.env_for(url, :method => 'GET')

        # Fetch
        status, headers, body_parts = @app.call(env)
        body = ''
        body_parts.each { |part| body << part }
        [ status, headers, body ]
      end

    end

    # CHiCk::CacheController sets the Cache-Control header (and more
    # specifically, max-age) to limit the number of necessary requests.
    class CacheController

      DEFAULT_OPTIONS = {
        :max_age => 10 # maximum age in seconds
      }

      def initialize(app, options={})
        @app = app
        @options = DEFAULT_OPTIONS.merge(options)
      end

      def call(env)
        res = @app.call(env)
        unless res[1].has_key?('Cache-Control') || res[1].has_key?('Expires')
          res[1]['Cache-Control'] = "max-age=#{@options[:max_age]}"
        end
        res
      end

    end

    # CHiCk::RackClient performs the actual HTTP requests. It does not perform
    # any caching.
    class RackClient

      METHOD_TO_CLASS_MAPPING = {
        'DELETE'  => Net::HTTP::Delete,
        'GET'     => Net::HTTP::Get,
        'HEAD'    => Net::HTTP::Head,
        'POST'    => Net::HTTP::Post,
        'PUT'     => Net::HTTP::Put
      }

      def self.call(env)
        # Build request
        request = Rack::Request.new(env)

        # Build headers and strip HTTP_
        request_headers = env.inject({}) do |m,(k,v)|
          k =~ /^HTTP_(.*)$/ ? m.merge($1.gsub(/_/, '-') => v) : m
        end

        # Build Net::HTTP request
        http = Net::HTTP.new(request.host, request.port)
        net_http_request_class = METHOD_TO_CLASS_MAPPING[request.request_method]
        raise ArgumentError, "Unsupported method: #{request.request_method}" if net_http_request_class.nil?
        net_http_request = net_http_request_class.new(request.fullpath, request_headers)
        net_http_request.body = env['rack.input'].read if [ 'POST', 'PUT' ].include?(request.request_method)

        # Perform request
        http.request(net_http_request) do |response|
          # Build Rack response triplet
          return [
            response.code.to_i,
            response.to_hash.inject({}) { |m,(k,v)| m.merge(k => v[0]) },
            [ response.body ]
          ]
        end
      end

    end

  end

end