her 0.3.3 → 0.3.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|