racknga 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/AUTHORS ADDED
@@ -0,0 +1 @@
1
+ Kouhei Sutou <kou@clear-code.com>
data/NEWS.ja.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = お知らせ
2
+
3
+ == 0.9.0: 2010-07-04
4
+
5
+ * 最初のリリース!
data/NEWS.rdoc ADDED
@@ -0,0 +1,5 @@
1
+ = NEWS
2
+
3
+ == 0.9.0: 2010-07-04
4
+
5
+ * Initial release!
data/README.ja.rdoc ADDED
@@ -0,0 +1,44 @@
1
+ = はじめに
2
+
3
+ == 名前
4
+
5
+ racknga(らくんが)
6
+
7
+ == 説明
8
+
9
+ rroongaの機能を利用したRackのミドルウェアを提供します。
10
+
11
+ * rroonga: http://groonga.rubyforge.org/
12
+ * Rack: http://rack.rubyforge.org/
13
+
14
+ == 作者
15
+
16
+ Kouhei Sutou:: <tt><kou@clear-code.com></tt>
17
+
18
+ == ライセンス
19
+
20
+ LGPL 2.1です。詳しくはlicense/LGPLを見てください。
21
+
22
+ == 依存ソフトウェア
23
+
24
+ * Ruby >= 1.9.1
25
+ * rroonga
26
+ * Rack
27
+
28
+ == インストール
29
+
30
+ % sudo gem install racknga
31
+
32
+ == ドキュメント
33
+
34
+ http://groonga.rubyforge.org/racknga/
35
+
36
+ == メーリングリスト
37
+
38
+ 質問、要望、バグ報告などはgroongaのMLにお願いします。
39
+
40
+ http://lists.sourceforge.jp/mailman/listinfo/groonga-dev
41
+
42
+ == 感謝
43
+
44
+ * ...
data/README.rdoc ADDED
@@ -0,0 +1,44 @@
1
+ = README
2
+
3
+ == Name
4
+
5
+ racknga
6
+
7
+ == Description
8
+
9
+ Rack middlewares that uses rroonga features.
10
+
11
+ * rroonga: http://groonga.rubyforge.org/
12
+ * Rack: http://rack.rubyforge.org/
13
+
14
+ == Authors
15
+
16
+ Kouhei Sutou:: <tt><kou@clear-code.com></tt>
17
+
18
+ == License
19
+
20
+ LGPL 2.1. See license/LGPL for details.
21
+
22
+ == Dependencies
23
+
24
+ * Ruby >= 1.9.1
25
+ * rroonga
26
+ * Rack
27
+
28
+ == Install
29
+
30
+ % sudo gem install racknga
31
+
32
+ == Documents
33
+
34
+ Japanese only. Sorry.
35
+
36
+ http://groonga.rubyforge.org/racknga/
37
+
38
+ == Mailing list
39
+
40
+ http://rubyforge.org/mailman/listinfo/groonga-users-en
41
+
42
+ == Thanks
43
+
44
+ * ...
data/Rakefile ADDED
@@ -0,0 +1,189 @@
1
+ # -*- coding: utf-8; mode: ruby -*-
2
+ #
3
+ # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ require 'English'
19
+
20
+ require 'find'
21
+ require 'fileutils'
22
+ require 'pathname'
23
+ require 'erb'
24
+ require 'rubygems'
25
+ gem 'rdoc'
26
+ require 'hoe'
27
+
28
+ ENV["NODOT"] = "yes"
29
+
30
+ base_dir = Pathname(__FILE__).dirname
31
+ truncate_base_dir = Proc.new do |path|
32
+ path.relative_path_from(base_dir)
33
+ end
34
+
35
+ racknga_lib_dir = base_dir + 'lib'
36
+ $LOAD_PATH.unshift(racknga_lib_dir.to_s)
37
+
38
+ def guess_version
39
+ require 'racknga/version'
40
+ Racknga::VERSION
41
+ end
42
+
43
+ manifest = base_dir + "Manifest.txt"
44
+ manifest_contents = []
45
+ base_dir_included_components = %w(AUTHORS Rakefile
46
+ README.rdoc README.ja.rdoc
47
+ NEWS.rdoc NEWS.ja.rdoc
48
+ rroonga-build.rb extconf.rb pkg-config.rb)
49
+ excluded_components = %w(.cvsignore .gdb_history CVS depend Makefile doc pkg
50
+ .svn .git doc data .test-result tmp)
51
+ excluded_suffixes = %w(.png .ps .pdf .o .so .a .txt .~)
52
+ unless ENV["RUBY_CC_VERSION"]
53
+ excluded_components << "vendor"
54
+ end
55
+ base_dir.find do |target|
56
+ target = truncate_base_dir[target]
57
+ components = target.to_s.split(File::SEPARATOR)
58
+ if components.size == 1 and !target.directory?
59
+ next unless base_dir_included_components.include?(components[0])
60
+ end
61
+ Find.prune if (excluded_components - components) != excluded_components
62
+ next if excluded_suffixes.include?(File.extname(target))
63
+ manifest_contents << target if File.file?(target)
64
+ end
65
+
66
+ File.open(manifest, "w") do |f|
67
+ f.puts manifest_contents.sort.join("\n")
68
+ end
69
+
70
+ # For Hoe's no user friendly default behavior. :<
71
+ File.open("README.txt", "w") {|file| file << "= Dummy README\n== XXX\n"}
72
+ FileUtils.cp("NEWS.rdoc", "History.txt")
73
+ at_exit do
74
+ FileUtils.rm_f("README.txt")
75
+ FileUtils.rm_f("History.txt")
76
+ FileUtils.rm_f(manifest)
77
+ end
78
+
79
+ def cleanup_white_space(entry)
80
+ entry.gsub(/(\A\n+|\n+\z)/, '') + "\n"
81
+ end
82
+
83
+ ENV["VERSION"] ||= guess_version
84
+ version = ENV["VERSION"]
85
+ project = nil
86
+ Hoe.spec('racknga') do
87
+ Hoe::Test::SUPPORTED_TEST_FRAMEWORKS[:testunit2] = "test/run-test.rb"
88
+ project = self
89
+ project.version = version.dup
90
+ project.rubyforge_name = 'groonga'
91
+ authors = File.join(base_dir, "AUTHORS")
92
+ project.author = File.readlines(authors).collect do |line|
93
+ if /\s*<[^<>]*>$/ =~ line
94
+ $PREMATCH
95
+ else
96
+ nil
97
+ end
98
+ end.compact
99
+ project.email = ['groonga-users-en@rubyforge.org',
100
+ 'groonga-dev@lists.sourceforge.jp']
101
+ project.url = 'http://groonga.rubyforge.org/'
102
+ project.testlib = :testunit2
103
+ project.test_globs = ["test/run-test.rb"]
104
+ project.extra_deps += [["rroonga"],
105
+ ["rack"]]
106
+ project.spec_extras = {
107
+ :extra_rdoc_files => Dir.glob("**/*.rdoc"),
108
+ :licenses => ["LGPL 2.1"]
109
+ }
110
+ project.readme_file = "README.ja.rdoc"
111
+
112
+ news_of_current_release = File.read("NEWS.rdoc").split(/^==\s.*$/)[1]
113
+ project.changes = cleanup_white_space(news_of_current_release)
114
+
115
+ entries = File.read("README.rdoc").split(/^==\s(.*)$/)
116
+ description = cleanup_white_space(entries[entries.index("Description") + 1])
117
+ project.summary, project.description, = description.split(/\n\n+/, 3)
118
+
119
+ project.remote_rdoc_dir = "racknga"
120
+ end
121
+
122
+ ObjectSpace.each_object(Rake::RDocTask) do |rdoc_task|
123
+ options = rdoc_task.options
124
+ t_option_index = options.index("--title") || options.index("-t")
125
+ rdoc_task.options[t_option_index, 2] = [] if t_option_index
126
+ rdoc_task.title = "racknga - #{version}"
127
+ end
128
+
129
+ task :publish_docs => [:prepare_docs_for_publishing]
130
+
131
+
132
+ include ERB::Util
133
+
134
+ def apply_template(file, head, header, footer)
135
+ content = File.read(file)
136
+ content = content.sub(/lang="en"/, 'lang="ja"')
137
+
138
+ title = nil
139
+ content = content.sub(/<title>(.+?)<\/title>/) do
140
+ title = $1
141
+ head.result(binding)
142
+ end
143
+
144
+ content = content.sub(/<body(?:.*?)>/) do |body_start|
145
+ "#{body_start}\n#{header.result(binding)}\n"
146
+ end
147
+
148
+ content = content.sub(/<\/body/) do |body_end|
149
+ "\n#{footer.result(binding)}\n#{body_end}"
150
+ end
151
+
152
+ File.open(file, "w") do |file|
153
+ file.print(content)
154
+ end
155
+ end
156
+
157
+ def erb_template(name)
158
+ file = File.join("html", "#{name}.html.erb")
159
+ template = File.read(file)
160
+ erb = ERB.new(template, nil, "-")
161
+ erb.filename = file
162
+ erb
163
+ end
164
+
165
+ task :prepare_docs_for_publishing do
166
+ head = erb_template("head")
167
+ header = erb_template("header")
168
+ footer = erb_template("footer")
169
+ Find.find("doc") do |file|
170
+ if /\.html\z/ =~ file and /_(?:c|rb)\.html\z/ !~ file
171
+ apply_template(file, head, header, footer)
172
+ end
173
+ end
174
+ end
175
+
176
+ task :publish_html do
177
+ config = YAML.load(File.read(File.expand_path("~/.rubyforge/user-config.yml")))
178
+ host = "#{config["username"]}@rubyforge.org"
179
+
180
+ rsync_args = "-av --exclude '*.erb' --exclude '*.svg' --exclude .svn"
181
+ remote_dir = "/var/www/gforge-projects/#{project.rubyforge_name}/"
182
+ sh "rsync #{rsync_args} html/ #{host}:#{remote_dir}"
183
+ end
184
+
185
+ task :tag do
186
+ sh("git tag -a #{version} -m 'release #{version}!!!'")
187
+ end
188
+
189
+ task(:release).prerequisites.reject! {|name| name == "clean"}
@@ -0,0 +1,28 @@
1
+ </div>
2
+
3
+ <div class="sponsors">
4
+ <p id="sponsor-rubyforge">
5
+ <a href="http://rubyforge.org/projects/groonga/">
6
+ <img src="/rubyforge.png" width="120" height="24" border="0" alt="Ruby/groongaプロジェクトはRubyForge.orgにホスティングしてもらっています。" />
7
+ </a>
8
+ </p>
9
+ <p id="sponsor-tango">
10
+ <a href="http://tango.freedesktop.org/">
11
+ <img width="120" height="53" border="0" alt="Tango Desktop Projectのアイコンを利用しています。" src="/tango-logo.png" />
12
+ </a>
13
+ </p>
14
+
15
+ <!-- Piwik -->
16
+ <a href="http://piwik.org" title="Web analytics" onclick="window.open(this.href);return(false);">
17
+ <script type="text/javascript">
18
+ var pkBaseURL = (("https:" == document.location.protocol) ? "https://www.clear-code.com/piwik/" : "http://www.clear-code.com/piwik/");
19
+ document.write(unescape("%3Cscript src='" + pkBaseURL + "piwik.js' type='text/javascript'%3E%3C/script%3E"));
20
+ </script><script type="text/javascript">
21
+ piwik_action_name = '';
22
+ piwik_idsite = 2;
23
+ piwik_url = pkBaseURL + "piwik.php";
24
+ piwik_log(piwik_action_name, piwik_idsite, piwik_url);
25
+ </script>
26
+ <object><noscript><p>Web analytics <img src="http://www.clear-code.com/piwik/piwik.php?idsite=2" style="border:0" alt=""/></p></noscript></object></a>
27
+ <!-- End Piwik Tag -->
28
+ </div>
@@ -0,0 +1,4 @@
1
+ <link rel="stylesheet" href="/ranguba.css" type="text/css" />
2
+ <link rel="shortcut icon" href="/favicon.ico" />
3
+ <link rel="icon" href="/favicon.png" />
4
+ <title><%= h(title) %> - ラングバ</title>
@@ -0,0 +1,17 @@
1
+ <div class="header">
2
+ <div class="title">
3
+ <a href="/">
4
+ <span class="title"><%= h(title) %></span>
5
+ <span class="title-separator">-</span>
6
+ <span class="title-project">ラングバ</span>
7
+ </a>
8
+ </div>
9
+ <ul class="menu">
10
+ <li id="menu-reference"><a href="/rroonga/">リファレンスマニュアル</a></li>
11
+ <li id="menu-tutorial"><a href="/rroonga/text/TUTORIAL_ja_rdoc.html">チュートリアル</a></li>
12
+ <li id="menu-install"><a href="/#install-rroonga">インストール</a></li>
13
+ <li id="menu-developer"><a href="/developer.html">開発者向け情報</a></li>
14
+ </ul>
15
+ </div>
16
+
17
+ <div class="content">
@@ -0,0 +1,75 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ module Racknga
19
+ class CacheDatabase
20
+ def initialize(database_path)
21
+ @database_path = database_path
22
+ @context = Groonga::Context.new(:encoding => :none)
23
+ ensure_database
24
+ end
25
+
26
+ def responses
27
+ @context["Responses"]
28
+ end
29
+
30
+ def purge_old_responses(threshold_time_stamp=nil)
31
+ threshold_time_stamp ||= Time.now
32
+ responses.each do |response|
33
+ response.delete if response.created_at < threshold_time_stamp
34
+ end
35
+ end
36
+
37
+ def ensure_database
38
+ if File.exist?(@database_path)
39
+ @database = Groonga::Database.open(@database_path, :context => @context)
40
+ else
41
+ create_database
42
+ end
43
+ ensure_tables
44
+ end
45
+
46
+ def close_database
47
+ @database.close
48
+ end
49
+
50
+ private
51
+ def create_responses_table
52
+ Groonga::Schema.define(:context => @context) do |schema|
53
+ schema.create_table("Responses",
54
+ :type => :hash,
55
+ :key_type => "ShortText") do |table|
56
+ table.uint32("status")
57
+ table.short_text("headers")
58
+ table.text("body")
59
+ table.time("created_at")
60
+ end
61
+ end
62
+ end
63
+
64
+ def create_database
65
+ FileUtils.mkdir_p(File.dirname(@database_path))
66
+ @database = Groonga::Database.create(:path => @database_path,
67
+ :context => @context)
68
+ end
69
+
70
+ def ensure_tables
71
+ return if responses
72
+ create_responses_table
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,144 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ require 'time'
19
+ require 'net/smtp'
20
+ require 'etc'
21
+ require 'socket'
22
+
23
+ require 'racknga/utils'
24
+
25
+ module Racknga
26
+ # Ruby 1.9 only. 1.8 isn't supported.
27
+ class ExceptionMailNotifier
28
+ def initialize(options)
29
+ @options = Utils.normalize_options(options || {})
30
+ end
31
+
32
+ def notify(exception, environment)
33
+ host = @options[:host] || "localhost"
34
+ return if to.empty?
35
+ mail = format(exception, environment)
36
+ Net::SMTP.start(host, @options[:port]) do |smtp|
37
+ smtp.send_message(mail, from, *to)
38
+ end
39
+ end
40
+
41
+ private
42
+ def format(exception, environment)
43
+ header = format_header(exception, environment)
44
+ body = format_body(exception, environment)
45
+ mail = "#{header}\r\n#{body}"
46
+ mail.force_encoding("utf-8")
47
+ begin
48
+ mail = mail.encode(charset)
49
+ rescue EncodingError
50
+ end
51
+ mail.force_encoding("ASCII-8BIT")
52
+ end
53
+
54
+ def format_header(exception, environment)
55
+ <<-EOH
56
+ MIME-Version: 1.0
57
+ Content-Type: Text/Plain; charset=#{charset}
58
+ Content-Transfer-Encoding: #{transfer_encoding}
59
+ From: #{from}
60
+ To: #{to.join(', ')}
61
+ Subject: #{encode_subject(subject(exception, environment))}
62
+ Date: #{Time.now.rfc2822}
63
+ EOH
64
+ end
65
+
66
+ def format_body(exception, environment)
67
+ request = Rack::Request.new(environment)
68
+ body = <<-EOB
69
+ URL: #{request.url}
70
+ --
71
+ #{exception.class}: #{exception}
72
+ --
73
+ #{exception.backtrace.join("\n")}
74
+ EOB
75
+ params = request.params
76
+ max_key_size = (environment.keys.collect(&:size) +
77
+ params.keys.collect(&:size)).max
78
+ body << <<-EOE
79
+ --
80
+ Environments:
81
+ EOE
82
+ environment.sort_by {|key, value| key}.each do |key, value|
83
+ body << " %*s: <%s>\n" % [max_key_size, key, value]
84
+ end
85
+
86
+ unless params.empty?
87
+ body << <<-EOE
88
+ --
89
+ Parameters:
90
+ EOE
91
+ params.sort_by {|key, value| key}.each do |key, value|
92
+ body << " %#{max_key_size}s: <%s>\n" % [key, value]
93
+ end
94
+ end
95
+
96
+ body
97
+ end
98
+
99
+ def subject(exception, environment)
100
+ [@options[:subject_label], exception.to_s].compact.join(' ')
101
+ end
102
+
103
+ def to
104
+ @to ||= ensure_array(@options[:to]) || []
105
+ end
106
+
107
+ def ensure_array(maybe_array)
108
+ maybe_array = [maybe_array] if maybe_array.is_a?(String)
109
+ maybe_array
110
+ end
111
+
112
+ def from
113
+ @from ||= @options[:from] || guess_from
114
+ end
115
+
116
+ def guess_from
117
+ name = Etc.getpwuid(Process.uid).name
118
+ host = Socket.gethostname
119
+ "#{name}@#{host}"
120
+ end
121
+
122
+ def charset
123
+ @options[:charset] || 'utf-8'
124
+ end
125
+
126
+ def transfer_encoding
127
+ case charset
128
+ when /\Autf-8\z/i
129
+ "8bit"
130
+ else
131
+ "7bit"
132
+ end
133
+ end
134
+
135
+ def encode_subject(subject)
136
+ case charset
137
+ when /\Aiso-2022-jp\z/i
138
+ NKF.nkf('-Wj -M', subject)
139
+ else
140
+ NKF.nkf('-Ww -M', subject)
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,122 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2010 Kouhei Sutou <kou@clear-code.com>
4
+ #
5
+ # This library is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU Lesser General Public
7
+ # License version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ require 'yaml'
19
+ require 'racknga/cache_database'
20
+
21
+ module Racknga
22
+ module Middleware
23
+ class Cache
24
+ def initialize(application, options={})
25
+ @application = application
26
+ @options = Utils.normalize_options(options || {})
27
+ database_path = @options[:database_path]
28
+ raise ArgumentError, ":database_path is missing" if database_path.nil?
29
+ @database = CacheDatabase.new(database_path)
30
+ end
31
+
32
+ def call(environment)
33
+ request = Rack::Request.new(environment)
34
+ return @application.call(environment) unless use_cache?(request)
35
+ key = "#{key_prefix(request)}:#{normalize_path(request.fullpath)}"
36
+ cache = @database.responses
37
+ record = cache[key]
38
+ if record
39
+ handle_request_with_cache(cache, key, record, request)
40
+ else
41
+ handle_request(cache, key, request)
42
+ end
43
+ end
44
+
45
+ def ensure_database
46
+ @database.ensure_database
47
+ end
48
+
49
+ def close_database
50
+ @database.close_database
51
+ end
52
+
53
+ private
54
+ def use_cache?(requeust)
55
+ requeust.get? or requeust.head?
56
+ end
57
+
58
+ def key_prefix(request)
59
+ if request.respond_to?(:mobile?) and request.mobile?
60
+ last_component = request.mobile.class.name.split(/::/).last
61
+ "mobile:#{last_component.downcase}"
62
+ else
63
+ "pc"
64
+ end
65
+ end
66
+
67
+ def normalize_path(path)
68
+ path.gsub(/&callback=jsonp\d+&_=\d+\z/, '')
69
+ end
70
+
71
+ def skip_caching_response?(status, headers, body)
72
+ return true if status != 200
73
+ return true if status != 200
74
+
75
+ headers = Rack::Utils::HeaderHash.new(headers)
76
+ content_type = headers["Content-Type"]
77
+ if /\A(\w+)\/([\w.+\-]+)\b/ =~ content_type
78
+ media_type = $1
79
+ sub_type = $2
80
+ return false if media_type == "text"
81
+ return false if sub_type == "json"
82
+ return false if sub_type == "xml"
83
+ return false if /\+xml\z/ =~ sub_type
84
+ end
85
+ true
86
+ end
87
+
88
+ def handle_request(cache, key, request)
89
+ status, headers, body = @application.call(request.env)
90
+ if skip_caching_response?(status, headers, body)
91
+ return [status, headers, body]
92
+ end
93
+
94
+ now = Time.now
95
+ headers = Rack::Utils::HeaderHash.new(headers)
96
+ headers["Last-Modified"] ||= now.httpdate
97
+ stringified_body = ''
98
+ body.each do |data|
99
+ stringified_body << data
100
+ end
101
+ headers = headers.to_hash
102
+ cache[key] = {
103
+ :status => status,
104
+ :headers => headers.to_yaml,
105
+ :body => stringified_body.force_encoding("ASCII-8BIT"),
106
+ :created_at => now,
107
+ }
108
+ body = [stringified_body]
109
+ [status, headers, body]
110
+ end
111
+
112
+ def handle_request_with_cache(cache, key, record, request)
113
+ body = record["body"]
114
+ return handle_request(cache, key, request) if body.nil?
115
+
116
+ status = record["status"]
117
+ headers = YAML.load(record["headers"])
118
+ [status, headers, [body]]
119
+ end
120
+ end
121
+ end
122
+ end