herr 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +15 -0
- data/.yardopts +2 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +10 -0
- data/LICENSE +7 -0
- data/README.md +990 -0
- data/Rakefile +11 -0
- data/UPGRADE.md +81 -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/her.gemspec +30 -0
- data/lib/her.rb +16 -0
- data/lib/her/api.rb +115 -0
- data/lib/her/collection.rb +12 -0
- data/lib/her/errors.rb +27 -0
- data/lib/her/middleware.rb +10 -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/parse_json.rb +21 -0
- data/lib/her/middleware/second_level_parse_json.rb +36 -0
- data/lib/her/model.rb +72 -0
- data/lib/her/model/associations.rb +141 -0
- data/lib/her/model/associations/association.rb +103 -0
- data/lib/her/model/associations/association_proxy.rb +46 -0
- data/lib/her/model/associations/belongs_to_association.rb +96 -0
- data/lib/her/model/associations/has_many_association.rb +100 -0
- data/lib/her/model/associations/has_one_association.rb +79 -0
- data/lib/her/model/attributes.rb +266 -0
- data/lib/her/model/base.rb +33 -0
- data/lib/her/model/deprecated_methods.rb +61 -0
- data/lib/her/model/http.rb +114 -0
- data/lib/her/model/introspection.rb +65 -0
- data/lib/her/model/nested_attributes.rb +45 -0
- data/lib/her/model/orm.rb +205 -0
- data/lib/her/model/parse.rb +227 -0
- data/lib/her/model/paths.rb +121 -0
- data/lib/her/model/relation.rb +164 -0
- data/lib/her/version.rb +3 -0
- data/spec/api_spec.rb +131 -0
- data/spec/collection_spec.rb +26 -0
- data/spec/middleware/accept_json_spec.rb +10 -0
- data/spec/middleware/first_level_parse_json_spec.rb +62 -0
- data/spec/middleware/second_level_parse_json_spec.rb +35 -0
- data/spec/model/associations_spec.rb +416 -0
- data/spec/model/attributes_spec.rb +268 -0
- data/spec/model/callbacks_spec.rb +145 -0
- data/spec/model/dirty_spec.rb +86 -0
- data/spec/model/http_spec.rb +194 -0
- data/spec/model/introspection_spec.rb +76 -0
- data/spec/model/nested_attributes_spec.rb +134 -0
- data/spec/model/orm_spec.rb +479 -0
- data/spec/model/parse_spec.rb +373 -0
- data/spec/model/paths_spec.rb +341 -0
- data/spec/model/relation_spec.rb +226 -0
- data/spec/model/validations_spec.rb +42 -0
- data/spec/model_spec.rb +31 -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 +29 -0
- data/spec/support/macros/request_macros.rb +27 -0
- metadata +280 -0
@@ -0,0 +1,227 @@
|
|
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.attributes, self.changes)
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
# Parse data before assigning it to a resource, based on `parse_root_in_json`.
|
18
|
+
#
|
19
|
+
# @param [Hash] data
|
20
|
+
# @private
|
21
|
+
def parse(data)
|
22
|
+
if parse_root_in_json? && root_element_included?(data)
|
23
|
+
if json_api_format?
|
24
|
+
|
25
|
+
parse_json_api_format(data, parsed_root_element)
|
26
|
+
else
|
27
|
+
data.fetch(parsed_root_element) { data }
|
28
|
+
end
|
29
|
+
else
|
30
|
+
data
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def to_params(attributes, changes={})
|
36
|
+
filtered_attributes = attributes.dup.symbolize_keys
|
37
|
+
filtered_attributes.merge!(embeded_params(attributes))
|
38
|
+
if her_api.options[:send_only_modified_attributes]
|
39
|
+
filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
|
40
|
+
hash[attribute] = filtered_attributes[attribute]
|
41
|
+
hash
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
if include_root_in_json?
|
46
|
+
if json_api_format?
|
47
|
+
{ included_root_element => [filtered_attributes] }
|
48
|
+
else
|
49
|
+
{ included_root_element => filtered_attributes }
|
50
|
+
end
|
51
|
+
else
|
52
|
+
filtered_attributes
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @private
|
57
|
+
def parse_json_api_format(data, root_element)
|
58
|
+
element = data.fetch(root_element)
|
59
|
+
if element.is_a?(Array)
|
60
|
+
element.first
|
61
|
+
else
|
62
|
+
element
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
# @private
|
68
|
+
# TODO: Handle has_one
|
69
|
+
def embeded_params(attributes)
|
70
|
+
associations[:has_many].select { |a| attributes.include?(a[:data_key])}.compact.inject({}) do |hash, association|
|
71
|
+
params = attributes[association[:data_key]].map(&:to_params)
|
72
|
+
next if params.empty?
|
73
|
+
if association[:class_name].constantize.include_root_in_json?
|
74
|
+
root = association[:class_name].constantize.root_element
|
75
|
+
hash[association[:data_key]] = params.map { |n| n[root] }
|
76
|
+
else
|
77
|
+
hash[association[:data_key]] = params
|
78
|
+
end
|
79
|
+
hash
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return or change the value of `include_root_in_json`
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# class User
|
87
|
+
# include Her::Model
|
88
|
+
# include_root_in_json true
|
89
|
+
# end
|
90
|
+
def include_root_in_json(value, options = {})
|
91
|
+
@_her_include_root_in_json = value
|
92
|
+
@_her_include_root_in_json_format = options[:format]
|
93
|
+
end
|
94
|
+
|
95
|
+
# Return or change the value of `parse_root_in_json`
|
96
|
+
#
|
97
|
+
# @example
|
98
|
+
# class User
|
99
|
+
# include Her::Model
|
100
|
+
# parse_root_in_json true
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# class User
|
104
|
+
# include Her::Model
|
105
|
+
# parse_root_in_json true, format: :active_model_serializers
|
106
|
+
# end
|
107
|
+
#
|
108
|
+
# class User
|
109
|
+
# include Her::Model
|
110
|
+
# parse_root_in_json true, format: :json_api
|
111
|
+
# end
|
112
|
+
def parse_root_in_json(value, options = {})
|
113
|
+
@_her_parse_root_in_json = value
|
114
|
+
@_her_parse_root_in_json_format = options[:format]
|
115
|
+
end
|
116
|
+
|
117
|
+
# Return or change the value of `request_new_object_on_build`
|
118
|
+
#
|
119
|
+
# @example
|
120
|
+
# class User
|
121
|
+
# include Her::Model
|
122
|
+
# request_new_object_on_build true
|
123
|
+
# end
|
124
|
+
def request_new_object_on_build(value = nil)
|
125
|
+
@_her_request_new_object_on_build = value
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return or change the value of `root_element`. Always defaults to the base name of the class.
|
129
|
+
#
|
130
|
+
# @example
|
131
|
+
# class User
|
132
|
+
# include Her::Model
|
133
|
+
# parse_root_in_json true
|
134
|
+
# root_element :huh
|
135
|
+
# end
|
136
|
+
#
|
137
|
+
# user = User.find(1) # { :huh => { :id => 1, :name => "Tobias" } }
|
138
|
+
# user.name # => "Tobias"
|
139
|
+
def root_element(value = nil)
|
140
|
+
if value.nil?
|
141
|
+
if json_api_format?
|
142
|
+
@_her_root_element ||= self.name.split("::").last.pluralize.underscore.to_sym
|
143
|
+
else
|
144
|
+
@_her_root_element ||= self.name.split("::").last.underscore.to_sym
|
145
|
+
end
|
146
|
+
else
|
147
|
+
@_her_root_element = value.to_sym
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# @private
|
152
|
+
def root_element_included?(data)
|
153
|
+
data.keys.to_s.include? @_her_root_element.to_s
|
154
|
+
end
|
155
|
+
|
156
|
+
# @private
|
157
|
+
def included_root_element
|
158
|
+
include_root_in_json? == true ? root_element : include_root_in_json?
|
159
|
+
end
|
160
|
+
|
161
|
+
# Extract an array from the request data
|
162
|
+
#
|
163
|
+
# @example
|
164
|
+
# # with parse_root_in_json true, :format => :active_model_serializers
|
165
|
+
# class User
|
166
|
+
# include Her::Model
|
167
|
+
# parse_root_in_json true, :format => :active_model_serializers
|
168
|
+
# end
|
169
|
+
#
|
170
|
+
# users = User.all # { :users => [ { :id => 1, :name => "Tobias" } ] }
|
171
|
+
# users.first.name # => "Tobias"
|
172
|
+
#
|
173
|
+
# # without parse_root_in_json
|
174
|
+
# class User
|
175
|
+
# include Her::Model
|
176
|
+
# end
|
177
|
+
#
|
178
|
+
# users = User.all # [ { :id => 1, :name => "Tobias" } ]
|
179
|
+
# users.first.name # => "Tobias"
|
180
|
+
#
|
181
|
+
# @private
|
182
|
+
def extract_array(request_data)
|
183
|
+
if active_model_serializers_format? || json_api_format?
|
184
|
+
request_data[:data][pluralized_parsed_root_element]
|
185
|
+
else
|
186
|
+
request_data[:data]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# @private
|
191
|
+
def pluralized_parsed_root_element
|
192
|
+
parsed_root_element.to_s.pluralize.to_sym
|
193
|
+
end
|
194
|
+
|
195
|
+
# @private
|
196
|
+
def parsed_root_element
|
197
|
+
parse_root_in_json? == true ? root_element : parse_root_in_json?
|
198
|
+
end
|
199
|
+
|
200
|
+
# @private
|
201
|
+
def active_model_serializers_format?
|
202
|
+
@_her_parse_root_in_json_format == :active_model_serializers || (superclass.respond_to?(:active_model_serializers_format?) && superclass.active_model_serializers_format?)
|
203
|
+
end
|
204
|
+
|
205
|
+
# @private
|
206
|
+
def json_api_format?
|
207
|
+
@_her_parse_root_in_json_format == :json_api || (superclass.respond_to?(:json_api_format?) && superclass.json_api_format?)
|
208
|
+
end
|
209
|
+
|
210
|
+
# @private
|
211
|
+
def request_new_object_on_build?
|
212
|
+
@_her_request_new_object_on_build || (superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?)
|
213
|
+
end
|
214
|
+
|
215
|
+
# @private
|
216
|
+
def include_root_in_json?
|
217
|
+
@_her_include_root_in_json || (superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?)
|
218
|
+
end
|
219
|
+
|
220
|
+
# @private
|
221
|
+
def parse_root_in_json?
|
222
|
+
@_her_parse_root_in_json || (superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
@@ -0,0 +1,121 @@
|
|
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
|
+
value = $1.to_sym
|
110
|
+
parameters.delete(value) || parameters.delete(:"_#{value}") || raise(Her::Errors::PathError.new("Missing :_#{$1} parameter to build the request path. Path is `#{path}`. Parameters are `#{parameters.symbolize_keys.inspect}`.", $1))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# @private
|
115
|
+
def build_request_path_from_string_or_symbol(path, params={})
|
116
|
+
path.is_a?(Symbol) ? "#{build_request_path(params)}/#{path}" : path
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
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
|