amfetamine 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ require "dalli"
2
+
3
+ module Amfetamine
4
+ # Class that functions as the general talk-to cache
5
+ # Seperated from adapter functions to decrease method definitions
6
+ class Cache
7
+ include CachingAdapter
8
+ end
9
+ end
@@ -0,0 +1,66 @@
1
+ module Amfetamine
2
+ # This adapter wraps methods around memcached (dalli) methods
3
+ module CachingAdapter
4
+ def self.included(base)
5
+ base.extend ClassAndInstanceMethods
6
+ base.extend CacheServer
7
+ base.send(:include,ClassAndInstanceMethods)
8
+ end
9
+
10
+
11
+ def initialize(server, options={})
12
+ @cache_server ||= Dalli::Client.new(server, options)
13
+ end
14
+
15
+ def cache_server
16
+ @cache_server
17
+ end
18
+
19
+ private :cache_server
20
+
21
+ module ClassAndInstanceMethods
22
+ def get(key)
23
+ cache_server.get(key)
24
+ end
25
+
26
+ def set(key,data)
27
+ cache_server.set(key, data)
28
+ end
29
+
30
+ def add(key, data)
31
+ cache_server.add(key,data)
32
+ end
33
+
34
+ def delete(key)
35
+ cache_server.delete(key)
36
+ end
37
+
38
+ def flush
39
+ cache_server.flush
40
+ end
41
+
42
+ def fetch(key)
43
+ #cache_server.fetch(key,&block)
44
+ val = get(key)
45
+ if val.nil? && block_given?
46
+ val = yield
47
+ add(key,val)
48
+ else
49
+ Amfetamine.logger.info "Hit! #{key}"
50
+ end
51
+ val
52
+ end
53
+ end
54
+
55
+ module CacheServer
56
+ private
57
+ def cache_server
58
+ Amfetamine::Config.memcached_instance
59
+ end
60
+
61
+
62
+
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,34 @@
1
+ module Amfetamine
2
+ class Config
3
+ class << self
4
+
5
+ attr_reader :memcached_instance, :rest_client, :base_uri, :resource_suffix, :logger
6
+
7
+ def configure
8
+ yield(self)
9
+ @base_uri ||= ""
10
+ end
11
+
12
+ def memcached_instance=(value, options={})
13
+ raise ConfigurationInvalid, 'Invalid value for memcached_instance' if !value.is_a?(String)
14
+ @memcached_instance ||= Dalli::Client.new(value, options)
15
+ end
16
+
17
+ def rest_client=(value)
18
+ raise ConfigurationInvalid, 'Invalid value for rest_client' if ![:get,:put,:delete,:post].all? { |m| value.respond_to?(m) }
19
+ @rest_client ||= value
20
+ end
21
+
22
+ # Shouldn't be needed as our favourite rest clients are based on httparty, still, added it for opensource reasons
23
+ def base_uri=(value)
24
+ raise ConfigurationInvalid, "Invalid value for base uri, should be a string" if !value.is_a?(String)
25
+ @base_uri ||= value
26
+ end
27
+
28
+ def resource_suffix=(value)
29
+ raise ConfigurationInvalid, "Invalid value for resource suffix, should be a string" if !value.is_a?(String)
30
+ @resource_suffix ||= value
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ module Amfetamine
2
+ class InvalidPath < Exception; end; # Only used for paths not having parent ids
3
+ class ConfigurationInvalid < Exception;end; # -red
4
+ class UnknownRESTMethod < Exception; end; # Only used if rest method is not handled by amfetamine
5
+ class RecordNotFound < Exception; end;
6
+ class InvalidCacheData < Exception; end;
7
+ #class MatcherNotImplemented < Exception; end;
8
+ class ExternalConnectionsNotAllowed < Exception; end; # Used for test helpers if connection is not allowed and attempt is made to connect
9
+ end
@@ -0,0 +1,5 @@
1
+ RSpec::Matchers.define :make_request_to do |expected|
2
+ match do |actual|
3
+ raise MatcherNotImplemented
4
+ end
5
+ end
@@ -0,0 +1,113 @@
1
+ module Amfetamine
2
+ module TestHelpers
3
+ # Uses fakeweb to block all connections
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ # Allows for preventing external connections, also in a block
10
+ def prevent_external_connections!
11
+ save_rest_client
12
+ self.rest_client = NeinNeinNein.new
13
+
14
+ if block_given?
15
+ yield self.rest_client
16
+ restore_rest_client
17
+ end
18
+ end
19
+
20
+ def save_rest_client
21
+ @_old_rest_client = self.rest_client || @_old_rest_client
22
+ end
23
+
24
+ def restore_rest_client
25
+ self.rest_client = @_old_rest_client || self.rest_client
26
+ end
27
+
28
+ # Prevents external connections and provides a simple dsl
29
+ #
30
+ # Dummy.stub_responses! do |r|
31
+ # r.get(code: 200, path: '/dummies') { dummy }
32
+ # end
33
+ def stub_responses!
34
+ prevent_external_connections!
35
+ yield rest_client
36
+ end
37
+ end
38
+ end
39
+
40
+ # Annoying stub for http responses
41
+ class NeinNeinNein
42
+ def respond_to?(method)
43
+ if [:get,:post,:delete, :put].include?(method)
44
+ true
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def method_missing(method, *args, &block)
51
+ if [:get,:post,:delete,:put].include?(method)
52
+ # TODO: Dump and rewrite
53
+
54
+ # If this is the dsl calling
55
+ if args.first.is_a?(Hash) || args.empty?
56
+ opts = args.first || {}
57
+ path = opts[:path] || 'default'
58
+ code = opts[:code] || 200
59
+ query = opts[:query]
60
+ else # Else this is a request
61
+ path = args[0] || 'default'
62
+ query = args[1][:query]
63
+ end
64
+
65
+ paths_with_values = instance_variable_get("@#{method.to_s}") || {}
66
+
67
+ path.gsub!(/\/$/,'') #remove trailing slash
68
+ old_path = path
69
+ path += query.to_s.strip
70
+
71
+ if block_given?
72
+ paths_with_values[path]= FakeResponse.new(method, code, block)
73
+ instance_variable_set("@#{method.to_s}", paths_with_values)
74
+ end
75
+
76
+ response = paths_with_values ? (paths_with_values[path] || paths_with_values[old_path] || paths_with_values['default']) : nil
77
+
78
+ return response if response
79
+
80
+ raise Amfetamine::ExternalConnectionsNotAllowed, "Tried to do #{method} with #{args}\n Allowed paths: \n #{paths_with_values.keys.join("\n")}"
81
+ else
82
+ super
83
+ end
84
+ end
85
+
86
+ end
87
+
88
+ class FakeResponse
89
+ def initialize(method, code2, block)
90
+ @method = method
91
+ @response_code = code2 || 200
92
+ @inner_body = block.call || {}
93
+ end
94
+
95
+ def code
96
+ @response_code
97
+ end
98
+
99
+ def body
100
+ MultiJson.encode(@inner_body) if @inner_body
101
+ end
102
+
103
+ def parsed_response
104
+ if body
105
+ MultiJson.decode(body)
106
+ else
107
+ {}
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+
@@ -0,0 +1,18 @@
1
+ require 'singleton'
2
+
3
+ module Amfetamine
4
+ class Logger
5
+ include Singleton
6
+
7
+ def method_missing(method, args)
8
+ args = "[Amfetamine] #{args.to_s}"
9
+ if defined?(Rails)
10
+ Rails.logger.send(method,args)
11
+ elsif defined?(Merb)
12
+ Merb.logger.send(method,args)
13
+ else
14
+ puts args
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,187 @@
1
+ require 'active_support/core_ext' # For to_query
2
+
3
+ module Amfetamine
4
+ module QueryMethods
5
+ # Base method for finding objects
6
+ # Should this be refactored to a different class that checks if cached and returns object?
7
+ # Caching methods are called here ONLY.
8
+ def self.included(base)
9
+ base.extend ClassMethods
10
+ end
11
+
12
+ module ClassMethods
13
+ def find(id, opts={})
14
+ begin
15
+ key = opts[:nested_path] || self.find_path(id)
16
+ data = get_data(key, opts[:conditions])
17
+ if data[:status] == :success
18
+ build_object(data[:body])
19
+ else
20
+ nil
21
+ end
22
+ rescue
23
+ clean_cache!
24
+ raise
25
+ end
26
+ end
27
+
28
+ def all(opts={})
29
+ begin
30
+ key = opts[:nested_path] || self.rest_path
31
+ data = get_data(key, opts[:conditions])
32
+
33
+ if data[:status] == :success
34
+ data[:body].compact.map { |d| build_object(d) }
35
+ else
36
+ []
37
+ end
38
+ rescue
39
+ clean_cache!
40
+ raise
41
+ end
42
+ end
43
+
44
+ def cache_conditions(key, condition=nil)
45
+ return nil unless condition
46
+ conditions = cache.get("#{key}_conditions") || []
47
+ q_condition = condition.to_query
48
+
49
+ if !conditions.include?(q_condition)
50
+ conditions << condition.to_query
51
+ cache.set("#{key}_conditions", conditions)
52
+ end
53
+ end
54
+
55
+ def create(args={})
56
+ self.new(args).tap do |obj|
57
+ obj.run_callbacks(:create) { obj.save }
58
+ end
59
+ end
60
+
61
+ def get_data(key, conditions=nil, method=:get)
62
+ if cacheable?
63
+ if conditions
64
+ cache_key = key + conditions.to_query
65
+ cache_conditions(key, conditions)
66
+ else
67
+ cache_key = key
68
+ end
69
+
70
+ Amfetamine.logger.info "Fetching object from cache: #{cache_key}"
71
+ cache.fetch(cache_key) do
72
+ Amfetamine.logger.info "Miss! #{cache_key}"
73
+ handle_request(method, key, { :query => conditions } )
74
+ end
75
+ else
76
+ handle_request(method,key, { :query => conditions })
77
+ end
78
+ end
79
+
80
+ def clean_cache!
81
+ if cacheable?
82
+ cache.delete(rest_path)
83
+ condition_keys = cache.get("#{rest_path}_conditions") || []
84
+ condition_keys.each do |cc|
85
+ cache.delete(rest_path + cc)
86
+ end
87
+ Amfetamine.logger.info "Cleaned cache for #{self.model_name}"
88
+ end
89
+ end
90
+ end
91
+
92
+ def save
93
+ if !valid?
94
+ return false
95
+ end
96
+
97
+ run_callbacks(:save) do
98
+ response = if self.new?
99
+ path = self.belongs_to_relationship? ? belongs_to_relationships.first.rest_path : rest_path
100
+ self.class.handle_request(:post, path, {:body => self.to_json })
101
+ else
102
+ # Needs cleaning up, also needs to work with multiple belongs_to relationships (optional, I guess)
103
+ path = self.belongs_to_relationship? ? belongs_to_relationships.first.singular_path : singular_path
104
+ self.class.handle_request(:put, path, {:body => self.to_json})
105
+ end
106
+
107
+ if handle_response(response)
108
+ begin
109
+ update_attributes_from_response(response[:body])
110
+ ensure
111
+ clean_cache!
112
+ end
113
+ cache.set(singular_path, self.to_cacheable) if cacheable?
114
+ end
115
+ end
116
+ end
117
+
118
+ def destroy
119
+ if self.new?
120
+ return false
121
+ end
122
+
123
+ path = self.belongs_to_relationship? ? belongs_to_relationships.first.singular_path : singular_path
124
+ response = self.class.handle_request(:delete, path)
125
+
126
+ if handle_response(response)
127
+ clean_cache!
128
+ self.notsaved = true # Because its a new object if the server side got deleted
129
+ self.id = nil # Not saved? No ID.
130
+ end
131
+ end
132
+
133
+ def clean_cache!
134
+ if cacheable?
135
+ cache.delete(singular_path)
136
+ cache.delete(rest_path)
137
+ belongs_to_relationships.each do |r|
138
+ cache.delete(r.singular_path)
139
+ cache.delete(r.rest_path)
140
+ condition_keys = cache.get("#{r.rest_path}_conditions") || []
141
+ condition_keys.each do |cc|
142
+ cache.delete(r.rest_path + cc)
143
+ end
144
+ end
145
+
146
+ condition_keys = cache.get("#{rest_path}_conditions") || []
147
+ condition_keys.each do |cc|
148
+ cache.delete(rest_path + cc)
149
+ end
150
+ Amfetamine.logger.info "Cleaned cache for #{self.class_name} with ID #{self.id}"
151
+ end
152
+ end
153
+
154
+
155
+
156
+
157
+ def update_attributes(attrs)
158
+ return true if attrs.all? { |k,v| self.send(k) == v } # Don't update if no attributes change
159
+ attrs.each { |k,v| self.send("#{k}=", v) }
160
+ self.save
161
+ end
162
+
163
+
164
+ def new?
165
+ @notsaved
166
+ end
167
+
168
+ def to_cacheable
169
+ {
170
+ :status => :success,
171
+ :body => {
172
+ self.class.name.downcase.to_sym => self.attributes
173
+ }
174
+ }
175
+ end
176
+
177
+ private
178
+
179
+ def id=(id)
180
+ @id = id
181
+ end
182
+
183
+ def notsaved=(bool)
184
+ @notsaved = bool
185
+ end
186
+ end
187
+ end