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.
- data/.gitignore +8 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.md +196 -0
- data/Rakefile +11 -0
- data/amfetamine.gemspec +40 -0
- data/lib/amfetamine.rb +19 -0
- data/lib/amfetamine/base.rb +189 -0
- data/lib/amfetamine/cache.rb +9 -0
- data/lib/amfetamine/caching_adapter.rb +66 -0
- data/lib/amfetamine/config.rb +34 -0
- data/lib/amfetamine/exceptions.rb +9 -0
- data/lib/amfetamine/helpers/rspec_matchers.rb +5 -0
- data/lib/amfetamine/helpers/test_helpers.rb +113 -0
- data/lib/amfetamine/logger.rb +18 -0
- data/lib/amfetamine/query_methods.rb +187 -0
- data/lib/amfetamine/relationship.rb +108 -0
- data/lib/amfetamine/relationships.rb +77 -0
- data/lib/amfetamine/rest_helpers.rb +122 -0
- data/lib/amfetamine/version.rb +3 -0
- data/spec/amfetamine/base_spec.rb +207 -0
- data/spec/amfetamine/caching_spec.rb +37 -0
- data/spec/amfetamine/callbacks_spec.rb +36 -0
- data/spec/amfetamine/conditions_spec.rb +110 -0
- data/spec/amfetamine/dummy_spec.rb +27 -0
- data/spec/amfetamine/relationships_spec.rb +103 -0
- data/spec/amfetamine/rest_helpers_spec.rb +25 -0
- data/spec/amfetamine/rspec_matchers_spec.rb +7 -0
- data/spec/amfetamine/testing_helpers_spec.rb +101 -0
- data/spec/dummy/child.rb +25 -0
- data/spec/dummy/configure.rb +4 -0
- data/spec/dummy/dummy.rb +48 -0
- data/spec/dummy/dummy_rest_client.rb +6 -0
- data/spec/helpers/active_model_lint.rb +21 -0
- data/spec/helpers/fakeweb_responses.rb +120 -0
- data/spec/spec_helper.rb +33 -0
- metadata +246 -0
@@ -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,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
|