her 0.3.3 → 0.3.4
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/Rakefile +4 -4
- data/lib/her/api.rb +8 -8
- data/lib/her/collection.rb +2 -2
- data/lib/her/middleware/accept_json.rb +4 -4
- data/lib/her/middleware/first_level_parse_json.rb +4 -4
- data/lib/her/middleware/second_level_parse_json.rb +4 -4
- data/lib/her/model/hooks.rb +12 -12
- data/lib/her/model/http.rb +56 -56
- data/lib/her/model/introspection.rb +13 -13
- data/lib/her/model/orm.rb +85 -59
- data/lib/her/model/paths.rb +8 -8
- data/lib/her/model/relationships.rb +15 -13
- data/lib/her/version.rb +1 -1
- data/spec/api_spec.rb +16 -16
- data/spec/middleware/accept_json_spec.rb +2 -2
- data/spec/middleware/first_level_parse_json_spec.rb +4 -4
- data/spec/middleware/second_level_parse_json_spec.rb +4 -4
- data/spec/model/hooks_spec.rb +66 -66
- data/spec/model/http_spec.rb +56 -56
- data/spec/model/introspection_spec.rb +12 -12
- data/spec/model/orm_spec.rb +113 -94
- data/spec/model/paths_spec.rb +74 -74
- data/spec/model/relationships_spec.rb +52 -52
- metadata +5 -5
@@ -11,13 +11,13 @@ module Her
|
|
11
11
|
#
|
12
12
|
# @user = User.find(1)
|
13
13
|
# p @user # => #<User(/users/1) id=1 name="Tobias Fünke">
|
14
|
-
def inspect
|
14
|
+
def inspect
|
15
15
|
"#<#{self.class}(#{request_path}) #{@data.inject([]) { |memo, item| key, value = item; memo << "#{key}=#{attribute_for_inspect(value)}"}.join(" ")}>"
|
16
|
-
end
|
16
|
+
end
|
17
17
|
|
18
18
|
private
|
19
19
|
# @private
|
20
|
-
def attribute_for_inspect(value)
|
20
|
+
def attribute_for_inspect(value)
|
21
21
|
if value.is_a?(String) && value.length > 50
|
22
22
|
"#{value[0..50]}...".inspect
|
23
23
|
elsif value.is_a?(Date) || value.is_a?(Time)
|
@@ -25,32 +25,32 @@ module Her
|
|
25
25
|
else
|
26
26
|
value.inspect
|
27
27
|
end
|
28
|
-
end
|
28
|
+
end
|
29
29
|
|
30
30
|
module ClassMethods
|
31
31
|
# Finds a class at the same level as this one or at the global level.
|
32
32
|
# @private
|
33
|
-
def nearby_class(name)
|
33
|
+
def nearby_class(name)
|
34
34
|
sibling_class(name) || name.constantize rescue nil
|
35
|
-
end
|
35
|
+
end
|
36
36
|
|
37
37
|
protected
|
38
38
|
# Looks for a class at the same level as this one with the given name.
|
39
39
|
# @private
|
40
|
-
def sibling_class(name)
|
40
|
+
def sibling_class(name)
|
41
41
|
if mod = self.containing_module
|
42
|
-
|
43
|
-
|
44
|
-
name.constantize rescue nil
|
42
|
+
@sibling_class ||= {}
|
43
|
+
@sibling_class[mod] ||= {}
|
44
|
+
@sibling_class[mod][name] ||= "#{mod.name}::#{name}".constantize rescue nil
|
45
45
|
end
|
46
|
-
end
|
46
|
+
end
|
47
47
|
|
48
48
|
# If available, returns the containing Module for this class.
|
49
49
|
# @private
|
50
|
-
def containing_module
|
50
|
+
def containing_module
|
51
51
|
return unless self.name =~ /::/
|
52
52
|
self.name.split("::")[0..-2].join("::").constantize
|
53
|
-
end
|
53
|
+
end
|
54
54
|
end
|
55
55
|
end
|
56
56
|
end
|
data/lib/her/model/orm.rb
CHANGED
@@ -6,31 +6,44 @@ module Her
|
|
6
6
|
attr_accessor :data, :metadata, :errors
|
7
7
|
|
8
8
|
# Initialize a new object with data received from an HTTP request
|
9
|
-
|
10
|
-
def initialize(data={}) # {{{
|
9
|
+
def initialize(params={})
|
11
10
|
@data = {}
|
12
|
-
@metadata =
|
13
|
-
@errors =
|
14
|
-
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@data.merge! self.class.parse_relationships(cleaned_data)
|
22
|
-
end # }}}
|
11
|
+
@metadata = params.delete(:_metadata) || {}
|
12
|
+
@errors = params.delete(:_errors) || {}
|
13
|
+
|
14
|
+
# Use setter methods first, then translate attributes of relationships
|
15
|
+
# into relationship instances, then merge the parsed_data into @data.
|
16
|
+
unset_data = Her::Model::ORM.use_setter_methods(self, params)
|
17
|
+
parsed_data = self.class.parse_relationships(unset_data)
|
18
|
+
@data.update(parsed_data)
|
19
|
+
end
|
23
20
|
|
24
21
|
# Initialize a collection of resources
|
25
22
|
# @private
|
26
|
-
def self.initialize_collection(klass, parsed_data={})
|
23
|
+
def self.initialize_collection(klass, parsed_data={})
|
27
24
|
collection_data = parsed_data[:data].map { |item_data| klass.new(item_data) }
|
28
25
|
Her::Collection.new(collection_data, parsed_data[:metadata], parsed_data[:errors])
|
29
|
-
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Use setter methods of model for each key / value pair in params
|
29
|
+
# Return key / value pairs for which no setter method was defined on the model
|
30
|
+
# @private
|
31
|
+
def self.use_setter_methods(model, params)
|
32
|
+
setter_method_names = model.class.setter_method_names
|
33
|
+
params.inject({}) do |memo, (key, value)|
|
34
|
+
setter_method = key.to_s + '='
|
35
|
+
if setter_method_names.include?(setter_method)
|
36
|
+
model.send(setter_method, value)
|
37
|
+
else
|
38
|
+
memo[key] = value
|
39
|
+
end
|
40
|
+
memo
|
41
|
+
end
|
42
|
+
end
|
30
43
|
|
31
44
|
# Handles missing methods by routing them through @data
|
32
45
|
# @private
|
33
|
-
def method_missing(method, *args, &blk)
|
46
|
+
def method_missing(method, *args, &blk)
|
34
47
|
if method.to_s.end_with?('=')
|
35
48
|
@data[method.to_s.chomp('=').to_sym] = args.first
|
36
49
|
elsif method.to_s.end_with?('?')
|
@@ -40,49 +53,59 @@ module Her
|
|
40
53
|
else
|
41
54
|
super
|
42
55
|
end
|
43
|
-
end
|
56
|
+
end
|
44
57
|
|
45
58
|
# Handles returning true for the cases handled by method_missing
|
46
|
-
def respond_to?(method, include_private = false)
|
59
|
+
def respond_to?(method, include_private = false)
|
47
60
|
method.to_s.end_with?('=') || method.to_s.end_with?('?') || @data.include?(method) || super
|
48
|
-
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Handles returning true for the accessible attributes
|
64
|
+
def has_key?(attribute_name)
|
65
|
+
@data.include?(attribute_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Handles returning attribute value from data
|
69
|
+
def [](attribute_name)
|
70
|
+
@data[attribute_name]
|
71
|
+
end
|
49
72
|
|
50
73
|
# Override the method to prevent from returning the object ID (in ruby-1.8.7)
|
51
74
|
# @private
|
52
|
-
def id
|
75
|
+
def id
|
53
76
|
@data[:id] || super
|
54
|
-
end
|
77
|
+
end
|
55
78
|
|
56
79
|
# Return `true` if a resource was not saved yet
|
57
|
-
def new?
|
80
|
+
def new?
|
58
81
|
!@data.include?(:id)
|
59
|
-
end
|
82
|
+
end
|
60
83
|
|
61
84
|
# Return `true` if a resource does not contain errors
|
62
|
-
def valid?
|
85
|
+
def valid?
|
63
86
|
@errors.empty?
|
64
|
-
end
|
87
|
+
end
|
65
88
|
|
66
89
|
# Return `true` if a resource contains errors
|
67
|
-
def invalid?
|
90
|
+
def invalid?
|
68
91
|
@errors.any?
|
69
|
-
end
|
92
|
+
end
|
70
93
|
|
71
94
|
# Return `true` if the other object is also a Her::Model and has matching data
|
72
|
-
def ==(other)
|
95
|
+
def ==(other)
|
73
96
|
other.is_a?(Her::Model) && @data == other.data
|
74
|
-
end
|
97
|
+
end
|
75
98
|
|
76
99
|
# Delegate to the == method
|
77
|
-
def eql?(other)
|
100
|
+
def eql?(other)
|
78
101
|
self == other
|
79
|
-
end
|
102
|
+
end
|
80
103
|
|
81
104
|
# Delegate to @data, allowing models to act correctly in code like:
|
82
105
|
# [ Model.find(1), Model.find(1) ].uniq # => [ Model.find(1) ]
|
83
|
-
def hash
|
106
|
+
def hash
|
84
107
|
@data.hash
|
85
|
-
end
|
108
|
+
end
|
86
109
|
|
87
110
|
# Save a resource
|
88
111
|
#
|
@@ -97,7 +120,7 @@ module Her
|
|
97
120
|
# @user = User.new({ :fullname => "Tobias Fünke" })
|
98
121
|
# @user.save
|
99
122
|
# # Called via POST "/users"
|
100
|
-
def save
|
123
|
+
def save
|
101
124
|
params = to_params
|
102
125
|
resource = self
|
103
126
|
|
@@ -114,10 +137,13 @@ module Her
|
|
114
137
|
self.data = parsed_data[:data]
|
115
138
|
self.metadata = parsed_data[:metadata]
|
116
139
|
self.errors = parsed_data[:errors]
|
140
|
+
|
141
|
+
return false if self.errors.any?
|
117
142
|
end
|
118
143
|
end
|
144
|
+
|
119
145
|
self
|
120
|
-
end
|
146
|
+
end
|
121
147
|
|
122
148
|
# Destroy a resource
|
123
149
|
#
|
@@ -125,7 +151,7 @@ module Her
|
|
125
151
|
# @user = User.find(1)
|
126
152
|
# @user.destroy
|
127
153
|
# # Called via DELETE "/users/1"
|
128
|
-
def destroy
|
154
|
+
def destroy
|
129
155
|
resource = self
|
130
156
|
self.class.wrap_in_hooks(resource, :destroy) do |resource, klass|
|
131
157
|
klass.request(:_method => :delete, :_path => "#{request_path}") do |parsed_data|
|
@@ -135,32 +161,24 @@ module Her
|
|
135
161
|
end
|
136
162
|
end
|
137
163
|
self
|
138
|
-
end
|
164
|
+
end
|
139
165
|
|
140
166
|
# Convert into a hash of request parameters
|
141
167
|
#
|
142
168
|
# @example
|
143
169
|
# @user.to_params
|
144
170
|
# # => { :id => 1, :name => 'John Smith' }
|
145
|
-
def to_params
|
171
|
+
def to_params
|
146
172
|
@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
|
155
|
-
end # }}}
|
173
|
+
end
|
156
174
|
|
157
175
|
module ClassMethods
|
158
176
|
# Initialize a collection of resources with raw data from an HTTP request
|
159
177
|
#
|
160
178
|
# @param [Array] parsed_data
|
161
|
-
def new_collection(parsed_data)
|
179
|
+
def new_collection(parsed_data)
|
162
180
|
Her::Model::ORM.initialize_collection(self, parsed_data)
|
163
|
-
end
|
181
|
+
end
|
164
182
|
|
165
183
|
# Fetch specific resource(s) by their ID
|
166
184
|
#
|
@@ -171,7 +189,7 @@ module Her
|
|
171
189
|
# @example
|
172
190
|
# @users = User.find([1, 2])
|
173
191
|
# # Fetched via GET "/users/1" and GET "/users/2"
|
174
|
-
def find(*ids)
|
192
|
+
def find(*ids)
|
175
193
|
params = ids.last.is_a?(Hash) ? ids.pop : {}
|
176
194
|
results = ids.flatten.compact.uniq.map do |id|
|
177
195
|
request(params.merge(:_method => :get, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data|
|
@@ -183,25 +201,25 @@ module Her
|
|
183
201
|
else
|
184
202
|
results.first
|
185
203
|
end
|
186
|
-
end
|
204
|
+
end
|
187
205
|
|
188
206
|
# Fetch a collection of resources
|
189
207
|
#
|
190
208
|
# @example
|
191
209
|
# @users = User.all
|
192
210
|
# # Fetched via GET "/users"
|
193
|
-
def all(params={})
|
211
|
+
def all(params={})
|
194
212
|
request(params.merge(:_method => :get, :_path => "#{build_request_path(params)}")) do |parsed_data|
|
195
213
|
new_collection(parsed_data)
|
196
214
|
end
|
197
|
-
end
|
215
|
+
end
|
198
216
|
|
199
217
|
# Create a resource and return it
|
200
218
|
#
|
201
219
|
# @example
|
202
220
|
# @user = User.create({ :fullname => "Tobias Fünke" })
|
203
221
|
# # Called via POST "/users/1"
|
204
|
-
def create(params={})
|
222
|
+
def create(params={})
|
205
223
|
resource = new(params)
|
206
224
|
wrap_in_hooks(resource, :create, :save) do |resource, klass|
|
207
225
|
params = resource.to_params
|
@@ -214,28 +232,36 @@ module Her
|
|
214
232
|
end
|
215
233
|
end
|
216
234
|
resource
|
217
|
-
end
|
235
|
+
end
|
218
236
|
|
219
237
|
# Save an existing resource and return it
|
220
238
|
#
|
221
239
|
# @example
|
222
240
|
# @user = User.save_existing(1, { :fullname => "Tobias Fünke" })
|
223
241
|
# # Called via PUT "/users/1"
|
224
|
-
def save_existing(id, params)
|
242
|
+
def save_existing(id, params)
|
225
243
|
resource = new(params.merge(:id => id))
|
226
244
|
resource.save
|
227
|
-
end
|
245
|
+
end
|
228
246
|
|
229
247
|
# Destroy an existing resource
|
230
248
|
#
|
231
249
|
# @example
|
232
250
|
# User.destroy_existing(1)
|
233
251
|
# # Called via DELETE "/users/1"
|
234
|
-
def destroy_existing(id, params={})
|
252
|
+
def destroy_existing(id, params={})
|
235
253
|
request(params.merge(:_method => :delete, :_path => "#{build_request_path(params.merge(:id => id))}")) do |parsed_data|
|
236
254
|
new(parsed_data[:data])
|
237
255
|
end
|
238
|
-
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# @private
|
259
|
+
def setter_method_names
|
260
|
+
@setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
|
261
|
+
memo << method_name.to_s if method_name.to_s.end_with?('=')
|
262
|
+
memo
|
263
|
+
end
|
264
|
+
end
|
239
265
|
end
|
240
266
|
end
|
241
267
|
end
|
data/lib/her/model/paths.rb
CHANGED
@@ -11,9 +11,9 @@ module Her
|
|
11
11
|
# end
|
12
12
|
#
|
13
13
|
# User.find(1) # Fetched via GET /utilisateurs/1
|
14
|
-
def request_path
|
14
|
+
def request_path
|
15
15
|
self.class.build_request_path(@data.dup)
|
16
|
-
end
|
16
|
+
end
|
17
17
|
|
18
18
|
module ClassMethods
|
19
19
|
# Defines a custom collection path for the resource
|
@@ -23,7 +23,7 @@ module Her
|
|
23
23
|
# include Her::Model
|
24
24
|
# collection_path "/users"
|
25
25
|
# end
|
26
|
-
def collection_path(path=nil)
|
26
|
+
def collection_path(path=nil)
|
27
27
|
@her_collection_path ||= begin
|
28
28
|
superclass.collection_path.dup if superclass.respond_to?(:collection_path)
|
29
29
|
end
|
@@ -31,7 +31,7 @@ module Her
|
|
31
31
|
return @her_collection_path unless path
|
32
32
|
@her_resource_path = "#{path}/:id"
|
33
33
|
@her_collection_path = path
|
34
|
-
end
|
34
|
+
end
|
35
35
|
|
36
36
|
# Defines a custom resource path for the resource
|
37
37
|
#
|
@@ -40,14 +40,14 @@ module Her
|
|
40
40
|
# include Her::Model
|
41
41
|
# resource_path "/users/:id"
|
42
42
|
# end
|
43
|
-
def resource_path(path=nil)
|
43
|
+
def resource_path(path=nil)
|
44
44
|
@her_resource_path ||= begin
|
45
45
|
superclass.resource_path.dup if superclass.respond_to?(:resource_path)
|
46
46
|
end
|
47
47
|
|
48
48
|
return @her_resource_path unless path
|
49
49
|
@her_resource_path = path
|
50
|
-
end
|
50
|
+
end
|
51
51
|
|
52
52
|
# Return a custom path based on the collection path and variable parameters
|
53
53
|
#
|
@@ -58,7 +58,7 @@ module Her
|
|
58
58
|
# end
|
59
59
|
#
|
60
60
|
# User.all # Fetched via GET /utilisateurs
|
61
|
-
def build_request_path(path=nil, parameters={})
|
61
|
+
def build_request_path(path=nil, parameters={})
|
62
62
|
unless path.is_a?(String)
|
63
63
|
parameters = path || {}
|
64
64
|
path = parameters.include?(:id) ? resource_path : collection_path
|
@@ -68,7 +68,7 @@ module Her
|
|
68
68
|
# Look for :key or :_key, otherwise raise an exception
|
69
69
|
parameters.delete($1.to_sym) || parameters.delete("_#{$1}".to_sym) || raise(Her::Errors::PathError.new("Missing :_#{$1} parameter to build the request path (#{path})."))
|
70
70
|
end
|
71
|
-
end
|
71
|
+
end
|
72
72
|
end
|
73
73
|
end
|
74
74
|
end
|
@@ -4,8 +4,9 @@ module Her
|
|
4
4
|
module Relationships
|
5
5
|
# Return @her_relationships, lazily initialized with copy of the
|
6
6
|
# superclass' her_relationships, or an empty hash.
|
7
|
+
#
|
7
8
|
# @private
|
8
|
-
def relationships
|
9
|
+
def relationships
|
9
10
|
@her_relationships ||= begin
|
10
11
|
if superclass.respond_to?(:relationships)
|
11
12
|
superclass.relationships.dup
|
@@ -13,16 +14,17 @@ module Her
|
|
13
14
|
{}
|
14
15
|
end
|
15
16
|
end
|
16
|
-
end
|
17
|
+
end
|
17
18
|
|
18
19
|
# Parse relationships data after initializing a new object
|
20
|
+
#
|
19
21
|
# @private
|
20
|
-
def parse_relationships(data)
|
22
|
+
def parse_relationships(data)
|
21
23
|
relationships.each_pair do |type, definitions|
|
22
24
|
definitions.each do |relationship|
|
23
25
|
name = relationship[:name]
|
26
|
+
next unless data[name]
|
24
27
|
klass = self.nearby_class(relationship[:class_name])
|
25
|
-
next if !data.include?(name) or data[name].nil?
|
26
28
|
data[name] = case type
|
27
29
|
when :has_many
|
28
30
|
Her::Model::ORM.initialize_collection(klass, :data => data[name])
|
@@ -34,7 +36,7 @@ module Her
|
|
34
36
|
end
|
35
37
|
end
|
36
38
|
data
|
37
|
-
end
|
39
|
+
end
|
38
40
|
|
39
41
|
# Define an *has_many* relationship.
|
40
42
|
#
|
@@ -54,7 +56,7 @@ module Her
|
|
54
56
|
# @user = User.find(1)
|
55
57
|
# @user.articles # => [#<Article(articles/2) id=2 title="Hello world.">]
|
56
58
|
# # Fetched via GET "/users/1/articles"
|
57
|
-
def has_many(name, attrs={})
|
59
|
+
def has_many(name, attrs={})
|
58
60
|
attrs = {
|
59
61
|
:class_name => name.to_s.classify,
|
60
62
|
:name => name,
|
@@ -71,7 +73,7 @@ module Her
|
|
71
73
|
@data[name] ||= klass.get_collection("#{self.class.build_request_path(:id => id)}#{attrs[:path]}")
|
72
74
|
end
|
73
75
|
end
|
74
|
-
end
|
76
|
+
end
|
75
77
|
|
76
78
|
# Define an *has_one* relationship.
|
77
79
|
#
|
@@ -91,7 +93,7 @@ module Her
|
|
91
93
|
# @user = User.find(1)
|
92
94
|
# @user.organization # => #<Organization(organizations/2) id=2 name="Foobar Inc.">
|
93
95
|
# # Fetched via GET "/users/1/organization"
|
94
|
-
def has_one(name, attrs={})
|
96
|
+
def has_one(name, attrs={})
|
95
97
|
attrs = {
|
96
98
|
:class_name => name.to_s.classify,
|
97
99
|
:name => name,
|
@@ -108,7 +110,7 @@ module Her
|
|
108
110
|
@data[name] ||= klass.get_resource("#{self.class.build_request_path(:id => id)}#{attrs[:path]}")
|
109
111
|
end
|
110
112
|
end
|
111
|
-
end
|
113
|
+
end
|
112
114
|
|
113
115
|
# Define a *belongs_to* relationship.
|
114
116
|
#
|
@@ -128,7 +130,7 @@ module Her
|
|
128
130
|
# @user = User.find(1)
|
129
131
|
# @user.team # => #<Team(teams/2) id=2 name="Developers">
|
130
132
|
# # Fetched via GET "/teams/2"
|
131
|
-
def belongs_to(name, attrs={})
|
133
|
+
def belongs_to(name, attrs={})
|
132
134
|
attrs = {
|
133
135
|
:class_name => name.to_s.classify,
|
134
136
|
:name => name,
|
@@ -146,17 +148,17 @@ module Her
|
|
146
148
|
@data[name] ||= klass.get_resource("#{klass.build_request_path(:id => @data[attrs[:foreign_key].to_sym])}")
|
147
149
|
end
|
148
150
|
end
|
149
|
-
end
|
151
|
+
end
|
150
152
|
|
151
153
|
# @private
|
152
|
-
def relationship_accessor(type, attrs)
|
154
|
+
def relationship_accessor(type, attrs)
|
153
155
|
name = attrs[:name]
|
154
156
|
class_name = attrs[:class_name]
|
155
157
|
define_method(name) do
|
156
158
|
klass = self.class.nearby_class(attrs[:class_name])
|
157
159
|
@data[name] ||= klass.get_resource("#{klass.build_request_path(attrs[:path], :id => @data[attrs[:foreign_key].to_sym])}")
|
158
160
|
end
|
159
|
-
end
|
161
|
+
end
|
160
162
|
end
|
161
163
|
end
|
162
164
|
end
|