amfetamine 0.1.5

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.
@@ -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