amfetamine 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ Guardfile
6
+ .yardoc
7
+ doc/
8
+ coverage/
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.3-p0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in amfetamine.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ Amfetamine, A REST object abstractavaganza
2
+ ====================================
3
+
4
+ > Makes your API calls f-f-f-ast!
5
+
6
+ Amfetamine adds an ActiveModel like interface to your REST services with support for any HTTParty client and caching using memcached.
7
+
8
+ Features
9
+ --------
10
+
11
+ It is still in beta and under heavy development. Some features:
12
+
13
+ * Mapping of REST services to objects.
14
+ * HTTP configuration agnostic; it works with any configuration of HTTParty. It should work with any object that has a similar syntax.
15
+ * Reasonably effective object caching with memcached.
16
+ * It has a lot of methods that you expect from ActiveModel objects. Find, all, save, create, new, destroy, update_attributes, update.
17
+ * It supports all validation methods ActiveModel provides, thanks to ActiveModel -red.
18
+ * You can fully configure your memcached client, it uses Dalli.
19
+ * Although the client has been tested and build with JSON, it should support XML as well.
20
+ * It supports single nested resources for now.
21
+ * It supports conditions in the all method (find method asap), just provice a :conditions hash like you're used to.
22
+ * Supports global HTTP and Memcached client as well as per object overriding.
23
+ * If a request passes validation on client side and not on service side, the client properly sets error messages from the service.
24
+ * Provides testing helpers
25
+ * Amfetamine supports some basic callbacks: before_save, after_save, around_save and before_create. More coming as needed.
26
+
27
+ Setup
28
+ =====
29
+
30
+ ### 1)
31
+ Add it to your gemfile (not released yet):
32
+
33
+ ```ruby
34
+ gem 'amfetamine'
35
+ ```
36
+
37
+ ### 2)
38
+ Create an initializer amfetamine_initializer.rb:
39
+
40
+ ```ruby
41
+ Amfetamine::Config.configure do |config|
42
+ config.memcached_instance = [HOST:PORT, OPTION1,OPTION2] || HOST:PORT
43
+ config.rest_client = REST_CLIENT
44
+
45
+ # Optional
46
+ config.resource_suffix = '.json' # The suffix to the path
47
+ config.base_uri = 'http://bla.bla/bla/' # If you either need a path or domain infront of your URI, you can do it here. Its advised to use httparty for this.
48
+ end
49
+ ```
50
+
51
+ ### 3)
52
+ Configure your object:
53
+
54
+ ```ruby
55
+ class Banana < Amfetamine::Base
56
+ # You need to setup an attribute for each attribute your object has, apart from id (thats _mandatory_)
57
+ amfetamine_attributes :name, :shape, :color, :created_at, :updated_at
58
+
59
+ # OPTIONAL: Per object configuration
60
+ amfetamine_configure memcached_instance: 'localhost:11211',
61
+ rest_client: BananaRestclient
62
+
63
+ end
64
+ ```
65
+
66
+
67
+ ### 4)
68
+ Lastly, because I think its more semantic, you need to configure both your service and client to include the root element in JSON. However, Amfetamine will work fine without this.
69
+
70
+ ```ruby
71
+ # config/initializers/wrap_parameters.rb
72
+ ActiveSupport.on_load(:action_controller) do
73
+ wrap_parameters :format => [:json]
74
+ end
75
+ ```
76
+
77
+ Usage
78
+ =====
79
+
80
+ ### Relationships
81
+
82
+ ```ruby
83
+ has_many_resources PLURAL_OBJECT_NAME_SYMBOLS
84
+ belongs_to_resource SINGULAR_OBJECT_NAME_SYMBOL
85
+
86
+ parent.children.all # => Returns all nested resources, you can enumarate it with each, include? and several other helpers are available
87
+ parent.children.all(:conditions => SOMETHING) # Works as expected
88
+ parent.children << child # Sets a child to a parent, child still needs to be saved. This will append it to the current all array and set the parent_id, accessing #all will overwrite that array.
89
+ parent.children.find(ID) # => Returns the nested child with ID
90
+ children.parent # => returns a Amfetamine::Relationship with only the parent
91
+ ```
92
+
93
+ ### Querying
94
+
95
+ ```ruby
96
+ Object.all # => returns all objects, request: /objects
97
+ Object.all(conditions: {:other_parent_id => 2} ) # => request: objects?other_parent_id=2
98
+ Object.find(ID) # => returns object with ID, request: objects/ID
99
+ ```
100
+
101
+ ### Modifying data
102
+
103
+ ```ruby
104
+ object.save
105
+ object.destroy
106
+ object.update_attributes(HASH)
107
+ Object.create(HASH)
108
+ ```
109
+
110
+ Cache Invalidation
111
+ =================
112
+
113
+ Objects are cached by request with the body as value. Request status codes are also cached. Every time an object is created, destroyed or updated, the plural cache is also invalidated.
114
+
115
+ You can invalidate an object's cache any time by calling `clean_cache!` on an object. You can flush the whole cache by calling flush on either a class or Amfetamine::Cache.
116
+
117
+ Testing
118
+ =======
119
+
120
+ Amfetamine provides a testing helper to easilly stub out responses from external services, so you can better control what response you get.
121
+
122
+ ```ruby
123
+ # Rspec:
124
+ before do
125
+ AmfetamineObject.stub_responses! do |r|
126
+ # Setting the code / path is optional. If amfetamine picks the wrong path, this will give you some weird errors.
127
+ r.post(path: '/bananas/', code: 201) { some_object }
128
+ r.get { some_object } # this sets to the default for gets on the rest_client this object uses
129
+ end
130
+ end
131
+ ```
132
+
133
+ Also, if you're using a cache, you should flush the cache before each test to avoid confusion.
134
+
135
+ Building Custom Methods
136
+ =======================
137
+
138
+ Its important to note that caching might not work as expected when building custom methods. For now, please refer to the code.
139
+
140
+ Testing
141
+ =======
142
+
143
+ Amfetamine provides several testing helpers to make testing easier. I didn't think it would be wise to allow external connections, but I didn't want you to have to stub out all methods either.
144
+
145
+ ```ruby
146
+ Object.prevent_external_connections! # Raises an error if any external connections are made on this object
147
+
148
+ # You can provide a block as well, after the block the rest_client is set back to the default:
149
+ Object.prevent_external_connections! do |rest_client|
150
+ rest_client.should_receive('get').with('/objects').and_return(objects.to_json)
151
+ Object.all
152
+ end
153
+
154
+ # You can also use a dsl to predefine responses up front on a per object basis. You can use rspec 'let' objects in the response as well.
155
+ Object.stub_external_responses! do |r|
156
+ r.get {object}
157
+ end
158
+
159
+ r.all # => object, also will yield an error, expects an array
160
+ r.find(1) # => object
161
+
162
+ # You can go wilder with this as well so you can allow multiple requests. You can also use this dsl on the rest_client in #prevent_external_connections!
163
+ Object.stub_external_responses! do |r|
164
+ r.get(path: '/objects') { [object] } # Returns [object].to_json
165
+ r.get(path: "/objects/#{object.id}", code: 404) {} # Returns a resource not found
166
+ end
167
+ ```
168
+
169
+ TODO
170
+
171
+ Future Features
172
+ ===============
173
+
174
+ * Smarter caching
175
+ * Better support for custom methods
176
+ * Support for Reddis
177
+ * Automatic determining of attributes and validations
178
+ * Supporting any amount of nested relationships
179
+ * Supporting interobject relationship (database versus service)
180
+ * More callbacks
181
+ * Better typecasting, it doesn't always work.
182
+
183
+ Licence
184
+ =======
185
+
186
+ TBD
187
+
188
+ Contributing
189
+ ============
190
+
191
+ Please do a pull request and test your code!
192
+
193
+ Contributors
194
+ ============
195
+
196
+ * Timon Vonk
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ task :default => [:bundle_dependencies, :rspec]
4
+
5
+ task :bundle_dependencies do
6
+ sh "bundle"
7
+ end
8
+
9
+ task :rspec do
10
+ sh "rspec spec"
11
+ end
@@ -0,0 +1,40 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "amfetamine/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "amfetamine"
7
+ s.version = Amfetamine::VERSION
8
+ s.authors = ["Timon Vonk"]
9
+ s.email = ["timon@exvo.com"]
10
+ s.homepage = "http://www.github.com/exvo/amfetamine"
11
+ s.summary = %q{Wraps REST to objects}
12
+ s.description = %q{Wraps REST to objects, provides caching and makes your app go Bzzz!}
13
+
14
+ s.rubyforge_project = "amfetamine"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # Development dependencies
22
+ s.add_development_dependency "rspec"
23
+ s.add_development_dependency "guard"
24
+ s.add_development_dependency "guard-rspec"
25
+ #s.add_development_dependency "growl_notify"
26
+ #s.add_development_dependency "rb-fsevent"
27
+ s.add_development_dependency "growl"
28
+ s.add_development_dependency "httparty"
29
+ s.add_development_dependency "fakeweb"
30
+ s.add_development_dependency "simplecov"
31
+ s.add_development_dependency "simplecov-rcov"
32
+
33
+
34
+ # Runtime dependencies
35
+ s.add_runtime_dependency "dalli"
36
+ s.add_runtime_dependency "activesupport" # For helper methods
37
+ s.add_runtime_dependency "activemodel" # For validations and AM like behaviour
38
+ s.add_runtime_dependency "json"
39
+ s.add_runtime_dependency "rake"
40
+ end
data/lib/amfetamine.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "amfetamine/version"
2
+ require "amfetamine/helpers/test_helpers" # Testing helper methods
3
+ require "amfetamine/exceptions"
4
+ require "amfetamine/logger"
5
+ require 'amfetamine/relationship'
6
+ require "amfetamine/relationships"
7
+ require "amfetamine/caching_adapter" # Adapter that wraps memcache methods
8
+ require "amfetamine/cache" # Common caching methods
9
+ require "amfetamine/rest_helpers" # Methods for determining REST paths
10
+ require "amfetamine/query_methods" # Methods for interfacing with the classs
11
+ require "amfetamine/base" # Basics
12
+ require "amfetamine/config" # Configuration class
13
+
14
+ module Amfetamine
15
+
16
+ def self.logger
17
+ Amfetamine::Logger.instance
18
+ end
19
+ end
@@ -0,0 +1,189 @@
1
+ require 'active_model'
2
+
3
+ module Amfetamine
4
+ class Base
5
+ # Activemodel
6
+ extend ActiveModel::Naming
7
+ extend ActiveModel::Callbacks
8
+ include ActiveModel::Validations
9
+ include ActiveModel::Serialization
10
+ include ActiveModel::Serializers::JSON
11
+
12
+ #Callbacks
13
+ define_model_callbacks :create, :save
14
+
15
+
16
+ # amfetamine
17
+ include Amfetamine::RestHelpers
18
+ include Amfetamine::QueryMethods
19
+ include Amfetamine::Relationships
20
+
21
+ # Testing
22
+ include Amfetamine::TestHelpers
23
+
24
+
25
+ attr_reader :attributes
26
+
27
+ def id=(val)
28
+ @attributes['id'] = val
29
+ end
30
+
31
+ def id
32
+ @attributes['id']
33
+ end
34
+
35
+ private :'id='
36
+
37
+
38
+
39
+ def self.amfetamine_attributes(*attrs)
40
+ attrs.each do |attr|
41
+ define_method("#{attr}=") do |arg|
42
+ @attributes[attr.to_s] = arg
43
+ end
44
+
45
+ define_method("#{attr}") do
46
+ @attributes[attr.to_s]
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.amfetamine_configure(hash)
52
+ hash.each do |k,v|
53
+ self.send("#{k.to_s}=", v)
54
+ end
55
+ end
56
+
57
+ # Builds an object from JSON, later on will need more (maybe object id? Or should that go in find?)
58
+ # It parses the hash, builds the objects and sets new to false
59
+ def self.build_object(args)
60
+ # Cache corruption guard
61
+ args = normalize_cache_data(args)
62
+
63
+ obj = self.new(args)
64
+ obj.tap { |obj| obj.instance_variable_set('@notsaved',false) } # because I don't want a global writer
65
+ end
66
+
67
+ def update_attributes_from_response(args)
68
+ # We need to check this. If an api provides new data after an update, it will be set :-)
69
+ # Some apis return "nil" or something like that, so we need to double check its a hash
70
+
71
+ # TODO: Remove if statement because validation has been added
72
+ if args && args.is_a?(Hash) && args.has_key?(self.class_name)
73
+ args = args[self.class_name]
74
+ args.each { |k,v| self.send("#{k}=", v); self.attributes[k.to_sym] = v }
75
+ end
76
+ end
77
+
78
+ # Allows you to override the global caching server
79
+ def self.memcached_instance=(value, options={})
80
+ if value.is_a?(Array)
81
+ @cache_server = Amfetamine::Cache.new(value.shift, value.first) # First element is the server, second must be the options
82
+ else
83
+ @cache_server = Amfetamine::Cache.new(value, options)
84
+ end
85
+ end
86
+
87
+ # Base method for creating objects
88
+ def initialize(args={})
89
+ super
90
+ @attributes = {}
91
+ args.each { |k,v| self.send("#{k}=", v) }
92
+ @notsaved = true
93
+ self
94
+ end
95
+
96
+ def is_attribute?(attr)
97
+ @attributes.keys.include?(attr.to_sym)
98
+ end
99
+
100
+ def persisted?
101
+ !new?
102
+ end
103
+
104
+ def to_model
105
+ self
106
+ end
107
+
108
+ def to_json(*gen)
109
+ options = {}
110
+ options.merge!(:root => self.class.model_name.element)
111
+ super(self.as_json(options))
112
+ end
113
+
114
+ def to_key
115
+ persisted? ? [id] : nil
116
+ end
117
+
118
+ def to_param
119
+ persisted? ? id.to_s : nil
120
+ end
121
+
122
+ # Checks if object is cached
123
+ # TODO this is not very efficient, but dalli doesn't provide a polling function :(
124
+ def cached?
125
+ keys = belongs_to_relationships.collect { |r| r.singular_path } << self.singular_path
126
+ keys.any? { |k| cache.get(k) }
127
+ end
128
+
129
+ # Checks if object is cachable
130
+ # TODO implement
131
+ def self.cacheable?
132
+ true
133
+ end
134
+
135
+ def cacheable?
136
+ self.class.cacheable?
137
+ end
138
+
139
+ # Checks to see if an object is valid or not
140
+ def valid?
141
+ errors.clear
142
+ run_validations!
143
+ end
144
+
145
+ # We need to redefine this so it doesn't check on object_id
146
+ def ==(other)
147
+ return false unless self.id == other.id # Some APIs dont ALWAYS return an ID
148
+
149
+ self.attributes.all? do |k,v|
150
+ self.attributes[k] == other.attributes[k]
151
+ end
152
+ end
153
+
154
+ def errors
155
+ @errors ||= ActiveModel::Errors.new(self)
156
+ end
157
+
158
+
159
+
160
+ def class_name
161
+ self.class.class_name
162
+ end
163
+
164
+ def self.class_name
165
+ self.name.downcase
166
+ end
167
+
168
+ protected
169
+ def self.cache
170
+ @cache_server || Amfetamine::Cache
171
+ end
172
+
173
+ def cache
174
+ self.class.cache
175
+ end
176
+
177
+ # TODO: Refactor > cache, only cache should know if data is valid.
178
+ def self.normalize_cache_data(args)
179
+ # Validation predicates
180
+ raise InvalidCacheData, "Empty data" if args.nil?
181
+ raise InvalidCacheData, "Invalid data: #{args.to_s}" if !args.is_a?(Hash)
182
+ args.stringify_keys!
183
+ args = args[class_name] || args
184
+ # TODO remove [:id], stringify_keys! _should_ nail this.
185
+ raise InvalidCacheData, "No object or ID #{args}" unless args.present? && (args["id"] || args[:id])
186
+ args
187
+ end
188
+ end
189
+ end