her5 0.8.1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +17 -0
  5. data/.yardopts +2 -0
  6. data/CONTRIBUTING.md +26 -0
  7. data/Gemfile +10 -0
  8. data/LICENSE +7 -0
  9. data/README.md +1017 -0
  10. data/Rakefile +11 -0
  11. data/UPGRADE.md +101 -0
  12. data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  13. data/gemfiles/Gemfile.activemodel-4.0 +7 -0
  14. data/gemfiles/Gemfile.activemodel-4.1 +7 -0
  15. data/gemfiles/Gemfile.activemodel-4.2 +7 -0
  16. data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
  17. data/her5.gemspec +30 -0
  18. data/lib/her.rb +19 -0
  19. data/lib/her/api.rb +120 -0
  20. data/lib/her/collection.rb +12 -0
  21. data/lib/her/errors.rb +104 -0
  22. data/lib/her/json_api/model.rb +57 -0
  23. data/lib/her/middleware.rb +12 -0
  24. data/lib/her/middleware/accept_json.rb +17 -0
  25. data/lib/her/middleware/first_level_parse_json.rb +36 -0
  26. data/lib/her/middleware/json_api_parser.rb +68 -0
  27. data/lib/her/middleware/parse_json.rb +28 -0
  28. data/lib/her/middleware/second_level_parse_json.rb +36 -0
  29. data/lib/her/model.rb +75 -0
  30. data/lib/her/model/associations.rb +141 -0
  31. data/lib/her/model/associations/association.rb +107 -0
  32. data/lib/her/model/associations/association_proxy.rb +45 -0
  33. data/lib/her/model/associations/belongs_to_association.rb +101 -0
  34. data/lib/her/model/associations/has_many_association.rb +101 -0
  35. data/lib/her/model/associations/has_one_association.rb +80 -0
  36. data/lib/her/model/attributes.rb +297 -0
  37. data/lib/her/model/base.rb +33 -0
  38. data/lib/her/model/deprecated_methods.rb +61 -0
  39. data/lib/her/model/http.rb +113 -0
  40. data/lib/her/model/introspection.rb +65 -0
  41. data/lib/her/model/nested_attributes.rb +84 -0
  42. data/lib/her/model/orm.rb +207 -0
  43. data/lib/her/model/parse.rb +221 -0
  44. data/lib/her/model/paths.rb +126 -0
  45. data/lib/her/model/relation.rb +164 -0
  46. data/lib/her/version.rb +3 -0
  47. data/spec/api_spec.rb +114 -0
  48. data/spec/collection_spec.rb +26 -0
  49. data/spec/json_api/model_spec.rb +305 -0
  50. data/spec/middleware/accept_json_spec.rb +10 -0
  51. data/spec/middleware/first_level_parse_json_spec.rb +62 -0
  52. data/spec/middleware/json_api_parser_spec.rb +32 -0
  53. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  54. data/spec/model/associations/association_proxy_spec.rb +31 -0
  55. data/spec/model/associations_spec.rb +504 -0
  56. data/spec/model/attributes_spec.rb +389 -0
  57. data/spec/model/callbacks_spec.rb +145 -0
  58. data/spec/model/dirty_spec.rb +91 -0
  59. data/spec/model/http_spec.rb +158 -0
  60. data/spec/model/introspection_spec.rb +76 -0
  61. data/spec/model/nested_attributes_spec.rb +134 -0
  62. data/spec/model/orm_spec.rb +506 -0
  63. data/spec/model/parse_spec.rb +345 -0
  64. data/spec/model/paths_spec.rb +347 -0
  65. data/spec/model/relation_spec.rb +226 -0
  66. data/spec/model/validations_spec.rb +42 -0
  67. data/spec/model_spec.rb +44 -0
  68. data/spec/spec_helper.rb +26 -0
  69. data/spec/support/extensions/array.rb +5 -0
  70. data/spec/support/extensions/hash.rb +5 -0
  71. data/spec/support/macros/her_macros.rb +17 -0
  72. data/spec/support/macros/model_macros.rb +36 -0
  73. data/spec/support/macros/request_macros.rb +27 -0
  74. metadata +289 -0
@@ -0,0 +1,221 @@
1
+ module Her
2
+ module Model
3
+ # This module handles resource data parsing at the model level (after the parsing middleware)
4
+ module Parse
5
+ extend ActiveSupport::Concern
6
+
7
+ # Convert into a hash of request parameters, based on `include_root_in_json`.
8
+ #
9
+ # @example
10
+ # @user.to_params
11
+ # # => { :id => 1, :name => 'John Smith' }
12
+ def to_params
13
+ self.class.to_params(self.saved_attributes, self.changes)
14
+ end
15
+
16
+ def saved_attributes
17
+ simple_attributes = attributes.except(*self.class.association_names.map(&:to_s))
18
+ simple_attributes.merge(saved_nested_attributes)
19
+ end
20
+
21
+ module ClassMethods
22
+ # Parse data before assigning it to a resource, based on `parse_root_in_json`.
23
+ #
24
+ # @param [Hash] data
25
+ # @private
26
+ def parse(data)
27
+ if parse_root_in_json? && root_element_included?(data)
28
+ if json_api_format?
29
+ data.fetch(parsed_root_element).first
30
+ else
31
+ data.fetch(parsed_root_element) { data }
32
+ end
33
+ else
34
+ data
35
+ end
36
+ end
37
+
38
+ # @private
39
+ def to_params(attributes, changes={})
40
+ filtered_attributes = attributes.dup.symbolize_keys
41
+ # filtered_attributes.merge!(embeded_params(attributes))
42
+ if her_api.options[:send_only_modified_attributes]
43
+ filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
44
+ hash[attribute] = filtered_attributes[attribute]
45
+ hash
46
+ end
47
+ end
48
+
49
+ if include_root_in_json?
50
+ if json_api_format?
51
+ { included_root_element => [filtered_attributes] }
52
+ else
53
+ { included_root_element => filtered_attributes }
54
+ end
55
+ else
56
+ filtered_attributes
57
+ end
58
+ end
59
+
60
+
61
+ # @private
62
+ # TODO: Handle has_one
63
+ # def embeded_params(attributes)
64
+ # associations[:has_many].select { |a| attributes.include?(a[:data_key])}.compact.inject({}) do |hash, association|
65
+ # params = attributes[association[:data_key]].map(&:to_params)
66
+ # next if params.empty?
67
+ # if association[:class_name].constantize.include_root_in_json?
68
+ # root = association[:class_name].constantize.root_element
69
+ # hash[association[:data_key]] = params.map { |n| n[root] }
70
+ # else
71
+ # hash[association[:data_key]] = params
72
+ # end
73
+ # hash
74
+ # end
75
+ # end
76
+
77
+ # Return or change the value of `include_root_in_json`
78
+ #
79
+ # @example
80
+ # class User
81
+ # include Her::Model
82
+ # include_root_in_json true
83
+ # end
84
+ def include_root_in_json(value, options = {})
85
+ @_her_include_root_in_json = value
86
+ @_her_include_root_in_json_format = options[:format]
87
+ end
88
+
89
+ # Return or change the value of `parse_root_in_json`
90
+ #
91
+ # @example
92
+ # class User
93
+ # include Her::Model
94
+ # parse_root_in_json true
95
+ # end
96
+ #
97
+ # class User
98
+ # include Her::Model
99
+ # parse_root_in_json true, format: :active_model_serializers
100
+ # end
101
+ #
102
+ # class User
103
+ # include Her::Model
104
+ # parse_root_in_json true, format: :json_api
105
+ # end
106
+ def parse_root_in_json(value, options = {})
107
+ @_her_parse_root_in_json = value
108
+ @_her_parse_root_in_json_format = options[:format]
109
+ end
110
+
111
+ # Return or change the value of `request_new_object_on_build`
112
+ #
113
+ # @example
114
+ # class User
115
+ # include Her::Model
116
+ # request_new_object_on_build true
117
+ # end
118
+ def request_new_object_on_build(value = nil)
119
+ @_her_request_new_object_on_build = value
120
+ end
121
+
122
+ # Return or change the value of `root_element`. Always defaults to the base name of the class.
123
+ #
124
+ # @example
125
+ # class User
126
+ # include Her::Model
127
+ # parse_root_in_json true
128
+ # root_element :huh
129
+ # end
130
+ #
131
+ # user = User.find(1) # { :huh => { :id => 1, :name => "Tobias" } }
132
+ # user.name # => "Tobias"
133
+ def root_element(value = nil)
134
+ if value.nil?
135
+ if json_api_format?
136
+ @_her_root_element ||= self.name.split("::").last.pluralize.underscore.to_sym
137
+ else
138
+ @_her_root_element ||= self.name.split("::").last.underscore.to_sym
139
+ end
140
+ else
141
+ @_her_root_element = value.to_sym
142
+ end
143
+ end
144
+
145
+ # @private
146
+ def root_element_included?(data)
147
+ data.keys.to_s.include? @_her_root_element.to_s
148
+ end
149
+
150
+ # @private
151
+ def included_root_element
152
+ include_root_in_json? == true ? root_element : include_root_in_json?
153
+ end
154
+
155
+ # Extract an array from the request data
156
+ #
157
+ # @example
158
+ # # with parse_root_in_json true, :format => :active_model_serializers
159
+ # class User
160
+ # include Her::Model
161
+ # parse_root_in_json true, :format => :active_model_serializers
162
+ # end
163
+ #
164
+ # users = User.all # { :users => [ { :id => 1, :name => "Tobias" } ] }
165
+ # users.first.name # => "Tobias"
166
+ #
167
+ # # without parse_root_in_json
168
+ # class User
169
+ # include Her::Model
170
+ # end
171
+ #
172
+ # users = User.all # [ { :id => 1, :name => "Tobias" } ]
173
+ # users.first.name # => "Tobias"
174
+ #
175
+ # @private
176
+ def extract_array(request_data)
177
+ if request_data[:data].is_a?(Hash) && (active_model_serializers_format? || json_api_format?)
178
+ request_data[:data][pluralized_parsed_root_element]
179
+ else
180
+ request_data[:data]
181
+ end
182
+ end
183
+
184
+ # @private
185
+ def pluralized_parsed_root_element
186
+ parsed_root_element.to_s.pluralize.to_sym
187
+ end
188
+
189
+ # @private
190
+ def parsed_root_element
191
+ parse_root_in_json? == true ? root_element : parse_root_in_json?
192
+ end
193
+
194
+ # @private
195
+ def active_model_serializers_format?
196
+ @_her_parse_root_in_json_format == :active_model_serializers || (superclass.respond_to?(:active_model_serializers_format?) && superclass.active_model_serializers_format?)
197
+ end
198
+
199
+ # @private
200
+ def json_api_format?
201
+ @_her_parse_root_in_json_format == :json_api || (superclass.respond_to?(:json_api_format?) && superclass.json_api_format?)
202
+ end
203
+
204
+ # @private
205
+ def request_new_object_on_build?
206
+ @_her_request_new_object_on_build || (superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?)
207
+ end
208
+
209
+ # @private
210
+ def include_root_in_json?
211
+ @_her_include_root_in_json || (superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?)
212
+ end
213
+
214
+ # @private
215
+ def parse_root_in_json?
216
+ @_her_parse_root_in_json || (superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?)
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,126 @@
1
+ module Her
2
+ module Model
3
+ module Paths
4
+ extend ActiveSupport::Concern
5
+ # Return a path based on the collection path and a resource data
6
+ #
7
+ # @example
8
+ # class User
9
+ # include Her::Model
10
+ # collection_path "/utilisateurs"
11
+ # end
12
+ #
13
+ # User.find(1) # Fetched via GET /utilisateurs/1
14
+ #
15
+ # @param [Hash] params An optional set of additional parameters for
16
+ # path construction. These will not override attributes of the resource.
17
+ def request_path(params = {})
18
+ self.class.build_request_path(params.merge(attributes.dup))
19
+ end
20
+
21
+ module ClassMethods
22
+
23
+ # Define the primary key field that will be used to find and save records
24
+ #
25
+ # @example
26
+ # class User
27
+ # include Her::Model
28
+ # primary_key 'UserId'
29
+ # end
30
+ #
31
+ # @param [Symbol] value
32
+ def primary_key(value = nil)
33
+ @_her_primary_key ||= begin
34
+ superclass.primary_key if superclass.respond_to?(:primary_key)
35
+ end
36
+
37
+ return @_her_primary_key unless value
38
+ @_her_primary_key = value.to_sym
39
+ end
40
+
41
+ # Defines a custom collection path for the resource
42
+ #
43
+ # @example
44
+ # class User
45
+ # include Her::Model
46
+ # collection_path "/users"
47
+ # end
48
+ def collection_path(path = nil)
49
+ if path.nil?
50
+ @_her_collection_path ||= root_element.to_s.pluralize
51
+ else
52
+ @_her_collection_path = path
53
+ @_her_resource_path = "#{path}/:id"
54
+ end
55
+ end
56
+
57
+ # Defines a custom resource path for the resource
58
+ #
59
+ # @example
60
+ # class User
61
+ # include Her::Model
62
+ # resource_path "/users/:id"
63
+ # end
64
+ #
65
+ # Note that, if used in combination with resource_path, you may specify
66
+ # either the real primary key or the string ':id'. For example:
67
+ #
68
+ # @example
69
+ # class User
70
+ # include Her::Model
71
+ # primary_key 'user_id'
72
+ #
73
+ # # This works because we'll have a user_id attribute
74
+ # resource_path '/users/:user_id'
75
+ #
76
+ # # This works because we replace :id with :user_id
77
+ # resource_path '/users/:id'
78
+ # end
79
+ #
80
+ def resource_path(path = nil)
81
+ if path.nil?
82
+ @_her_resource_path ||= "#{root_element.to_s.pluralize}/:id"
83
+ else
84
+ @_her_resource_path = path
85
+ end
86
+ end
87
+
88
+ # Return a custom path based on the collection path and variable parameters
89
+ #
90
+ # @private
91
+ def build_request_path(path=nil, parameters={})
92
+ parameters = parameters.try(:with_indifferent_access)
93
+
94
+ unless path.is_a?(String)
95
+ parameters = path.try(:with_indifferent_access) || parameters
96
+ path =
97
+ if parameters.include?(primary_key) && parameters[primary_key] && !parameters[primary_key].kind_of?(Array)
98
+ resource_path.dup
99
+ else
100
+ collection_path.dup
101
+ end
102
+
103
+ # Replace :id with our actual primary key
104
+ path.gsub!(/(\A|\/):id(\Z|\/)/, "\\1:#{primary_key}\\2")
105
+ end
106
+
107
+ path.gsub(/:([\w_]+)/) do
108
+ # Look for :key or :_key, otherwise raise an exception
109
+ key = $1.to_sym
110
+ value = parameters.delete(key) || parameters.delete(:"_#{key}")
111
+ if value
112
+ Faraday::Utils.escape value
113
+ else
114
+ raise(Her::Errors::PathError.new("Missing :_#{$1} parameter to build the request path. Path is `#{path}`. Parameters are `#{parameters.symbolize_keys.inspect}`.", $1))
115
+ end
116
+ end
117
+ end
118
+
119
+ # @private
120
+ def build_request_path_from_string_or_symbol(path, params={})
121
+ path.is_a?(Symbol) ? "#{build_request_path(params)}/#{path}" : path
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,164 @@
1
+ module Her
2
+ module Model
3
+ class Relation
4
+ # @private
5
+ attr_accessor :params
6
+
7
+ # @private
8
+ def initialize(parent)
9
+ @parent = parent
10
+ @params = {}
11
+ end
12
+
13
+ # @private
14
+ def apply_to(attributes)
15
+ @params.merge(attributes)
16
+ end
17
+
18
+ # Build a new resource
19
+ def build(attributes = {})
20
+ @parent.build(@params.merge(attributes))
21
+ end
22
+
23
+ # Add a query string parameter
24
+ #
25
+ # @example
26
+ # @users = User.all
27
+ # # Fetched via GET "/users"
28
+ #
29
+ # @example
30
+ # @users = User.where(:approved => 1).all
31
+ # # Fetched via GET "/users?approved=1"
32
+ def where(params = {})
33
+ return self if params.blank? && !@_fetch.nil?
34
+ self.clone.tap do |r|
35
+ r.params = r.params.merge(params)
36
+ r.clear_fetch_cache!
37
+ end
38
+ end
39
+ alias all where
40
+
41
+ # Bubble all methods to the fetched collection
42
+ #
43
+ # @private
44
+ def method_missing(method, *args, &blk)
45
+ fetch.send(method, *args, &blk)
46
+ end
47
+
48
+ # @private
49
+ def respond_to?(method, *args)
50
+ super || fetch.respond_to?(method, *args)
51
+ end
52
+
53
+ # @private
54
+ def nil?
55
+ fetch.nil?
56
+ end
57
+
58
+ # @private
59
+ def kind_of?(thing)
60
+ fetch.kind_of?(thing)
61
+ end
62
+
63
+ # Fetch a collection of resources
64
+ #
65
+ # @private
66
+ def fetch
67
+ @_fetch ||= begin
68
+ path = @parent.build_request_path(@params)
69
+ method = @parent.method_for(:find)
70
+ @parent.request(@params.merge(:_method => method, :_path => path)) do |parsed_data, response|
71
+ @parent.new_collection(parsed_data)
72
+ end
73
+ end
74
+ end
75
+
76
+ # Fetch specific resource(s) by their ID
77
+ #
78
+ # @example
79
+ # @user = User.find(1)
80
+ # # Fetched via GET "/users/1"
81
+ #
82
+ # @example
83
+ # @users = User.find([1, 2])
84
+ # # Fetched via GET "/users/1" and GET "/users/2"
85
+ def find(*ids)
86
+ params = @params.merge(ids.last.is_a?(Hash) ? ids.pop : {})
87
+ ids = Array(params[@parent.primary_key]) if params.key?(@parent.primary_key)
88
+
89
+ results = ids.flatten.compact.uniq.map do |id|
90
+ resource = nil
91
+ request_params = params.merge(
92
+ :_method => @parent.method_for(:find),
93
+ :_path => @parent.build_request_path(params.merge(@parent.primary_key => id))
94
+ )
95
+
96
+ @parent.request(request_params) do |parsed_data, response|
97
+ if response.success?
98
+ resource = @parent.new_from_parsed_data(parsed_data)
99
+ resource.instance_variable_set(:@changed_attributes, {})
100
+ resource.run_callbacks :find
101
+ else
102
+ return nil
103
+ end
104
+ end
105
+
106
+ resource
107
+ end
108
+
109
+ ids.length > 1 || ids.first.kind_of?(Array) ? results : results.first
110
+ end
111
+
112
+ # Create a resource and return it
113
+ #
114
+ # @example
115
+ # @user = User.create(:fullname => "Tobias Fünke")
116
+ # # Called via POST "/users/1" with `&fullname=Tobias+Fünke`
117
+ #
118
+ # @example
119
+ # @user = User.where(:email => "tobias@bluth.com").create(:fullname => "Tobias Fünke")
120
+ # # Called via POST "/users/1" with `&email=tobias@bluth.com&fullname=Tobias+Fünke`
121
+ def create(attributes = {})
122
+ attributes ||= {}
123
+ resource = @parent.new(@params.merge(attributes))
124
+ resource.save
125
+
126
+ resource
127
+ end
128
+
129
+ # Fetch a resource and create it if it's not found
130
+ #
131
+ # @example
132
+ # @user = User.where(:email => "remi@example.com").find_or_create
133
+ #
134
+ # # Returns the first item of the collection if present:
135
+ # # GET "/users?email=remi@example.com"
136
+ #
137
+ # # If collection is empty:
138
+ # # POST /users with `email=remi@example.com`
139
+ def first_or_create(attributes = {})
140
+ fetch.first || create(attributes)
141
+ end
142
+
143
+ # Fetch a resource and build it if it's not found
144
+ #
145
+ # @example
146
+ # @user = User.where(:email => "remi@example.com").find_or_initialize
147
+ #
148
+ # # Returns the first item of the collection if present:
149
+ # # GET "/users?email=remi@example.com"
150
+ #
151
+ # # If collection is empty:
152
+ # @user.email # => "remi@example.com"
153
+ # @user.new? # => true
154
+ def first_or_initialize(attributes = {})
155
+ fetch.first || build(attributes)
156
+ end
157
+
158
+ # @private
159
+ def clear_fetch_cache!
160
+ instance_variable_set(:@_fetch, nil)
161
+ end
162
+ end
163
+ end
164
+ end