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 +4 -1
- data/Rakefile +2 -2
- data/her.gemspec +2 -2
- data/lib/her/model.rb +2 -4
- data/lib/her/model/hooks.rb +10 -7
- data/lib/her/model/http.rb +14 -2
- data/lib/her/model/introspection.rb +27 -27
- data/lib/her/model/orm.rb +132 -80
- data/lib/her/model/paths.rb +58 -46
- data/lib/her/model/relationships.rb +36 -18
- data/lib/her/version.rb +1 -1
- 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 +43 -32
- data/spec/model/http_spec.rb +82 -34
- data/spec/model/introspection_spec.rb +8 -8
- data/spec/model/orm_spec.rb +172 -0
- data/spec/model/paths_spec.rb +116 -16
- data/spec/model/relationships_spec.rb +28 -3
- data/spec/spec_helper.rb +2 -1
- metadata +8 -8
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Her [](http://travis-ci.org/remiprev/her) [](https://gemnasium.com/remiprev/her)
|
1
|
+
# Her [](http://travis-ci.org/remiprev/her) [](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
data/her.gemspec
CHANGED
@@ -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", "~>
|
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
|
data/lib/her/model.rb
CHANGED
@@ -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 "
|
40
|
-
resource_path "
|
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
|
data/lib/her/model/hooks.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
data/lib/her/model/http.rb
CHANGED
@@ -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={}
|
13
|
-
|
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}(#{
|
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
|
data/lib/her/model/orm.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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,
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@data
|
35
|
-
|
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
|
-
|
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
|
-
#
|
74
|
-
#
|
75
|
-
|
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
|
-
#
|
85
|
-
#
|
86
|
-
|
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
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
|
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 =
|
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
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
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
|
-
#
|
140
|
+
# Convert into a hash of request parameters
|
179
141
|
#
|
180
142
|
# @example
|
181
|
-
#
|
182
|
-
# #
|
183
|
-
def
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|