her 0.5.5 → 0.6

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.
Files changed (63) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +1 -1
  3. data/README.md +78 -63
  4. data/UPGRADE.md +21 -0
  5. data/lib/her/model.rb +2 -1
  6. data/lib/her/model/associations.rb +17 -54
  7. data/lib/her/model/associations/association.rb +46 -0
  8. data/lib/her/model/associations/belongs_to_association.rb +34 -0
  9. data/lib/her/model/associations/has_many_association.rb +43 -0
  10. data/lib/her/model/associations/has_one_association.rb +33 -0
  11. data/lib/her/model/attributes.rb +19 -19
  12. data/lib/her/model/base.rb +5 -0
  13. data/lib/her/model/http.rb +17 -21
  14. data/lib/her/model/orm.rb +11 -35
  15. data/lib/her/model/parse.rb +4 -12
  16. data/lib/her/model/paths.rb +3 -2
  17. data/lib/her/model/relation.rb +113 -0
  18. data/lib/her/version.rb +1 -1
  19. data/spec/model/associations_spec.rb +48 -4
  20. data/spec/model/introspection_spec.rb +1 -1
  21. data/spec/model/orm_spec.rb +21 -102
  22. data/spec/model/parse_spec.rb +36 -7
  23. data/spec/model/paths_spec.rb +3 -3
  24. data/spec/model/relation_spec.rb +89 -0
  25. data/spec/spec_helper.rb +1 -0
  26. data/spec/support/macros/her_macros.rb +17 -0
  27. data/spec/support/macros/request_macros.rb +19 -0
  28. metadata +13 -37
  29. data/examples/grape-and-her/.env.default +0 -3
  30. data/examples/grape-and-her/Procfile +0 -2
  31. data/examples/grape-and-her/README.md +0 -27
  32. data/examples/grape-and-her/api/Gemfile +0 -11
  33. data/examples/grape-and-her/api/Rakefile +0 -14
  34. data/examples/grape-and-her/api/app/api.rb +0 -49
  35. data/examples/grape-and-her/api/app/models/organization.rb +0 -7
  36. data/examples/grape-and-her/api/app/models/user.rb +0 -9
  37. data/examples/grape-and-her/api/app/views/organizations/_base.rabl +0 -2
  38. data/examples/grape-and-her/api/app/views/organizations/index.rabl +0 -3
  39. data/examples/grape-and-her/api/app/views/organizations/show.rabl +0 -3
  40. data/examples/grape-and-her/api/app/views/users/_base.rabl +0 -8
  41. data/examples/grape-and-her/api/app/views/users/index.rabl +0 -3
  42. data/examples/grape-and-her/api/app/views/users/show.rabl +0 -3
  43. data/examples/grape-and-her/api/config.ru +0 -5
  44. data/examples/grape-and-her/api/config/boot.rb +0 -17
  45. data/examples/grape-and-her/api/config/unicorn.rb +0 -7
  46. data/examples/grape-and-her/api/db/migrations/001_create_users.rb +0 -11
  47. data/examples/grape-and-her/api/db/migrations/002_create_organizations.rb +0 -8
  48. data/examples/grape-and-her/consumer/Gemfile +0 -23
  49. data/examples/grape-and-her/consumer/app/assets/stylesheets/application.scss +0 -190
  50. data/examples/grape-and-her/consumer/app/assets/stylesheets/reset.scss +0 -53
  51. data/examples/grape-and-her/consumer/app/consumer.rb +0 -74
  52. data/examples/grape-and-her/consumer/app/models/organization.rb +0 -13
  53. data/examples/grape-and-her/consumer/app/models/user.rb +0 -13
  54. data/examples/grape-and-her/consumer/app/views/index.haml +0 -9
  55. data/examples/grape-and-her/consumer/app/views/layout.haml +0 -20
  56. data/examples/grape-and-her/consumer/app/views/organizations/index.haml +0 -25
  57. data/examples/grape-and-her/consumer/app/views/organizations/show.haml +0 -11
  58. data/examples/grape-and-her/consumer/app/views/users/index.haml +0 -33
  59. data/examples/grape-and-her/consumer/app/views/users/show.haml +0 -9
  60. data/examples/grape-and-her/consumer/config.ru +0 -20
  61. data/examples/grape-and-her/consumer/config/boot.rb +0 -30
  62. data/examples/grape-and-her/consumer/config/unicorn.rb +0 -7
  63. data/examples/grape-and-her/consumer/lib/response_logger.rb +0 -18
@@ -1,3 +1,8 @@
1
+ require "her/model/associations/association"
2
+ require "her/model/associations/belongs_to_association"
3
+ require "her/model/associations/has_many_association"
4
+ require "her/model/associations/has_one_association"
5
+
1
6
  module Her
2
7
  module Model
3
8
  # This module adds associations to models
@@ -86,29 +91,11 @@ module Her
86
91
  }.merge(attrs)
87
92
  (associations[:has_many] ||= []) << attrs
88
93
 
89
- define_method(name) do |*method_attrs|
90
- method_attrs = method_attrs[0] || {}
91
- klass = self.class.her_nearby_class(attrs[:class_name])
92
-
93
- return Her::Collection.new if @attributes.include?(name) && @attributes[name].empty? && method_attrs.empty?
94
-
95
- if @attributes[name].blank? || method_attrs.any?
96
- path = begin
97
- request_path(method_attrs)
98
- rescue Her::Errors::PathError
99
- return nil
100
- end
101
-
102
- @attributes[name] = klass.get_collection("#{path}#{attrs[:path]}", method_attrs)
103
- end
104
-
105
- inverse_of = attrs[:inverse_of] || self.class.name.split('::').last.tableize.singularize
94
+ define_method(name) do
95
+ cached_name = :"@_her_association_#{name}"
106
96
 
107
- @attributes[name].each do |entry|
108
- entry.send("#{inverse_of}=", self)
109
- end
110
-
111
- @attributes[name]
97
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
98
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasManyAssociation.new(self, attrs))
112
99
  end
113
100
  end
114
101
 
@@ -139,23 +126,11 @@ module Her
139
126
  }.merge(attrs)
140
127
  (associations[:has_one] ||= []) << attrs
141
128
 
142
- define_method(name) do |*method_attrs|
143
- method_attrs = method_attrs[0] || {}
144
- klass = self.class.her_nearby_class(attrs[:class_name])
145
-
146
- return nil if @attributes.include?(name) && @attributes[name].nil? && method_attrs.empty?
129
+ define_method(name) do
130
+ cached_name = :"@_her_association_#{name}"
147
131
 
148
- if @attributes[name].blank? || method_attrs.any?
149
- path = begin
150
- request_path(method_attrs)
151
- rescue Her::Errors::PathError
152
- return nil
153
- end
154
-
155
- @attributes[name] = klass.get_resource("#{path}#{attrs[:path]}", method_attrs)
156
- end
157
-
158
- @attributes[name]
132
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
133
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::HasOneAssociation.new(self, attrs))
159
134
  end
160
135
  end
161
136
 
@@ -187,23 +162,11 @@ module Her
187
162
  }.merge(attrs)
188
163
  (associations[:belongs_to] ||= []) << attrs
189
164
 
190
- define_method(name) do |*method_attrs|
191
- method_attrs = method_attrs[0] || {}
192
- klass = self.class.her_nearby_class(attrs[:class_name])
193
-
194
- return nil if @attributes.include?(name) && @attributes[name].nil? && method_attrs.empty?
195
-
196
- if @attributes[name].blank? || method_attrs.any?
197
- path = begin
198
- klass.build_request_path(@attributes.merge(method_attrs.merge(klass.primary_key => @attributes[attrs[:foreign_key].to_sym])))
199
- rescue Her::Errors::PathError
200
- return nil
201
- end
202
-
203
- @attributes[name] = klass.get_resource("#{path}", method_attrs)
204
- end
165
+ define_method(name) do
166
+ cached_name = :"@_her_association_#{name}"
205
167
 
206
- @attributes[name]
168
+ cached_data = (instance_variable_defined?(cached_name) && instance_variable_get(cached_name))
169
+ cached_data || instance_variable_set(cached_name, Her::Model::Associations::BelongsToAssociation.new(self, attrs))
207
170
  end
208
171
  end
209
172
  end
@@ -0,0 +1,46 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class Association
5
+ attr_accessor :query_attrs
6
+
7
+ # @private
8
+ def initialize(parent, opts = {})
9
+ @parent = parent
10
+ @opts = opts
11
+ @query_attrs = {}
12
+
13
+ @klass = @parent.class.her_nearby_class(@opts[:class_name])
14
+ @name = @opts[:name]
15
+ end
16
+
17
+ def where(attrs = {})
18
+ return self if attrs.blank?
19
+ self.clone.tap { |a| a.query_attrs = a.query_attrs.merge(attrs) }
20
+ end
21
+ alias :all :where
22
+
23
+ # @private
24
+ def nil?
25
+ fetch.nil?
26
+ end
27
+
28
+ # @private
29
+ def kind_of?(thing)
30
+ fetch.kind_of?(thing)
31
+ end
32
+
33
+ # @private
34
+ def ==(other)
35
+ fetch.eql?(other)
36
+ end
37
+ alias :eql? :==
38
+
39
+ # @private
40
+ def method_missing(method, *args, &blk)
41
+ fetch.send(method, *args, &blk)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,34 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class BelongsToAssociation < Association
5
+ def build(attributes = {})
6
+ @klass.new(attributes)
7
+ end
8
+
9
+ def create(attributes = {})
10
+ resource = build(attributes)
11
+ @parent.attributes[@name] = resource if resource.save
12
+ resource
13
+ end
14
+
15
+ def fetch
16
+ foreign_key_value = @parent.attributes[@opts[:foreign_key].to_sym]
17
+ return nil if (@parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @query_attrs.empty?) || foreign_key_value.blank?
18
+
19
+ if @parent.attributes[@name].blank? || @query_attrs.any?
20
+ path = begin
21
+ @klass.build_request_path(@parent.attributes.merge(@query_attrs.merge(@klass.primary_key => foreign_key_value)))
22
+ rescue Her::Errors::PathError
23
+ return nil
24
+ end
25
+
26
+ @klass.get_resource("#{path}", @query_attrs)
27
+ else
28
+ @parent.attributes[@name]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,43 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class HasManyAssociation < Association
5
+ def build(attributes = {})
6
+ @klass.new(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
7
+ end
8
+
9
+ def create(attributes = {})
10
+ resource = build(attributes)
11
+
12
+ if resource.save
13
+ @parent.attributes[@name] ||= Her::Collection.new
14
+ @parent.attributes[@name] << resource
15
+ end
16
+
17
+ resource
18
+ end
19
+
20
+ def fetch
21
+ return Her::Collection.new if @parent.attributes.include?(@name) && @parent.attributes[@name].empty? && @query_attrs.empty?
22
+
23
+ output = if @parent.attributes[@name].blank? || @query_attrs.any?
24
+ path = begin
25
+ @parent.request_path(@query_attrs)
26
+ rescue Her::Errors::PathError
27
+ return nil
28
+ end
29
+
30
+ @klass.get_collection("#{path}#{@opts[:path]}", @query_attrs)
31
+ else
32
+ @parent.attributes[@name]
33
+ end
34
+
35
+ inverse_of = @opts[:inverse_of] || @parent.singularized_resource_name
36
+ output.each { |entry| entry.send("#{inverse_of}=", @parent) }
37
+
38
+ output
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,33 @@
1
+ module Her
2
+ module Model
3
+ module Associations
4
+ class HasOneAssociation < Association
5
+ def build(attributes = {})
6
+ @klass.new(attributes.merge(:"#{@parent.singularized_resource_name}_id" => @parent.id))
7
+ end
8
+
9
+ def create(attributes = {})
10
+ resource = build(attributes)
11
+ @parent.attributes[@name] = resource if resource.save
12
+ resource
13
+ end
14
+
15
+ def fetch
16
+ return nil if @parent.attributes.include?(@name) && @parent.attributes[@name].nil? && @query_attrs.empty?
17
+
18
+ if @parent.attributes[@name].blank? || @query_attrs.any?
19
+ path = begin
20
+ @parent.request_path(@query_attrs)
21
+ rescue Her::Errors::PathError
22
+ return nil
23
+ end
24
+
25
+ @klass.get_resource("#{path}#{@opts[:path]}", @query_attrs)
26
+ else
27
+ @parent.attributes[@name]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -40,9 +40,7 @@ module Her
40
40
  if setter_method_names.include?(setter_method)
41
41
  model.send(setter_method, value)
42
42
  else
43
- if key.is_a?(String)
44
- key = key.to_sym
45
- end
43
+ key = key.to_sym if key.is_a?(String)
46
44
  memo[key] = value
47
45
  end
48
46
  memo
@@ -145,33 +143,26 @@ module Her
145
143
  attributes.each do |attribute|
146
144
  attribute = attribute.to_sym
147
145
 
148
- define_method "#{attribute}".to_sym do
146
+ define_method attribute do
149
147
  @attributes.include?(attribute) ? @attributes[attribute] : nil
150
148
  end
151
149
 
152
- define_method "#{attribute}=".to_sym do |value|
153
- self.send("#{attribute}_will_change!".to_sym) if @attributes[attribute] != value
150
+ define_method :"#{attribute}=" do |value|
151
+ self.send(:"#{attribute}_will_change!") if @attributes[attribute] != value
154
152
  @attributes[attribute] = value
155
153
  end
156
154
 
157
- define_method "#{attribute}?".to_sym do
155
+ define_method :"#{attribute}?" do
158
156
  @attributes.include?(attribute) && @attributes[attribute].present?
159
157
  end
160
158
  end
161
159
  end
162
160
 
163
- # @private
164
- def setter_method_names
165
- @_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
166
- memo << method_name.to_s if method_name.to_s.end_with?('=')
167
- memo
168
- end
169
- end
170
-
161
+ # Define the accessor in which the API response errors (obtained from the parsing middleware) will be stored
171
162
  def store_response_errors(value = nil)
172
163
  if @_her_store_response_errors
173
164
  remove_method @_her_store_response_errors
174
- remove_method "#{@_her_store_response_errors}="
165
+ remove_method :"#{@_her_store_response_errors}="
175
166
  end
176
167
 
177
168
  @_her_store_response_errors ||= begin
@@ -182,13 +173,14 @@ module Her
182
173
  @_her_store_response_errors = value
183
174
 
184
175
  define_method(@_her_store_response_errors) { @response_errors }
185
- define_method("#{@_her_store_response_errors}=") { |value| @response_errors = value }
176
+ define_method(:"#{@_her_store_response_errors}=") { |value| @response_errors = value }
186
177
  end
187
178
 
179
+ # Define the accessor in which the API response metadata (obtained from the parsing middleware) will be stored
188
180
  def store_metadata(value = nil)
189
181
  if @_her_store_metadata
190
182
  remove_method @_her_store_metadata
191
- remove_method "#{@_her_store_metadata}="
183
+ remove_method :"#{@_her_store_metadata}="
192
184
  end
193
185
 
194
186
  @_her_store_metadata ||= begin
@@ -199,7 +191,15 @@ module Her
199
191
  @_her_store_metadata = value
200
192
 
201
193
  define_method(@_her_store_metadata) { @metadata }
202
- define_method("#{@_her_store_metadata}=") { |value| @metadata = value }
194
+ define_method(:"#{@_her_store_metadata}=") { |value| @metadata = value }
195
+ end
196
+
197
+ # @private
198
+ def setter_method_names
199
+ @_her_setter_method_names ||= instance_methods.inject(Set.new) do |memo, method_name|
200
+ memo << method_name.to_s if method_name.to_s.end_with?('=')
201
+ memo
202
+ end
203
203
  end
204
204
  end
205
205
  end
@@ -19,6 +19,11 @@ module Her
19
19
  get_attribute(attribute_name) ||
20
20
  get_association(attribute_name)
21
21
  end
22
+
23
+ # @private
24
+ def singularized_resource_name
25
+ self.class.name.split('::').last.tableize.singularize
26
+ end
22
27
  end
23
28
  end
24
29
  end
@@ -5,21 +5,17 @@ module Her
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  module ClassMethods
8
- # Automatically inherit a superclass' api
9
- def her_api
10
- @_her_api ||= begin
11
- if superclass.respond_to?(:her_api)
12
- superclass.her_api
13
- else
14
- Her::API.default_api
15
- end
8
+ # Change which API the model will use to make its HTTP requests
9
+ def use_api(value = nil)
10
+ @_her_use_api ||= begin
11
+ superclass.use_api if superclass.respond_to?(:use_api)
16
12
  end
17
- end
18
13
 
19
- # Link a model with a Her::API object
20
- def uses_api(api)
21
- @_her_api = api
14
+ return @_her_use_api unless value
15
+ @_her_use_api = value
22
16
  end
17
+ alias :her_api :use_api
18
+ alias :uses_api :use_api
23
19
 
24
20
  # Main request wrapper around Her::API. Used to make custom request to the API.
25
21
  # @private
@@ -40,10 +36,10 @@ module Her
40
36
  # - <method>_collection(path, attrs, &block)
41
37
  # - <method>_resource(path, attrs, &block)
42
38
  # - custom_<method>(path, attrs)
43
- %w{GET POST PUT PATCH DELETE}.map(&:downcase).map(&:to_sym).each do |method|
39
+ [:get, :post, :put, :patch, :delete].each do |method|
44
40
  define_method method do |path, attrs={}|
45
41
  path = build_request_path_from_string_or_symbol(path, attrs)
46
- send("#{method}_raw".to_sym, path, attrs) do |parsed_data, response|
42
+ send(:"#{method}_raw", path, attrs) do |parsed_data, response|
47
43
  if parsed_data[:data].is_a?(Array)
48
44
  new_collection(parsed_data)
49
45
  else
@@ -52,31 +48,31 @@ module Her
52
48
  end
53
49
  end
54
50
 
55
- define_method "#{method}_raw".to_sym do |path, attrs={}, &block|
51
+ define_method :"#{method}_raw" do |path, attrs={}, &block|
56
52
  path = build_request_path_from_string_or_symbol(path, attrs)
57
53
  request(attrs.merge(:_method => method, :_path => path), &block)
58
54
  end
59
55
 
60
- define_method "#{method}_collection".to_sym do |path=nil, attrs={}|
56
+ define_method :"#{method}_collection" do |path=nil, attrs={}|
61
57
  path = build_request_path_from_string_or_symbol(path, attrs)
62
- send("#{method}_raw".to_sym, build_request_path_from_string_or_symbol(path, attrs), attrs) do |parsed_data, response|
58
+ send(:"#{method}_raw", build_request_path_from_string_or_symbol(path, attrs), attrs) do |parsed_data, response|
63
59
  new_collection(parsed_data)
64
60
  end
65
61
  end
66
62
 
67
- define_method "#{method}_resource".to_sym do |path, attrs={}|
63
+ define_method :"#{method}_resource" do |path, attrs={}|
68
64
  path = build_request_path_from_string_or_symbol(path, attrs)
69
- send("#{method}_raw".to_sym, path, attrs) do |parsed_data, response|
65
+ send(:"#{method}_raw", path, attrs) do |parsed_data, response|
70
66
  new(parse(parsed_data[:data]).merge :_metadata => parsed_data[:metadata], :_errors => parsed_data[:errors])
71
67
  end
72
68
  end
73
69
 
74
- define_method "custom_#{method}".to_sym do |*paths|
70
+ define_method :"custom_#{method}" do |*paths|
75
71
  metaclass = (class << self; self; end)
76
72
  opts = paths.last.is_a?(Hash) ? paths.pop : Hash.new
77
73
 
78
74
  paths.each do |path|
79
- metaclass.send(:define_method, path.to_sym) do |*attrs|
75
+ metaclass.send(:define_method, path) do |*attrs|
80
76
  attrs = attrs.first || Hash.new
81
77
  send(method, path, attrs)
82
78
  end
data/lib/her/model/orm.rb CHANGED
@@ -19,7 +19,8 @@ module Her
19
19
  @destroyed == true
20
20
  end
21
21
 
22
- # Save a resource
22
+ # Save a resource and return `false` if the response is not a successful one or
23
+ # if there are errors in the resource. Otherwise, return the newly updated resource
23
24
  #
24
25
  # @example Save a resource after fetching it
25
26
  # @user = User.find(1)
@@ -33,9 +34,6 @@ module Her
33
34
  # @user.save
34
35
  # # Called via POST "/users"
35
36
  def save
36
- params = to_params
37
- resource = self
38
-
39
37
  if new?
40
38
  callback = :create
41
39
  method = :post
@@ -46,7 +44,7 @@ module Her
46
44
 
47
45
  run_callbacks callback do
48
46
  run_callbacks :save do
49
- self.class.request(params.merge(:_method => method, :_path => "#{request_path}")) do |parsed_data, response|
47
+ self.class.request(to_params.merge(:_method => method, :_path => "#{request_path}")) do |parsed_data, response|
50
48
  assign_attributes(self.class.parse(parsed_data[:data])) if parsed_data[:data].any?
51
49
  self.metadata = parsed_data[:metadata]
52
50
  self.response_errors = parsed_data[:errors]
@@ -103,6 +101,7 @@ module Her
103
101
  end
104
102
  resource
105
103
  end
104
+
106
105
  if ids.length > 1 || ids.first.kind_of?(Array)
107
106
  results
108
107
  else
@@ -110,39 +109,16 @@ module Her
110
109
  end
111
110
  end
112
111
 
113
- # Fetch a collection of resources
114
- #
115
- # @example
116
- # @users = User.all
117
- # # Fetched via GET "/users"
118
- def all(params={})
119
- request(params.merge(:_method => :get, :_path => "#{build_request_path(params)}")) do |parsed_data, response|
120
- new_collection(parsed_data)
112
+ # Delegate the following methods to `scoped`
113
+ [:all, :where, :create, :page, :per_page].each do |method|
114
+ define_method method do |*attrs|
115
+ scoped.send(method, attrs.first)
121
116
  end
122
117
  end
123
118
 
124
- # Create a resource and return it
125
- #
126
- # @example
127
- # @user = User.create({ :fullname => "Tobias Fünke" })
128
- # # Called via POST "/users/1"
129
- def create(params={})
130
- resource = new(params)
131
- resource.run_callbacks :create do
132
- resource.run_callbacks :save do
133
- params = resource.to_params
134
- request(params.merge(:_method => :post, :_path => "#{build_request_path(params)}")) do |parsed_data, response|
135
- data = parse(parsed_data[:data])
136
- resource.instance_eval do
137
- assign_attributes(data)
138
- @metadata = parsed_data[:metadata]
139
- @response_errors = parsed_data[:errors]
140
- @changed_attributes.clear if @changed_attributes.present?
141
- end
142
- end
143
- end
144
- end
145
- resource
119
+ # @private
120
+ def scoped
121
+ Relation.new(self)
146
122
  end
147
123
 
148
124
  # Save an existing resource and return it