cacho 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +19 -0
  2. data/Rakefile +6 -0
  3. data/cacho.gemspec +13 -0
  4. data/lib/cacho.rb +104 -0
  5. data/test/cacho.rb +85 -0
  6. metadata +112 -0
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Damian Janowski & Michel Martens
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ task :test do
2
+ require "cutest"
3
+ Cutest.run(Dir["test/*"])
4
+ end
5
+
6
+ task :default => :test
data/cacho.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "cacho"
3
+ s.version = "0.0.1"
4
+ s.summary = "Cache aware, Redis based HTTP client."
5
+ s.description = "HTTP client that understands cache responses and stores results in Redis."
6
+ s.authors = ["Damian Janowski", "Michel Martens"]
7
+ s.email = ["djanowski@dimaion.com", "michel@soveran.com"]
8
+ s.homepage = "http://github.com/djanowski/cacho"
9
+ s.files = ["LICENSE", "Rakefile", "lib/cacho.rb", "cacho.gemspec", "test/cacho.rb"]
10
+ s.add_dependency "redis", "~> 2.1"
11
+ s.add_dependency "curb", "~> 0.7"
12
+ s.add_dependency "mock-server", "~> 0.1"
13
+ end
data/lib/cacho.rb ADDED
@@ -0,0 +1,104 @@
1
+ require "curb"
2
+ require "redis"
3
+ require "json"
4
+
5
+ class Cacho
6
+ VERSION = "0.0.1"
7
+
8
+ def self.get(url, request_headers = {})
9
+ response = Local.get(url)
10
+
11
+ if response.nil?
12
+ response = Remote.get(url, Local.validation_for(url).merge(request_headers))
13
+ Local.set(url, response)
14
+ end
15
+
16
+ response
17
+ end
18
+
19
+ class Local
20
+ def self.get(url)
21
+ expire, json = redis.hmget(url, :expire, :response)
22
+
23
+ if json && (expire.nil? || Time.utc(expire) <= Time.now)
24
+ JSON.parse(json)
25
+ end
26
+ end
27
+
28
+ def self.set(url, response)
29
+ return unless cacheable?(response)
30
+
31
+ _, headers, _ = response
32
+
33
+ fields = {}
34
+
35
+ if headers["Cache-Control"]
36
+ ttl = headers["Cache-Control"][/max\-age=(\d+)/, 1].to_i
37
+
38
+ fields[:expire] = (Time.now + ttl).to_i
39
+ end
40
+
41
+ fields[:response] = response.to_json
42
+
43
+ fields[:etag] = headers["Etag"]
44
+ fields[:last_modified] = headers["Last-Modified"]
45
+
46
+ redis.hmset(url, *fields.to_a.flatten)
47
+ end
48
+
49
+ def self.validation_for(url)
50
+ etag, last_modified = redis.hmget(url, :etag, :last_modified)
51
+
52
+ {}.tap do |headers|
53
+ headers["If-None-Match"] = etag if etag
54
+ headers["If-Modified-Since"] = last_modified if last_modified
55
+ end
56
+ end
57
+
58
+ def self.cacheable?(response)
59
+ status, headers, _ = response
60
+
61
+ status == 200 && (headers["Cache-Control"] || headers["Etag"] || headers["Last-Modified"])
62
+ end
63
+
64
+ def self.redis
65
+ Redis.current
66
+ end
67
+ end
68
+
69
+ class Remote
70
+ def self.get(url, request_headers)
71
+ status = nil
72
+ headers = {}
73
+ body = ""
74
+
75
+ Curl::Easy.http_get(url) do |curl|
76
+ curl.headers = request_headers
77
+
78
+ curl.on_header do |header|
79
+ headers.store(*header.rstrip.split(": ", 2)) if header.include?(":")
80
+ header.bytesize
81
+ end
82
+
83
+ curl.on_body do |string|
84
+ body << string
85
+ string.bytesize
86
+ end
87
+
88
+ curl.on_complete do |response|
89
+ status = response.response_code
90
+ end
91
+ end
92
+
93
+ if status == 301
94
+ Local.get(url)
95
+ else
96
+ [status, headers, body]
97
+ end
98
+ end
99
+
100
+ def self.redis
101
+ Redis.current
102
+ end
103
+ end
104
+ end
data/test/cacho.rb ADDED
@@ -0,0 +1,85 @@
1
+ require "cutest"
2
+ require "socket"
3
+ require "mock_server"
4
+
5
+ require File.expand_path("../lib/cacho", File.dirname(__FILE__))
6
+
7
+ include MockServer::Methods
8
+
9
+ mock_server do
10
+ get "/cacheable" do
11
+ response.headers["Cache-Control"] = "public, max-age=1"
12
+ response.headers["Content-Type"] = "text/plain"
13
+ Time.now.httpdate
14
+ end
15
+
16
+ get "/non-cacheable" do
17
+ response.headers["Content-Type"] = "text/plain"
18
+ Time.now.httpdate
19
+ end
20
+
21
+ get "/etag" do
22
+ if request.env["HTTP_IF_MODIFIED_SINCE"]
23
+ halt 301
24
+ else
25
+ time = Time.now
26
+
27
+ response.headers["Etag"] = time.hash.to_s
28
+ response.headers["Last-Modified"] = time.httpdate
29
+ response.headers["Content-Type"] = "text/plain"
30
+
31
+ time.httpdate
32
+ end
33
+ end
34
+
35
+ get "/echo" do
36
+ request.env.map do |name, value|
37
+ "#{name}: #{value}"
38
+ end.join("\n")
39
+ end
40
+ end
41
+
42
+ prepare do
43
+ Redis.current.flushdb
44
+ end
45
+
46
+ test "caches cacheable responses" do
47
+ status, headers, body = Cacho.get("http://localhost:4000/cacheable")
48
+
49
+ assert_equal status, 200
50
+ assert_equal headers["Content-Type"], "text/plain"
51
+
52
+ t1 = body
53
+
54
+ status, headers, body = Cacho.get("http://localhost:4000/cacheable")
55
+
56
+ assert_equal t1, body
57
+
58
+ sleep 2
59
+
60
+ status, headers, body = Cacho.get("http://localhost:4000/cacheable")
61
+
62
+ assert body > t1
63
+ end
64
+
65
+ test "does not cache non-cacheable responses" do
66
+ _, _, t1 = Cacho.get("http://localhost:4000/non-cacheable")
67
+ sleep 1
68
+ _, _, t2 = Cacho.get("http://localhost:4000/non-cacheable")
69
+
70
+ assert t2 > t1
71
+ end
72
+
73
+ test "performs conditional GETs" do
74
+ _, _, t1 = Cacho.get("http://localhost:4000/etag")
75
+ sleep 1
76
+ _, _, t2 = Cacho.get("http://localhost:4000/etag")
77
+
78
+ assert_equal t1, t2
79
+ end
80
+
81
+ test "allows to pass custom HTTP headers" do
82
+ _, _, body = Cacho.get("http://localhost:4000/echo", "Accept" => "text/plain")
83
+
84
+ assert body =~ %r{HTTP_ACCEPT: text/plain}
85
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cacho
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Damian Janowski
13
+ - Michel Martens
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-22 00:00:00 -03:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: redis
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 2
31
+ - 1
32
+ version: "2.1"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: curb
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 0
45
+ - 7
46
+ version: "0.7"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: mock-server
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ~>
56
+ - !ruby/object:Gem::Version
57
+ segments:
58
+ - 0
59
+ - 1
60
+ version: "0.1"
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ description: HTTP client that understands cache responses and stores results in Redis.
64
+ email:
65
+ - djanowski@dimaion.com
66
+ - michel@soveran.com
67
+ executables: []
68
+
69
+ extensions: []
70
+
71
+ extra_rdoc_files: []
72
+
73
+ files:
74
+ - LICENSE
75
+ - Rakefile
76
+ - lib/cacho.rb
77
+ - cacho.gemspec
78
+ - test/cacho.rb
79
+ has_rdoc: true
80
+ homepage: http://github.com/djanowski/cacho
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ segments:
94
+ - 0
95
+ version: "0"
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ requirements: []
105
+
106
+ rubyforge_project:
107
+ rubygems_version: 1.3.7
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: Cache aware, Redis based HTTP client.
111
+ test_files: []
112
+