her 0.3.1 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Her [![Build Status](https://secure.travis-ci.org/remiprev/her.png)](http://travis-ci.org/remiprev/her) [![Gem dependency status](https://gemnasium.com/remiprev/her.png?travis)](https://gemnasium.com/remiprev/her)
1
+ # Her [![Build Status](https://secure.travis-ci.org/remiprev/her.png?branch=master)](http://travis-ci.org/remiprev/her) [![Gem dependency status](https://gemnasium.com/remiprev/her.png?travis)](https://gemnasium.com/remiprev/her)
2
2
 
3
3
  Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects. It is designed to build applications that are powered by a RESTful API instead of a database.
4
4
 
@@ -75,6 +75,7 @@ For example, to add a API token header to your requests, you would do something
75
75
  ```ruby
76
76
  class TokenAuthentication < Faraday::Middleware
77
77
  def initialize(app, options={})
78
+ @app = app
78
79
  @options = options
79
80
  end
80
81
 
@@ -549,6 +550,8 @@ These fine folks helped with Her:
549
550
  * [@tysontate](https://github.com/tysontate)
550
551
  * [@nfo](https://github.com/nfo)
551
552
  * [@simonprevost](https://github.com/simonprevost)
553
+ * [@jmlacroix](https://github.com/jmlacroix)
554
+ * [@thomsbg](https://github.com/thomsbg)
552
555
 
553
556
  ## License
554
557
 
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require "bundler"
2
- Bundler.require :development
3
-
2
+ require "rake"
3
+ require "yard"
4
4
  require "bundler/gem_tasks"
5
5
  require "rspec/core/rake_task"
6
6
 
@@ -23,11 +23,11 @@ Gem::Specification.new do |s|
23
23
  s.add_development_dependency "redcarpet", "~> 2.1"
24
24
  s.add_development_dependency "mocha", "~> 0.12"
25
25
  s.add_development_dependency "guard", "~> 1.2"
26
- s.add_development_dependency "guard-rspec", "~> 0.7"
26
+ s.add_development_dependency "guard-rspec", "~> 1.2"
27
27
  s.add_development_dependency "rb-fsevent", "~> 0.9"
28
28
  s.add_development_dependency "growl", "~> 1.0"
29
29
 
30
- s.add_runtime_dependency "activesupport"
30
+ s.add_runtime_dependency "activesupport", ">= 3.0.0"
31
31
  s.add_runtime_dependency "faraday", "~> 0.8"
32
32
  s.add_runtime_dependency "multi_json", "~> 1.3"
33
33
  end
@@ -29,15 +29,13 @@ module Her
29
29
  included do
30
30
  extend Her::Model::Base
31
31
  extend Her::Model::HTTP
32
- extend Her::Model::ORM
33
32
  extend Her::Model::Relationships
34
33
  extend Her::Model::Hooks
35
- extend Her::Model::Paths
36
34
 
37
35
  # Define default settings
38
36
  base_path = self.name.split("::").last.underscore.pluralize
39
- collection_path "/#{base_path}"
40
- resource_path "/#{base_path}/:id"
37
+ collection_path "#{base_path}"
38
+ resource_path "#{base_path}/:id"
41
39
  uses_api Her::API.default_api
42
40
  end
43
41
  end
@@ -59,23 +59,26 @@ module Her
59
59
  perform_after_hooks(resource, *hooks.reverse)
60
60
  end # }}}
61
61
 
62
- private
63
62
  # @private
64
63
  def hooks # {{{
65
- @her_hooks
64
+ @her_hooks ||= begin
65
+ if superclass.respond_to?(:hooks)
66
+ superclass.hooks.dup
67
+ else
68
+ {}
69
+ end
70
+ end
66
71
  end # }}}
67
72
 
73
+ private
68
74
  # @private
69
75
  def set_hook(time, name, action) # {{{
70
- @her_hooks ||= {}
71
- (@her_hooks["#{time}_#{name}".to_sym] ||= []) << action
76
+ (self.hooks["#{time}_#{name}".to_sym] ||= []) << action
72
77
  end # }}}
73
78
 
74
79
  # @private
75
80
  def perform_hook(record, time, name) # {{{
76
- @her_hooks ||= {}
77
- hooks = @her_hooks["#{time}_#{name}".to_sym] || []
78
- hooks.each do |hook|
81
+ Array(self.hooks["#{time}_#{name}".to_sym]).each do |hook|
79
82
  if hook.is_a? Symbol
80
83
  record.send(hook)
81
84
  else
@@ -2,6 +2,13 @@ module Her
2
2
  module Model
3
3
  # This module interacts with Her::API to fetch HTTP data
4
4
  module HTTP
5
+ # Automatically inherit a superclass' api
6
+ def her_api # {{{
7
+ @her_api ||= begin
8
+ superclass.her_api if superclass.respond_to?(:her_api)
9
+ end
10
+ end # }}}
11
+
5
12
  # Link a model with a Her::API object
6
13
  def uses_api(api) # {{{
7
14
  @her_api = api
@@ -9,8 +16,13 @@ module Her
9
16
 
10
17
  # Main request wrapper around Her::API. Used to make custom request to the API.
11
18
  # @private
12
- def request(attrs={}, &block) # {{{
13
- yield @her_api.request(attrs)
19
+ def request(attrs={}) # {{{
20
+ parsed_data = her_api.request(attrs)
21
+ if block_given?
22
+ yield parsed_data
23
+ else
24
+ parsed_data
25
+ end
14
26
  end # }}}
15
27
 
16
28
  # Make a GET request and return either a collection or a resource
@@ -2,32 +2,6 @@ module Her
2
2
  module Model
3
3
  module Introspection
4
4
  extend ActiveSupport::Concern
5
-
6
- module ClassMethods
7
- # Finds a class at the same level as this one or at the global level.
8
- def nearby_class(name)
9
- sibling_class(name) || name.constantize rescue nil
10
- end
11
-
12
- protected
13
- # Looks for a class at the same level as this one with the given name.
14
- # @private
15
- def sibling_class(name)
16
- if mod = self.containing_module
17
- "#{mod.name}::#{name}".constantize rescue nil
18
- else
19
- name.constantize rescue nil
20
- end
21
- end
22
-
23
- # If available, returns the containing Module for this class.
24
- # @private
25
- def containing_module # {{{
26
- return unless self.name =~ /::/
27
- self.name.split("::")[0..-2].join("::").constantize
28
- end # }}}
29
- end
30
-
31
5
  # Inspect an element, returns it for introspection.
32
6
  #
33
7
  # @example
@@ -38,7 +12,7 @@ module Her
38
12
  # @user = User.find(1)
39
13
  # p @user # => #<User(/users/1) id=1 name="Tobias Fünke">
40
14
  def inspect # {{{
41
- "#<#{self.class}(#{self.class.build_request_path(@data)}) #{@data.inject([]) { |memo, item| key, value = item; memo << "#{key}=#{attribute_for_inspect(value)}"}.join(" ")}>"
15
+ "#<#{self.class}(#{request_path}) #{@data.inject([]) { |memo, item| key, value = item; memo << "#{key}=#{attribute_for_inspect(value)}"}.join(" ")}>"
42
16
  end # }}}
43
17
 
44
18
  private
@@ -52,6 +26,32 @@ module Her
52
26
  value.inspect
53
27
  end
54
28
  end # }}}
29
+
30
+ module ClassMethods
31
+ # Finds a class at the same level as this one or at the global level.
32
+ # @private
33
+ def nearby_class(name) # {{{
34
+ sibling_class(name) || name.constantize rescue nil
35
+ end # }}}
36
+
37
+ protected
38
+ # Looks for a class at the same level as this one with the given name.
39
+ # @private
40
+ def sibling_class(name) # {{{
41
+ if mod = self.containing_module
42
+ "#{mod.name}::#{name}".constantize rescue nil
43
+ else
44
+ name.constantize rescue nil
45
+ end
46
+ end # }}}
47
+
48
+ # If available, returns the containing Module for this class.
49
+ # @private
50
+ def containing_module # {{{
51
+ return unless self.name =~ /::/
52
+ self.name.split("::")[0..-2].join("::").constantize
53
+ end # }}}
54
+ end
55
55
  end
56
56
  end
57
57
  end
@@ -2,7 +2,8 @@ module Her
2
2
  module Model
3
3
  # This module adds ORM-like capabilities to the model
4
4
  module ORM
5
- attr_reader :metadata, :errors
5
+ extend ActiveSupport::Concern
6
+ attr_accessor :data, :metadata, :errors
6
7
 
7
8
  # Initialize a new object with data received from an HTTP request
8
9
  # @private
@@ -10,10 +11,12 @@ module Her
10
11
  @data = {}
11
12
  @metadata = data.delete(:_metadata) || {}
12
13
  @errors = data.delete(:_errors) || {}
14
+
15
+ # Only keep the keys that don't have corresponding writer methods
13
16
  cleaned_data = data.inject({}) do |memo, item|
14
17
  key, value = item
15
18
  send "#{key}=".to_sym, value unless value.nil?
16
- respond_to?("#{key}=") ? memo : memo.merge({ key => value })
19
+ writer_method_defined?(key) ? memo : memo.merge({ key => value })
17
20
  end
18
21
  @data.merge! self.class.parse_relationships(cleaned_data)
19
22
  end # }}}
@@ -27,34 +30,29 @@ module Her
27
30
 
28
31
  # Handles missing methods by routing them through @data
29
32
  # @private
30
- def method_missing(method, attrs=nil) # {{{
31
- assignment_method = method.to_s =~ /\=$/
32
- method = method.to_s.gsub(/(\?|\!|\=)$/, "").to_sym
33
- if !attrs.nil? and assignment_method
34
- @data ||= {}
35
- @data[method.to_s.gsub(/\=$/, "").to_sym] = attrs
33
+ def method_missing(method, *args, &blk) # {{{
34
+ if method.to_s.end_with?('=')
35
+ @data[method.to_s.chomp('=').to_sym] = args.first
36
+ elsif method.to_s.end_with?('?')
37
+ @data.include?(method.to_s.chomp('?').to_sym)
38
+ elsif @data.include?(method)
39
+ @data[method]
36
40
  else
37
- if @data and @data.include?(method)
38
- @data[method]
39
- else
40
- super
41
- end
41
+ super
42
42
  end
43
43
  end # }}}
44
44
 
45
+ # Handles returning true for the cases handled by method_missing
46
+ def respond_to?(method, include_private = false) # {{{
47
+ method.to_s.end_with?('=') || method.to_s.end_with?('?') || @data.include?(method) || super
48
+ end # }}}
49
+
45
50
  # Override the method to prevent from returning the object ID (in ruby-1.8.7)
46
51
  # @private
47
52
  def id # {{{
48
53
  @data[:id] || super
49
54
  end # }}}
50
55
 
51
- # Initialize a collection of resources with raw data from an HTTP request
52
- #
53
- # @param [Array] parsed_data
54
- def new_collection(parsed_data) # {{{
55
- Her::Model::ORM.initialize_collection(self, parsed_data)
56
- end # }}}
57
-
58
56
  # Return `true` if a resource was not saved yet
59
57
  def new? # {{{
60
58
  !@data.include?(:id)
@@ -70,56 +68,20 @@ module Her
70
68
  @errors.any?
71
69
  end # }}}
72
70
 
73
- # Fetch a specific resource based on an ID
74
- #
75
- # @example
76
- # @user = User.find(1)
77
- # # Fetched via GET "/users/1"
78
- def find(id, params={}) # {{{
79
- request(params.merge(:_method => :get, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data|
80
- new(parsed_data[:data])
81
- end
71
+ # Return `true` if the other object is also a Her::Model and has matching data
72
+ def ==(other) # {{{
73
+ other.is_a?(Her::Model) && @data == other.data
82
74
  end # }}}
83
75
 
84
- # Fetch a collection of resources
85
- #
86
- # @example
87
- # @users = User.all
88
- # # Fetched via GET "/users"
89
- def all(params={}) # {{{
90
- request(params.merge(:_method => :get, :_path => "#{build_request_path(params)}")) do |parsed_data|
91
- new_collection(parsed_data)
92
- end
76
+ # Delegate to the == method
77
+ def eql?(other) # {{{
78
+ self == other
93
79
  end # }}}
94
80
 
95
- # Create a resource and return it
96
- #
97
- # @example
98
- # @user = User.create({ :fullname => "Tobias Fünke" })
99
- # # Called via POST "/users/1"
100
- def create(params={}) # {{{
101
- resource = new(params)
102
- wrap_in_hooks(resource, :create, :save) do |resource, klass|
103
- params = resource.instance_eval { @data }
104
- request(params.merge(:_method => :post, :_path => "#{build_request_path(params)}")) do |parsed_data|
105
- resource.instance_eval do
106
- @data = parsed_data[:data]
107
- @metadata = parsed_data[:metadata]
108
- @errors = parsed_data[:errors]
109
- end
110
- end
111
- end
112
- resource
113
- end # }}}
114
-
115
- # Save an existing resource and return it
116
- #
117
- # @example
118
- # @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
119
- # # Called via PUT "/users/1"
120
- def save_existing(id, params) # {{{
121
- resource = new(params.merge(:id => id))
122
- resource.save
81
+ # Delegate to @data, allowing models to act correctly in code like:
82
+ # [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
83
+ def hash # {{{
84
+ @data.hash
123
85
  end # }}}
124
86
 
125
87
  # Save a resource
@@ -136,7 +98,7 @@ module Her
136
98
  # @user.save
137
99
  # # Called via POST "/users"
138
100
  def save # {{{
139
- params = @data.dup
101
+ params = to_params
140
102
  resource = self
141
103
 
142
104
  if @data[:id]
@@ -149,9 +111,9 @@ module Her
149
111
 
150
112
  self.class.wrap_in_hooks(resource, *hooks) do |resource, klass|
151
113
  klass.request(params.merge(:_method => method, :_path => "#{request_path}")) do |parsed_data|
152
- @data = parsed_data[:data]
153
- @metadata = parsed_data[:metadata]
154
- @errors = parsed_data[:errors]
114
+ self.data = parsed_data[:data]
115
+ self.metadata = parsed_data[:metadata]
116
+ self.errors = parsed_data[:errors]
155
117
  end
156
118
  end
157
119
  self
@@ -167,24 +129,114 @@ module Her
167
129
  resource = self
168
130
  self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
169
131
  klass.request(:_method => :delete, :_path => "#{request_path}") do |parsed_data|
170
- @data = parsed_data[:data]
171
- @metadata = parsed_data[:metadata]
172
- @errors = parsed_data[:errors]
132
+ self.data = parsed_data[:data]
133
+ self.metadata = parsed_data[:metadata]
134
+ self.errors = parsed_data[:errors]
173
135
  end
174
136
  end
175
137
  self
176
138
  end # }}}
177
139
 
178
- # Destroy an existing resource
140
+ # Convert into a hash of request parameters
179
141
  #
180
142
  # @example
181
- # User.destroy_existing(1)
182
- # # Called via DELETE "/users/1"
183
- def destroy_existing(id, params={}) # {{{
184
- request(params.merge(:_method => :delete, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data|
185
- new(parsed_data[:data])
186
- end
143
+ # @user.to_params
144
+ # # => { :id => 1, :name => 'John Smith' }
145
+ def to_params # {{{
146
+ @data.dup
147
+ end # }}}
148
+
149
+ private
150
+
151
+ # @private
152
+ def writer_method_defined?(key) # {{{
153
+ self.class.instance_methods.include?("#{key}=".to_sym) || # Ruby 1.9
154
+ self.class.instance_methods.include?("#{key}=") # Ruby 1.8
187
155
  end # }}}
156
+
157
+ module ClassMethods
158
+ # Initialize a collection of resources with raw data from an HTTP request
159
+ #
160
+ # @param [Array] parsed_data
161
+ def new_collection(parsed_data) # {{{
162
+ Her::Model::ORM.initialize_collection(self, parsed_data)
163
+ end # }}}
164
+
165
+ # Fetch specific resource(s) by their ID
166
+ #
167
+ # @example
168
+ # @user = User.find(1)
169
+ # # Fetched via GET "/users/1"
170
+ #
171
+ # @example
172
+ # @users = User.find([1, 2])
173
+ # # Fetched via GET "/users/1" and GET "/users/2"
174
+ def find(*ids) # {{{
175
+ params = ids.last.is_a?(Hash) ? ids.pop : {}
176
+ results = ids.flatten.compact.uniq.map do |id|
177
+ request(params.merge(:_method => :get, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data|
178
+ new(parsed_data[:data])
179
+ end
180
+ end
181
+ if ids.length > 1 || ids.first.kind_of?(Array)
182
+ results
183
+ else
184
+ results.first
185
+ end
186
+ end # }}}
187
+
188
+ # Fetch a collection of resources
189
+ #
190
+ # @example
191
+ # @users = User.all
192
+ # # Fetched via GET "/users"
193
+ def all(params={}) # {{{
194
+ request(params.merge(:_method => :get, :_path => "#{build_request_path(params)}")) do |parsed_data|
195
+ new_collection(parsed_data)
196
+ end
197
+ end # }}}
198
+
199
+ # Create a resource and return it
200
+ #
201
+ # @example
202
+ # @user = User.create({ :fullname => "Tobias Fünke" })
203
+ # # Called via POST "/users/1"
204
+ def create(params={}) # {{{
205
+ resource = new(params)
206
+ wrap_in_hooks(resource, :create, :save) do |resource, klass|
207
+ params = resource.to_params
208
+ request(params.merge(:_method => :post, :_path => "#{build_request_path(params)}")) do |parsed_data|
209
+ resource.instance_eval do
210
+ @data = parsed_data[:data]
211
+ @metadata = parsed_data[:metadata]
212
+ @errors = parsed_data[:errors]
213
+ end
214
+ end
215
+ end
216
+ resource
217
+ end # }}}
218
+
219
+ # Save an existing resource and return it
220
+ #
221
+ # @example
222
+ # @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
223
+ # # Called via PUT "/users/1"
224
+ def save_existing(id, params) # {{{
225
+ resource = new(params.merge(:id => id))
226
+ resource.save
227
+ end # }}}
228
+
229
+ # Destroy an existing resource
230
+ #
231
+ # @example
232
+ # User.destroy_existing(1)
233
+ # # Called via DELETE "/users/1"
234
+ def destroy_existing(id, params={}) # {{{
235
+ request(params.merge(:_method => :delete, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data|
236
+ new(parsed_data[:data])
237
+ end
238
+ end # }}}
239
+ end
188
240
  end
189
241
  end
190
242
  end