rattlecache 0.1

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/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .rvmrc
6
+ test.rb
7
+ .idea/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in rattlecache.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2011 Marv Cool
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,2 @@
1
+ Rattlecache is an abstraction layer between the battle.net API and Libraries (in particular the ruby gem "battlnet") that consume it.
2
+ It supports multiple backends (in which the json objects are stored) and takes care of expiring timers for the objects.
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = ['--color', '-f progress', '-r ./spec/spec_helper.rb']
7
+ t.pattern = 'spec/**/*_spec.rb'
8
+ t.fail_on_error = false
9
+ end
@@ -0,0 +1,49 @@
1
+ module Rattlecache
2
+ module Backend
3
+
4
+ class InvalidBackend < Exception; end
5
+
6
+ extend self
7
+
8
+ attr_reader :backends
9
+
10
+ @backends = {
11
+ :filesystem => "Filesystem"
12
+ #:redis => "Redis",
13
+ #:somOtherCoolBackend => "someOther"
14
+ }
15
+
16
+ def fetch(backend_name)
17
+ unless @backends.include?(backend_name)
18
+ raise InvalidBackend.new("#{backend_name.to_s} is not a valid backend!")
19
+ end
20
+
21
+ backend_class = @backends[backend_name]
22
+ return load_backend(backend_name, backend_class)
23
+ end
24
+
25
+ def register(identifier, klass)
26
+ @backends[identifier] = klass
27
+ end
28
+
29
+ private
30
+
31
+ def load_backend(backend_name, klass_name)
32
+ begin
33
+ klass = Rattlecache::Backend.const_get("#{klass_name}", false)
34
+ rescue NameError
35
+ begin
36
+ backend_file = "backends/#{backend_name.to_s}"
37
+ require backend_file
38
+ klass = Rattlecache::Backend.const_get("#{klass_name}", false)
39
+ rescue LoadError
40
+ raise InvalidBackend.new("backend #{klass_name} does not exist, and file #{backend_file} does not exist")
41
+ rescue NameError
42
+ raise InvalidBackend.new("expected #{backend_file} to define Rattlecache::Backend::#{klass_name}")
43
+ end
44
+ end
45
+
46
+ return klass.new
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,55 @@
1
+ require 'json'
2
+ require 'base64'
3
+
4
+ module Rattlecache
5
+ module Backend
6
+ class Filesystem < Cache
7
+
8
+ def initialize(prefix = "/tmp/rattlecache/")
9
+ @prefix = prefix
10
+ end
11
+
12
+ def get(objectKey)
13
+ #puts "Filesystem gets you: #{objectKey}"
14
+ # get the file from the filesystem
15
+ ret = {:status => 404, :lastModified => nil, :object => nil}
16
+ begin
17
+ f = open_file(objectKey)
18
+ firstline = f.first
19
+ object = f.read
20
+ mtime = f.mtime
21
+ f.close
22
+ ret = {:status => 200,
23
+ :object => object,
24
+ :lastModified => mtime,
25
+ :header => Base64.strict_decode64(firstline.chomp)
26
+ }
27
+ rescue Errno::ENOENT
28
+ end
29
+ return ret
30
+ end
31
+
32
+ def post(object)
33
+ # put the file to the filesysten
34
+ firstline = Base64.strict_encode64(object[:header].to_json)
35
+ #puts "Debug: putting headers as firstline: #{firstline}"
36
+ f = open_file(object[:key],"w")
37
+ f.puts(firstline)
38
+ f.puts(object[:data])
39
+ f.close
40
+ #puts "Debug: filesystem posted #{object[:key]}"
41
+ end
42
+
43
+ def open_file(objectKey,how="r")
44
+ begin
45
+ Dir.mkdir(@prefix) unless File.directory?(@prefix)
46
+ File.open(@prefix+objectKey,how)
47
+ rescue
48
+ # raise this to the caller
49
+ raise
50
+ end
51
+ end
52
+
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,79 @@
1
+ module Rattlecache
2
+ class Auctionscache < Cache
3
+
4
+
5
+ # @param backend [Rattlecache::Backend]
6
+ # @param adapter [Battlenet::Adapter::AbstractAdapter]
7
+ def initialize(backend, adapter)
8
+ @adapter = adapter
9
+ @backend = backend
10
+ end
11
+
12
+ # @param url [String]
13
+ # @return [String|nil]
14
+ def get(url,header)
15
+
16
+ cached_data = @backend.get(sanitize(url))
17
+
18
+ # Check if any fields are requested.
19
+ # If yes, request without fields and look into ['lastModified']
20
+ # if we need to fetch new or answer with our cached result.",
21
+ if cached_data[:status] == 200
22
+ puts "Debug: Auctionscache: cache HIT for #{url}"
23
+ unless is_lmt_and_url_request(url)
24
+ puts "Debug: Auctionscache: has fields. Timethings: #{cached_data[:lastModified]} < #{get_lmt(url,header)} #{cached_data[:lastModified] < get_lmt(url,header)}"
25
+ if cached_data[:lastModified] < get_lmt(url,header)
26
+ cached_data = {:status => 404}
27
+ end
28
+ else
29
+ # if a guild is requested without additional fields,
30
+ # simply check if this file is still valid
31
+ if generic_needs_request?(cached_data[:header],cached_data[:lastModified])
32
+ cached_data = {:status => 404}
33
+ end
34
+ end
35
+ else
36
+ puts "Debug: Auctionscache: cache MISS for #{url}"
37
+ end
38
+ cached_data
39
+ end
40
+
41
+ # check if the supplied url is not pointing to a .json auctions file,
42
+ # but to a generic url for lmt and the .json url
43
+ def is_lmt_and_url_request(url)
44
+ not url.include?(".json")
45
+ end
46
+
47
+ # method to get the lastModified info for a specified url
48
+ # @param url [String] the url for which the LMT is wanted
49
+ # @param header [Hash] the Hash of headers for a api request
50
+ def get_lmt(url,header)
51
+ cached_data = @backend.get(sanitize(generic_auctions_url(url)))
52
+ puts "Debug: get_lmt() cache result for generic url: #{cached_data[:status]}"
53
+ # if the object was not in the cache or it is expired
54
+ if cached_data[:status] != 200 or generic_needs_request?(cached_data[:header],cached_data[:lastModified])
55
+ # request it from the api
56
+ request_generic(url,header)
57
+ # and load the new object
58
+ cached_data = @backend.get(sanitize(generic_auctions_url(url)))
59
+ end
60
+ # this a JS milliseconds timestamp, we only do seconds!
61
+ Time.at(JSON.parse(cached_data[:object])["lastModified"]/1000)
62
+ end
63
+
64
+ def request_generic(url,header)
65
+ # need to request the generic guild response
66
+ url = generic_auctions_url(url)
67
+ # request it from the API:
68
+ got = request_raw(url,header)
69
+ @backend.post({:key => sanitize(url),:header => got.header.to_hash, :data => got.body}) # and put into cache
70
+ end
71
+
72
+
73
+ def generic_auctions_url(url)
74
+ u = URI.parse(url)
75
+ u.path=(u.path.gsub("-data","/data").gsub(/\/auctions.*\.json$/,""))
76
+ u.to_s
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,73 @@
1
+ module Rattlecache
2
+ class Fieldsrequestcache < Cache
3
+
4
+
5
+ # @param backend [Rattlecache::Backend]
6
+ # @param adapter [Battlenet::Adapter::AbstractAdapter]
7
+ def initialize(backend, adapter)
8
+ @adapter = adapter
9
+ @backend = backend
10
+ end
11
+
12
+ # @param url [String]
13
+ # @return [String|nil]
14
+ def get(url,header)
15
+
16
+ cached_data = @backend.get(sanitize(url))
17
+
18
+ # Check if any fields are requested.
19
+ # If yes, request without fields and look into ['lastModified']
20
+ # if we need to fetch new or answer with our cached result.",
21
+ if cached_data[:status] == 200
22
+ puts "Debug: guildcache: cache HIT for #{url}"
23
+ if has_fields?(url)
24
+ puts "Debug: guildcache: has fields. Timethings: #{cached_data[:lastModified]} < #{get_lmt(url,header)} #{cached_data[:lastModified] < get_lmt(url,header)}"
25
+ if cached_data[:lastModified] < get_lmt(url,header)
26
+ cached_data = {:status => 404}
27
+ end
28
+ else
29
+ # if a guild is requested without additional fields,
30
+ # simply check if this file is still valid
31
+ if generic_needs_request?(cached_data[:header],cached_data[:lastModified])
32
+ cached_data = {:status => 404}
33
+ end
34
+ end
35
+ else
36
+ puts "Debug: guildcache: cache MISS for #{url}"
37
+ end
38
+ cached_data
39
+ end
40
+
41
+ # method to get the lastModified info for a specified url
42
+ # @param url [String] the url for which the LMT is wanted
43
+ # @param header [Hash] the Hash of headers for a api request
44
+ def get_lmt(url,header)
45
+ cached_data = @backend.get(sanitize(url_without_fields(url)))
46
+ puts "Debug: get_lmt() cache result for generic url: #{cached_data[:status]}"
47
+ # if the object was not in the cache or it is expired
48
+ if cached_data[:status] != 200 or generic_needs_request?(cached_data[:header],cached_data[:lastModified])
49
+ # request it from the api
50
+ request_without_fields(url,header)
51
+ # and load the new object
52
+ cached_data = @backend.get(sanitize(url_without_fields(url)))
53
+ end
54
+ # this a JS milliseconds timestamp, we only do seconds!
55
+ Time.at(JSON.parse(cached_data[:object])["lastModified"]/1000)
56
+ end
57
+
58
+ def request_without_fields(url,header)
59
+ # need to request the generic guild response
60
+ url = url_without_fields(url)
61
+ # request it from the API:
62
+ got = request_raw(url,header)
63
+ @backend.post({:key => sanitize(url),:header => got.header.to_hash, :data => got.body}) # and put into cache
64
+ end
65
+
66
+ def url_without_fields(url)
67
+ url_obj = URI.parse(url)
68
+ url_obj.query=(url_obj.query.gsub(/fields=.+&|$/,"")) # strip the fields
69
+ url_obj.to_s
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,144 @@
1
+ require 'backend_manager'
2
+ require 'net/http'
3
+ require 'digest/sha2'
4
+
5
+ module Rattlecache
6
+
7
+ class Cache
8
+
9
+ # @param backend [Symbol]
10
+ # @param adapter [Battlenet::Adapter::AbstractAdapter]
11
+ def initialize(backend = :filesystem, adapter = nil)
12
+ puts "Debug: rattlecache adapter: #{adapter}"
13
+ @adapter = adapter
14
+ @backend = Rattlecache::Backend.fetch(backend)
15
+ end
16
+
17
+ @request_pragmas = {
18
+ "guild" => "Check if any fields are requested. If yes, request without fields and look into ['lastModified'] if we need to fetch new or answer with out cached result.",
19
+ "data" => "Everything data related should be considered stable. Cache it for one week.",
20
+ "auction" => "dont know yet...",
21
+ "else" => "cache it for as long as 'RETRY-AFTER'-responseheader told us. (600 sec)"
22
+ }
23
+
24
+ # @param url [String]
25
+ # @param header [Hash]
26
+ def get(url, header = nil)
27
+ @header = header
28
+ @header['User-Agent'] = @header['User-Agent'] << " (with rattlecache)"
29
+ #puts "Cache class gets you: #{objectKey}"
30
+
31
+ # what to do with this request?
32
+ case request_type(url)
33
+ when "guild"
34
+ puts "Debug: its a guild related request!"
35
+ require 'caches/Fieldsrequestcache'
36
+ Rattlecache::Fieldsrequestcache.new(@backend,@adapter).get(url,header)
37
+ when "character"
38
+ puts "Debug: its a character related request!"
39
+ require 'caches/Fieldsrequestcache'
40
+ Rattlecache::Fieldsrequestcache.new(@backend,@adapter).get(url,header)
41
+ when /auction.*/
42
+ puts "Debug: its a auchtion related request!"
43
+ require 'caches/Auctionscache'
44
+ Rattlecache::Auctionscache.new(@backend,@adapter).get(url,header)
45
+ when "item"
46
+ # for items it seems reasonable to cache them at least for a week
47
+ # a week in seconds: 60*60*24*7 = 604800
48
+ check_and_return(@backend.get(sanitize(url)),604800)
49
+ else
50
+ puts "Debug: its a boring request!"
51
+ check_and_return(@backend.get(sanitize(url)))
52
+ end
53
+ end
54
+
55
+ def check_and_return(backend_result,given_time = nil)
56
+ if given_time.nil?
57
+ if backend_result[:status] == 200 and generic_needs_request?(backend_result[:header],backend_result[:lastModified])
58
+ backend_result = {:status => 404}
59
+ end
60
+ else
61
+ if backend_result[:status] == 200 and needs_request_with_given_time?(given_time,backend_result[:lastModified])
62
+ backend_result = {:status => 404}
63
+ end
64
+ end
65
+ backend_result
66
+ end
67
+
68
+ # @param object [Hash]
69
+ def post(object)
70
+ #puts "Cache class puts: #{object[:key]}"
71
+ @backend.post({:key => sanitize(object[:key]), :header => object[:header], :data => object[:data]})
72
+ end
73
+
74
+ # @param headerline [Hash]
75
+ # @param mtime [Time]
76
+ # @return [TrueClass|FalseClass]
77
+ def generic_needs_request?(headerline,mtime)
78
+ header = JSON.parse(headerline)
79
+ #header["date"][0] is a String with CGI.rfc1123_date() encoded time,
80
+ # as there is no easy method to inverse this coding, I will keep using the files mtime
81
+ # to estimate when the data was last recieved.
82
+ unless header["cache-control"].nil?
83
+ mtime+header["cache-control"][0].split("=")[1].to_i < Time.now()
84
+ else
85
+ unless header["retry-after"].nil?
86
+ mtime+(header["retry-after"][0].to_i) < Time.now()
87
+ else
88
+ # if we dont find any hint, pull it again!
89
+ puts "Warning: Cache couldn't find any hint if this object is still valid!"
90
+ true
91
+ end
92
+ end
93
+ end
94
+
95
+ def needs_request_with_given_time?(given_time,mtime)
96
+ mtime+given_time < Time.now
97
+ end
98
+
99
+ # @param objectKey [String]
100
+ # @return [String]
101
+ def sanitize(objectKey)
102
+ # strip scheme, sort paramters and encode for safty
103
+ urlObj = URI.parse(objectKey)
104
+ key = urlObj.host
105
+ key << urlObj.path
106
+ key << sort_params(urlObj.query)
107
+ Digest::SHA256.hexdigest(key)
108
+ end
109
+
110
+ # @param query [String]
111
+ # @return [String]
112
+ def sort_params(query)
113
+ q = Hash.new
114
+ query.split("&").each do |parampair|
115
+ q[parampair.split("=")[0]] = parampair.split("=")[1]
116
+ end
117
+ s = Array.new
118
+ q.sort.each { |pair| s << pair.join("=")}
119
+ "?"+s.join("&")
120
+ end
121
+
122
+ # @param objectKey [String]
123
+ # @return [String]
124
+ def request_type(objectKey)
125
+ #[0] = "". [1]= "api". [2]="wow", [3]= what we want
126
+ URI.parse(objectKey).path.split("/")[3]
127
+ end
128
+
129
+ # @param query [String]
130
+ # @return [TrueClass|FalseClass]
131
+ def has_fields?(query)
132
+ not query.scan(/fields=/).empty?
133
+ end
134
+
135
+ # @param url [String]
136
+ # @param header [Hash]
137
+ # @return [Net::HTTPResponse]
138
+ def request_raw(url,header)
139
+ req = @adapter.get(url,header,true)
140
+ req.get(url,header)
141
+ end
142
+
143
+ end
144
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "rattlecache"
6
+ s.version = "0.1"
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ["Marv Cool"]
9
+ s.email = "marv@hostin.is"
10
+ s.homepage = "https://github.com/MrMarvin/rattlcache/wiki"
11
+ s.summary = %q{A smart caching system for battlenet API Requests.}
12
+
13
+ if s.respond_to?(:add_development_dependency)
14
+ s.add_development_dependency "rspec"
15
+ end
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rattlecache
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ version: "0.1"
9
+ platform: ruby
10
+ authors:
11
+ - Marv Cool
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2011-08-23 00:00:00 +02:00
17
+ default_executable:
18
+ dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: rspec
21
+ prerelease: false
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description:
33
+ email: marv@hostin.is
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files: []
39
+
40
+ files:
41
+ - .gitignore
42
+ - Gemfile
43
+ - LICENSE
44
+ - README.md
45
+ - Rakefile
46
+ - lib/backend_manager.rb
47
+ - lib/backends/filesystem.rb
48
+ - lib/caches/Auctionscache.rb
49
+ - lib/caches/Fieldsrequestcache.rb
50
+ - lib/rattlecache.rb
51
+ - rattlecache.gemspec
52
+ has_rdoc: true
53
+ homepage: https://github.com/MrMarvin/rattlcache/wiki
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.7
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: A smart caching system for battlenet API Requests.
84
+ test_files: []
85
+