her 0.3.1 → 0.3.2

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/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