her 0.5.5 → 0.6

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