duck_record 0.0.8 → 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/MIT-LICENSE +21 -0
- data/lib/duck_record/associations/association.rb +5 -0
- data/lib/duck_record/associations/collection_proxy.rb +2 -4
- data/lib/duck_record/base.rb +19 -0
- data/lib/duck_record/nested_attributes.rb +21 -21
- data/lib/duck_record/validations.rb +2 -0
- data/lib/duck_record/validations/uniqueness_on_real_record.rb +248 -0
- data/lib/duck_record/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ecea50d445f7446966d18efc45e498a6748e965
|
4
|
+
data.tar.gz: b218dff359b164ae64f38663b756f9f5de023262
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b296929403c1a0753a179b07db103788861968c0842d02d9bd22f88e4bcb8937755b2975c11d80940e6d02427d6e42c4582166d98c3ee08b5390153b02d1c159
|
7
|
+
data.tar.gz: e96ee2e901aa8d146ea730c41d8fa817d0d290d3dcefa6448a21b87e2c68798e8544d075b62f2aa4211bab85b916a0207c32719f7aecdba681d005e61623430f
|
data/MIT-LICENSE
CHANGED
@@ -18,3 +18,24 @@ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
18
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
19
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
Copyright (c) 2004-2017 David Heinemeier Hansson
|
23
|
+
|
24
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
25
|
+
a copy of this software and associated documentation files (the
|
26
|
+
"Software"), to deal in the Software without restriction, including
|
27
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
28
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
29
|
+
permit persons to whom the Software is furnished to do so, subject to
|
30
|
+
the following conditions:
|
31
|
+
|
32
|
+
The above copyright notice and this permission notice shall be
|
33
|
+
included in all copies or substantial portions of the Software.
|
34
|
+
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
36
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
37
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
38
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
39
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
40
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
41
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -33,6 +33,11 @@ module DuckRecord
|
|
33
33
|
@target = nil
|
34
34
|
end
|
35
35
|
|
36
|
+
# Has the \target been already \loaded?
|
37
|
+
def loaded?
|
38
|
+
!!@target
|
39
|
+
end
|
40
|
+
|
36
41
|
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
|
37
42
|
def target=(target)
|
38
43
|
@target = target
|
@@ -35,6 +35,8 @@ module DuckRecord
|
|
35
35
|
:to_sentence, :to_formatted_s,
|
36
36
|
:shuffle, :split, :index, to: :records
|
37
37
|
|
38
|
+
delegate :target, :loaded?, to: :@association
|
39
|
+
|
38
40
|
attr_reader :klass
|
39
41
|
alias :model :klass
|
40
42
|
|
@@ -43,10 +45,6 @@ module DuckRecord
|
|
43
45
|
@association = association
|
44
46
|
end
|
45
47
|
|
46
|
-
def target
|
47
|
-
@association.target
|
48
|
-
end
|
49
|
-
|
50
48
|
##
|
51
49
|
# :method: first
|
52
50
|
#
|
data/lib/duck_record/base.rb
CHANGED
@@ -299,6 +299,25 @@ module DuckRecord #:nodoc:
|
|
299
299
|
def new_record?
|
300
300
|
true
|
301
301
|
end
|
302
|
+
|
303
|
+
def to_h(include_empty: true)
|
304
|
+
hash = serializable_hash
|
305
|
+
|
306
|
+
self.class.reflections.keys.each do |k|
|
307
|
+
records = send(k)
|
308
|
+
sub_hash = if records.respond_to?(:to_ary)
|
309
|
+
records.to_ary.map { |a| a.to_h }
|
310
|
+
else
|
311
|
+
records.to_h
|
312
|
+
end
|
313
|
+
|
314
|
+
if include_empty || sub_hash.any?
|
315
|
+
hash[k] = sub_hash
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
hash
|
320
|
+
end
|
302
321
|
end
|
303
322
|
|
304
323
|
ActiveSupport.run_load_hooks(:duck_record, Base)
|
@@ -340,27 +340,27 @@ module DuckRecord
|
|
340
340
|
|
341
341
|
private
|
342
342
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
343
|
+
# Generates a writer method for this association. Serves as a point for
|
344
|
+
# accessing the objects in the association. For example, this method
|
345
|
+
# could generate the following:
|
346
|
+
#
|
347
|
+
# def pirate_attributes=(attributes)
|
348
|
+
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
|
349
|
+
# end
|
350
|
+
#
|
351
|
+
# This redirects the attempts to write objects in an association through
|
352
|
+
# the helper methods defined below. Makes it seem like the nested
|
353
|
+
# associations are just regular associations.
|
354
|
+
def generate_association_writer(association_name, type)
|
355
|
+
generated_association_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
|
356
|
+
if method_defined?(:#{association_name}_attributes=)
|
357
|
+
remove_method(:#{association_name}_attributes=)
|
358
|
+
end
|
359
|
+
def #{association_name}_attributes=(attributes)
|
360
|
+
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
361
|
+
end
|
362
|
+
eoruby
|
363
|
+
end
|
364
364
|
end
|
365
365
|
|
366
366
|
# Marks this record to be destroyed as part of the parent's save transaction.
|
@@ -0,0 +1,248 @@
|
|
1
|
+
module DuckRecord
|
2
|
+
module Validations
|
3
|
+
class UniquenessOnRealRecordValidator < ActiveModel::EachValidator # :nodoc:
|
4
|
+
def initialize(options)
|
5
|
+
unless defined?(ActiveRecord)
|
6
|
+
raise NameError, "Not found Active Record."
|
7
|
+
end
|
8
|
+
|
9
|
+
if options[:conditions] && !options[:conditions].respond_to?(:call)
|
10
|
+
raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
|
11
|
+
"Pass a callable instead: `conditions: -> { where(approved: true) }`"
|
12
|
+
end
|
13
|
+
|
14
|
+
@finder_class = if options[:class_name].present?
|
15
|
+
@finder_class = options[:class_name].safe_constantize
|
16
|
+
elsif options[:class].present? && options[:class].is_a?(Class)
|
17
|
+
@finder_class = options[:class]
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
unless @finder_class
|
23
|
+
raise ArgumentError, "Must provide one of option :class_name or :class."
|
24
|
+
end
|
25
|
+
unless @finder_class < ActiveRecord::Base
|
26
|
+
raise ArgumentError, ":class must be an Active Record model, but got #{@finder_class}."
|
27
|
+
end
|
28
|
+
if @finder_class.abstract_class?
|
29
|
+
raise ArgumentError, ":class can't be an abstract class."
|
30
|
+
end
|
31
|
+
|
32
|
+
@primary_key = options[:primary_key]
|
33
|
+
|
34
|
+
super({ case_sensitive: true }.merge!(options))
|
35
|
+
end
|
36
|
+
|
37
|
+
def validate_each(record, attribute, value)
|
38
|
+
table = @finder_class.arel_table
|
39
|
+
value = map_enum_attribute(@finder_class, attribute, value)
|
40
|
+
|
41
|
+
relation = build_relation(@finder_class, table, attribute, value)
|
42
|
+
if @primary_key.present? && record.respond_to?(@primary_key)
|
43
|
+
if @finder_class.primary_key
|
44
|
+
relation = relation.where.not(@finder_class.primary_key => record.send(@primary_key))
|
45
|
+
else
|
46
|
+
raise ActiveRecord::UnknownPrimaryKey.new(@finder_class, "Can not validate uniqueness for persisted record without primary key.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
relation = scope_relation(record, table, relation)
|
50
|
+
relation = relation.merge(options[:conditions]) if options[:conditions]
|
51
|
+
|
52
|
+
if relation.exists?
|
53
|
+
error_options = options.except(:case_sensitive, :scope, :conditions)
|
54
|
+
error_options[:value] = value
|
55
|
+
|
56
|
+
record.errors.add(attribute, :taken, error_options)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
|
62
|
+
def build_relation(klass, table, attribute, value) #:nodoc:
|
63
|
+
if reflection = klass._reflect_on_association(attribute)
|
64
|
+
attribute = reflection.foreign_key
|
65
|
+
value = value.attributes[reflection.klass.primary_key] unless value.nil?
|
66
|
+
end
|
67
|
+
|
68
|
+
# the attribute may be an aliased attribute
|
69
|
+
if klass.attribute_alias?(attribute)
|
70
|
+
attribute = klass.attribute_alias(attribute)
|
71
|
+
end
|
72
|
+
|
73
|
+
attribute_name = attribute.to_s
|
74
|
+
|
75
|
+
column = klass.columns_hash[attribute_name]
|
76
|
+
cast_type = klass.type_for_attribute(attribute_name)
|
77
|
+
value = cast_type.serialize(value)
|
78
|
+
value = klass.connection.type_cast(value)
|
79
|
+
|
80
|
+
comparison = if !options[:case_sensitive] && !value.nil?
|
81
|
+
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
|
82
|
+
klass.connection.case_insensitive_comparison(table, attribute, column, value)
|
83
|
+
else
|
84
|
+
klass.connection.case_sensitive_comparison(table, attribute, column, value)
|
85
|
+
end
|
86
|
+
if value.nil?
|
87
|
+
klass.unscoped.where(comparison)
|
88
|
+
else
|
89
|
+
bind = ActiveRecord::Relation::QueryAttribute.new(attribute_name, value, ActiveRecord::Type::Value.new)
|
90
|
+
klass.unscoped.where(comparison, bind)
|
91
|
+
end
|
92
|
+
rescue RangeError
|
93
|
+
klass.none
|
94
|
+
end
|
95
|
+
|
96
|
+
def scope_relation(record, table, relation)
|
97
|
+
Array(options[:scope]).each do |scope_item|
|
98
|
+
scope_value = if record.class._reflect_on_association(scope_item)
|
99
|
+
record.association(scope_item).reader
|
100
|
+
else
|
101
|
+
record._read_attribute(scope_item)
|
102
|
+
end
|
103
|
+
relation = relation.where(scope_item => scope_value)
|
104
|
+
end
|
105
|
+
|
106
|
+
relation
|
107
|
+
end
|
108
|
+
|
109
|
+
def map_enum_attribute(klass, attribute, value)
|
110
|
+
mapping = klass.defined_enums[attribute.to_s]
|
111
|
+
value = mapping[value] if value && mapping
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
module ClassMethods
|
117
|
+
# Validates whether the value of the specified attributes are unique
|
118
|
+
# across the system. Useful for making sure that only one user
|
119
|
+
# can be named "davidhh".
|
120
|
+
#
|
121
|
+
# class Person < ActiveRecord::Base
|
122
|
+
# validates_uniqueness_of :user_name
|
123
|
+
# end
|
124
|
+
#
|
125
|
+
# It can also validate whether the value of the specified attributes are
|
126
|
+
# unique based on a <tt>:scope</tt> parameter:
|
127
|
+
#
|
128
|
+
# class Person < ActiveRecord::Base
|
129
|
+
# validates_uniqueness_of :user_name, scope: :account_id
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# Or even multiple scope parameters. For example, making sure that a
|
133
|
+
# teacher can only be on the schedule once per semester for a particular
|
134
|
+
# class.
|
135
|
+
#
|
136
|
+
# class TeacherSchedule < ActiveRecord::Base
|
137
|
+
# validates_uniqueness_of :teacher_id, scope: [:semester_id, :class_id]
|
138
|
+
# end
|
139
|
+
#
|
140
|
+
# It is also possible to limit the uniqueness constraint to a set of
|
141
|
+
# records matching certain conditions. In this example archived articles
|
142
|
+
# are not being taken into consideration when validating uniqueness
|
143
|
+
# of the title attribute:
|
144
|
+
#
|
145
|
+
# class Article < ActiveRecord::Base
|
146
|
+
# validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
|
147
|
+
# end
|
148
|
+
#
|
149
|
+
# When the record is created, a check is performed to make sure that no
|
150
|
+
# record exists in the database with the given value for the specified
|
151
|
+
# attribute (that maps to a column). When the record is updated,
|
152
|
+
# the same check is made but disregarding the record itself.
|
153
|
+
#
|
154
|
+
# Configuration options:
|
155
|
+
#
|
156
|
+
# * <tt>:message</tt> - Specifies a custom error message (default is:
|
157
|
+
# "has already been taken").
|
158
|
+
# * <tt>:scope</tt> - One or more columns by which to limit the scope of
|
159
|
+
# the uniqueness constraint.
|
160
|
+
# * <tt>:conditions</tt> - Specify the conditions to be included as a
|
161
|
+
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
|
162
|
+
# (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
|
163
|
+
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
|
164
|
+
# non-text columns (+true+ by default).
|
165
|
+
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
|
166
|
+
# attribute is +nil+ (default is +false+).
|
167
|
+
# * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
|
168
|
+
# attribute is blank (default is +false+).
|
169
|
+
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
|
170
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
171
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
172
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
173
|
+
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
|
174
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
175
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
176
|
+
# method, proc or string should return or evaluate to a +true+ or +false+
|
177
|
+
# value.
|
178
|
+
#
|
179
|
+
# === Concurrency and integrity
|
180
|
+
#
|
181
|
+
# Using this validation method in conjunction with
|
182
|
+
# {ActiveRecord::Base#save}[rdoc-ref:Persistence#save]
|
183
|
+
# does not guarantee the absence of duplicate record insertions, because
|
184
|
+
# uniqueness checks on the application level are inherently prone to race
|
185
|
+
# conditions. For example, suppose that two users try to post a Comment at
|
186
|
+
# the same time, and a Comment's title must be unique. At the database-level,
|
187
|
+
# the actions performed by these users could be interleaved in the following manner:
|
188
|
+
#
|
189
|
+
# User 1 | User 2
|
190
|
+
# ------------------------------------+--------------------------------------
|
191
|
+
# # User 1 checks whether there's |
|
192
|
+
# # already a comment with the title |
|
193
|
+
# # 'My Post'. This is not the case. |
|
194
|
+
# SELECT * FROM comments |
|
195
|
+
# WHERE title = 'My Post' |
|
196
|
+
# |
|
197
|
+
# | # User 2 does the same thing and also
|
198
|
+
# | # infers that their title is unique.
|
199
|
+
# | SELECT * FROM comments
|
200
|
+
# | WHERE title = 'My Post'
|
201
|
+
# |
|
202
|
+
# # User 1 inserts their comment. |
|
203
|
+
# INSERT INTO comments |
|
204
|
+
# (title, content) VALUES |
|
205
|
+
# ('My Post', 'hi!') |
|
206
|
+
# |
|
207
|
+
# | # User 2 does the same thing.
|
208
|
+
# | INSERT INTO comments
|
209
|
+
# | (title, content) VALUES
|
210
|
+
# | ('My Post', 'hello!')
|
211
|
+
# |
|
212
|
+
# | # ^^^^^^
|
213
|
+
# | # Boom! We now have a duplicate
|
214
|
+
# | # title!
|
215
|
+
#
|
216
|
+
# This could even happen if you use transactions with the 'serializable'
|
217
|
+
# isolation level. The best way to work around this problem is to add a unique
|
218
|
+
# index to the database table using
|
219
|
+
# {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
|
220
|
+
# In the rare case that a race condition occurs, the database will guarantee
|
221
|
+
# the field's uniqueness.
|
222
|
+
#
|
223
|
+
# When the database catches such a duplicate insertion,
|
224
|
+
# {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will raise an ActiveRecord::StatementInvalid
|
225
|
+
# exception. You can either choose to let this error propagate (which
|
226
|
+
# will result in the default Rails exception page being shown), or you
|
227
|
+
# can catch it and restart the transaction (e.g. by telling the user
|
228
|
+
# that the title already exists, and asking them to re-enter the title).
|
229
|
+
# This technique is also known as
|
230
|
+
# {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
|
231
|
+
#
|
232
|
+
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
|
233
|
+
# constraint errors from other types of database errors by throwing an
|
234
|
+
# ActiveRecord::RecordNotUnique exception. For other adapters you will
|
235
|
+
# have to parse the (database-specific) exception message to detect such
|
236
|
+
# a case.
|
237
|
+
#
|
238
|
+
# The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
|
239
|
+
#
|
240
|
+
# * ActiveRecord::ConnectionAdapters::Mysql2Adapter.
|
241
|
+
# * ActiveRecord::ConnectionAdapters::SQLite3Adapter.
|
242
|
+
# * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.
|
243
|
+
def validates_uniqueness_on_real_record_of(*attr_names)
|
244
|
+
validates_with UniquenessOnRealRecordValidator, _merge_attributes(attr_names)
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
data/lib/duck_record/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duck_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- jasl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '5.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
description: "It looks like Active Record and quacks like Active Record, but it can't
|
56
70
|
do persistence or querying,\n it's Duck Record! \n Actually it's extract from
|
57
71
|
Active Record.\n Used for creating virtual models like ActiveType or ModelAttribute
|
@@ -118,6 +132,7 @@ files:
|
|
118
132
|
- lib/duck_record/type/time.rb
|
119
133
|
- lib/duck_record/type/unsigned_integer.rb
|
120
134
|
- lib/duck_record/validations.rb
|
135
|
+
- lib/duck_record/validations/uniqueness_on_real_record.rb
|
121
136
|
- lib/duck_record/version.rb
|
122
137
|
- lib/tasks/acts_as_record_tasks.rake
|
123
138
|
homepage: https://github.com/jasl/duck_record
|