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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +17 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +1017 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +101 -0
- data/gemfiles/Gemfile.activemodel-3.2.x +7 -0
- data/gemfiles/Gemfile.activemodel-4.0 +7 -0
- data/gemfiles/Gemfile.activemodel-4.1 +7 -0
- data/gemfiles/Gemfile.activemodel-4.2 +7 -0
- data/gemfiles/Gemfile.activemodel-5.0.x +7 -0
- data/her5.gemspec +30 -0
- data/lib/her.rb +19 -0
- data/lib/her/api.rb +120 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +104 -0
- data/lib/her/json_api/model.rb +57 -0
- data/lib/her/middleware.rb +12 -0
- data/lib/her/middleware/accept_json.rb +17 -0
- data/lib/her/middleware/first_level_parse_json.rb +36 -0
- data/lib/her/middleware/json_api_parser.rb +68 -0
- data/lib/her/middleware/parse_json.rb +28 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +75 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +107 -0
- data/lib/her/model/associations/association_proxy.rb +45 -0
- data/lib/her/model/associations/belongs_to_association.rb +101 -0
- data/lib/her/model/associations/has_many_association.rb +101 -0
- data/lib/her/model/associations/has_one_association.rb +80 -0
- data/lib/her/model/attributes.rb +297 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +113 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +84 -0
- data/lib/her/model/orm.rb +207 -0
- data/lib/her/model/parse.rb +221 -0
- data/lib/her/model/paths.rb +126 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +114 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/json_api/model_spec.rb +305 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/json_api_parser_spec.rb +32 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations/association_proxy_spec.rb +31 -0
- data/spec/model/associations_spec.rb +504 -0
- data/spec/model/attributes_spec.rb +389 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +91 -0
- data/spec/model/http_spec.rb +158 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +506 -0
- data/spec/model/parse_spec.rb +345 -0
- data/spec/model/paths_spec.rb +347 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +44 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/extensions/array.rb +5 -0
- data/spec/support/extensions/hash.rb +5 -0
- data/spec/support/macros/her_macros.rb +17 -0
- data/spec/support/macros/model_macros.rb +36 -0
- data/spec/support/macros/request_macros.rb +27 -0
- 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
|