ardm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +35 -0
- data/Gemfile +13 -0
- data/LICENSE +21 -0
- data/README.md +70 -0
- data/Rakefile +4 -0
- data/ardm.gemspec +29 -0
- data/db/.gitignore +1 -0
- data/lib/ardm/active_record/associations.rb +80 -0
- data/lib/ardm/active_record/base.rb +49 -0
- data/lib/ardm/active_record/dirty.rb +25 -0
- data/lib/ardm/active_record/hooks.rb +31 -0
- data/lib/ardm/active_record/inheritance.rb +37 -0
- data/lib/ardm/active_record/is/state_machine.rb +21 -0
- data/lib/ardm/active_record/is.rb +22 -0
- data/lib/ardm/active_record/not_found.rb +7 -0
- data/lib/ardm/active_record/predicate_builder/array_handler.rb +31 -0
- data/lib/ardm/active_record/predicate_builder/rails3.rb +147 -0
- data/lib/ardm/active_record/predicate_builder/rails4.rb +139 -0
- data/lib/ardm/active_record/predicate_builder/relation_handler.rb +15 -0
- data/lib/ardm/active_record/predicate_builder.rb +19 -0
- data/lib/ardm/active_record/property.rb +357 -0
- data/lib/ardm/active_record/query.rb +108 -0
- data/lib/ardm/active_record/record.rb +70 -0
- data/lib/ardm/active_record/relation.rb +83 -0
- data/lib/ardm/active_record/repository.rb +38 -0
- data/lib/ardm/active_record/serialization.rb +164 -0
- data/lib/ardm/active_record/storage_names.rb +28 -0
- data/lib/ardm/active_record/validations.rb +111 -0
- data/lib/ardm/active_record.rb +43 -0
- data/lib/ardm/data_mapper/not_found.rb +5 -0
- data/lib/ardm/data_mapper/record.rb +41 -0
- data/lib/ardm/data_mapper.rb +5 -0
- data/lib/ardm/env.rb +5 -0
- data/lib/ardm/property/api_key.rb +30 -0
- data/lib/ardm/property/bcrypt_hash.rb +31 -0
- data/lib/ardm/property/binary.rb +23 -0
- data/lib/ardm/property/boolean.rb +29 -0
- data/lib/ardm/property/class.rb +19 -0
- data/lib/ardm/property/comma_separated_list.rb +28 -0
- data/lib/ardm/property/csv.rb +35 -0
- data/lib/ardm/property/date.rb +12 -0
- data/lib/ardm/property/datetime.rb +12 -0
- data/lib/ardm/property/decimal.rb +38 -0
- data/lib/ardm/property/discriminator.rb +65 -0
- data/lib/ardm/property/enum.rb +51 -0
- data/lib/ardm/property/epoch_time.rb +38 -0
- data/lib/ardm/property/file_path.rb +25 -0
- data/lib/ardm/property/flag.rb +65 -0
- data/lib/ardm/property/float.rb +18 -0
- data/lib/ardm/property/integer.rb +24 -0
- data/lib/ardm/property/invalid_value_error.rb +22 -0
- data/lib/ardm/property/ip_address.rb +35 -0
- data/lib/ardm/property/json.rb +49 -0
- data/lib/ardm/property/lookup.rb +29 -0
- data/lib/ardm/property/numeric.rb +40 -0
- data/lib/ardm/property/object.rb +36 -0
- data/lib/ardm/property/paranoid_boolean.rb +18 -0
- data/lib/ardm/property/paranoid_datetime.rb +17 -0
- data/lib/ardm/property/regexp.rb +22 -0
- data/lib/ardm/property/serial.rb +16 -0
- data/lib/ardm/property/slug.rb +29 -0
- data/lib/ardm/property/string.rb +40 -0
- data/lib/ardm/property/support/dirty_minder.rb +169 -0
- data/lib/ardm/property/support/flags.rb +41 -0
- data/lib/ardm/property/support/paranoid_base.rb +78 -0
- data/lib/ardm/property/text.rb +11 -0
- data/lib/ardm/property/time.rb +12 -0
- data/lib/ardm/property/uri.rb +27 -0
- data/lib/ardm/property/uuid.rb +65 -0
- data/lib/ardm/property/validation.rb +208 -0
- data/lib/ardm/property/yaml.rb +38 -0
- data/lib/ardm/property.rb +891 -0
- data/lib/ardm/property_set.rb +152 -0
- data/lib/ardm/query/expression.rb +85 -0
- data/lib/ardm/query/ext/symbol.rb +37 -0
- data/lib/ardm/query/operator.rb +64 -0
- data/lib/ardm/record.rb +1 -0
- data/lib/ardm/support/assertions.rb +8 -0
- data/lib/ardm/support/deprecate.rb +12 -0
- data/lib/ardm/support/descendant_set.rb +89 -0
- data/lib/ardm/support/equalizer.rb +48 -0
- data/lib/ardm/support/ext/array.rb +22 -0
- data/lib/ardm/support/ext/blank.rb +25 -0
- data/lib/ardm/support/ext/hash.rb +67 -0
- data/lib/ardm/support/ext/module.rb +47 -0
- data/lib/ardm/support/ext/object.rb +57 -0
- data/lib/ardm/support/ext/string.rb +24 -0
- data/lib/ardm/support/ext/try_dup.rb +12 -0
- data/lib/ardm/support/hook.rb +405 -0
- data/lib/ardm/support/lazy_array.rb +451 -0
- data/lib/ardm/support/local_object_space.rb +13 -0
- data/lib/ardm/support/logger.rb +201 -0
- data/lib/ardm/support/mash.rb +176 -0
- data/lib/ardm/support/naming_conventions.rb +90 -0
- data/lib/ardm/support/ordered_set.rb +380 -0
- data/lib/ardm/support/subject.rb +33 -0
- data/lib/ardm/support/subject_set.rb +250 -0
- data/lib/ardm/version.rb +3 -0
- data/lib/ardm.rb +56 -0
- data/spec/fixtures/api_user.rb +11 -0
- data/spec/fixtures/article.rb +22 -0
- data/spec/fixtures/bookmark.rb +14 -0
- data/spec/fixtures/invention.rb +5 -0
- data/spec/fixtures/network_node.rb +23 -0
- data/spec/fixtures/person.rb +17 -0
- data/spec/fixtures/software_package.rb +22 -0
- data/spec/fixtures/ticket.rb +12 -0
- data/spec/fixtures/tshirt.rb +15 -0
- data/spec/integration/api_key_spec.rb +25 -0
- data/spec/integration/bcrypt_hash_spec.rb +45 -0
- data/spec/integration/comma_separated_list_spec.rb +85 -0
- data/spec/integration/dirty_minder_spec.rb +197 -0
- data/spec/integration/enum_spec.rb +79 -0
- data/spec/integration/epoch_time_spec.rb +59 -0
- data/spec/integration/file_path_spec.rb +158 -0
- data/spec/integration/flag_spec.rb +72 -0
- data/spec/integration/ip_address_spec.rb +151 -0
- data/spec/integration/json_spec.rb +70 -0
- data/spec/integration/slug_spec.rb +65 -0
- data/spec/integration/uri_spec.rb +136 -0
- data/spec/integration/uuid_spec.rb +102 -0
- data/spec/integration/yaml_spec.rb +85 -0
- data/spec/public/property/binary_spec.rb +41 -0
- data/spec/public/property/boolean_spec.rb +30 -0
- data/spec/public/property/class_spec.rb +28 -0
- data/spec/public/property/date_spec.rb +22 -0
- data/spec/public/property/date_time_spec.rb +22 -0
- data/spec/public/property/decimal_spec.rb +23 -0
- data/spec/public/property/discriminator_spec.rb +133 -0
- data/spec/public/property/float_spec.rb +22 -0
- data/spec/public/property/integer_spec.rb +22 -0
- data/spec/public/property/object_spec.rb +103 -0
- data/spec/public/property/serial_spec.rb +22 -0
- data/spec/public/property/string_spec.rb +22 -0
- data/spec/public/property/text_spec.rb +23 -0
- data/spec/public/property/time_spec.rb +22 -0
- data/spec/public/property_spec.rb +316 -0
- data/spec/rcov.opts +6 -0
- data/spec/schema.rb +86 -0
- data/spec/semipublic/property/binary_spec.rb +14 -0
- data/spec/semipublic/property/boolean_spec.rb +48 -0
- data/spec/semipublic/property/class_spec.rb +36 -0
- data/spec/semipublic/property/date_spec.rb +44 -0
- data/spec/semipublic/property/date_time_spec.rb +47 -0
- data/spec/semipublic/property/decimal_spec.rb +83 -0
- data/spec/semipublic/property/discriminator_spec.rb +22 -0
- data/spec/semipublic/property/float_spec.rb +83 -0
- data/spec/semipublic/property/integer_spec.rb +83 -0
- data/spec/semipublic/property/lookup_spec.rb +27 -0
- data/spec/semipublic/property/serial_spec.rb +14 -0
- data/spec/semipublic/property/string_spec.rb +14 -0
- data/spec/semipublic/property/text_spec.rb +30 -0
- data/spec/semipublic/property/time_spec.rb +49 -0
- data/spec/semipublic/property_spec.rb +51 -0
- data/spec/shared/flags_shared_spec.rb +36 -0
- data/spec/shared/identity_function_group.rb +5 -0
- data/spec/shared/public_property_spec.rb +229 -0
- data/spec/shared/semipublic_property_spec.rb +159 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/unit/bcrypt_hash_spec.rb +154 -0
- data/spec/unit/csv_spec.rb +139 -0
- data/spec/unit/dirty_minder_spec.rb +64 -0
- data/spec/unit/enum_spec.rb +125 -0
- data/spec/unit/epoch_time_spec.rb +72 -0
- data/spec/unit/file_path_spec.rb +75 -0
- data/spec/unit/flag_spec.rb +114 -0
- data/spec/unit/ip_address_spec.rb +109 -0
- data/spec/unit/json_spec.rb +127 -0
- data/spec/unit/paranoid_boolean_spec.rb +142 -0
- data/spec/unit/paranoid_datetime_spec.rb +149 -0
- data/spec/unit/regexp_spec.rb +62 -0
- data/spec/unit/uri_spec.rb +64 -0
- data/spec/unit/yaml_spec.rb +111 -0
- data/tasks/spec.rake +40 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +350 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ardm/property/string'
|
2
|
+
require 'stringex'
|
3
|
+
|
4
|
+
module Ardm
|
5
|
+
class Property
|
6
|
+
class Slug < String
|
7
|
+
|
8
|
+
# Maximum length chosen because URI type is limited to 2000
|
9
|
+
# characters, and a slug is a component of a URI, so it should
|
10
|
+
# not exceed the maximum URI length either.
|
11
|
+
length 2000
|
12
|
+
|
13
|
+
def typecast(value)
|
14
|
+
if value.nil?
|
15
|
+
nil
|
16
|
+
elsif value.respond_to?(:to_str)
|
17
|
+
escape(value.to_str)
|
18
|
+
else
|
19
|
+
raise ArgumentError, '+value+ must be nil or respond to #to_str'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def escape(string)
|
24
|
+
string.to_url
|
25
|
+
end
|
26
|
+
|
27
|
+
end # class Slug
|
28
|
+
end # class Property
|
29
|
+
end # module Ardm
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'ardm/property/object'
|
2
|
+
|
3
|
+
module Ardm
|
4
|
+
class Property
|
5
|
+
class String < Object
|
6
|
+
load_as ::String
|
7
|
+
dump_as ::String
|
8
|
+
coercion_method :to_string
|
9
|
+
|
10
|
+
accept_options :length
|
11
|
+
|
12
|
+
DEFAULT_LENGTH = 50
|
13
|
+
length(DEFAULT_LENGTH)
|
14
|
+
|
15
|
+
# Returns maximum property length (if applicable).
|
16
|
+
# This usually only makes sense when property is of
|
17
|
+
# type Range or custom
|
18
|
+
#
|
19
|
+
# @return [Integer, nil]
|
20
|
+
# the maximum length of this property
|
21
|
+
#
|
22
|
+
# @api semipublic
|
23
|
+
def length
|
24
|
+
if @length.kind_of?(Range)
|
25
|
+
@length.max
|
26
|
+
else
|
27
|
+
@length
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def initialize(model, name, options = {})
|
34
|
+
super
|
35
|
+
@length = @options.fetch(:length)
|
36
|
+
end
|
37
|
+
|
38
|
+
end # class String
|
39
|
+
end # class Property
|
40
|
+
end # module Ardm
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# Approach
|
2
|
+
#
|
3
|
+
# We need to detect whether or not the underlying Hash or Array changed and
|
4
|
+
# update the dirty-ness of the encapsulating Resource accordingly (so that it
|
5
|
+
# will actually save).
|
6
|
+
#
|
7
|
+
# DM's state-tracking code only triggers dirty-ness by comparing the new value
|
8
|
+
# against the instance's Property's current value. WRT mutation, we have to
|
9
|
+
# choose one of the following approaches:
|
10
|
+
#
|
11
|
+
# (1) mutate a copy ("after"), then invoke the Resource assignment and State
|
12
|
+
# tracking
|
13
|
+
#
|
14
|
+
# (2) create a copy ("before"), mutate self ("after"), then invoke the
|
15
|
+
# Resource assignment and State tracking
|
16
|
+
#
|
17
|
+
# (1) seemed simpler at first, but it required additional steps to alias the
|
18
|
+
# original (pre-hooked) methods before overriding them (so they could be invoked
|
19
|
+
# externally, ala self.clone.send("orig_...")), and more importantly it resulted
|
20
|
+
# in any external references keeping their old value (instead of getting the
|
21
|
+
# new), like so:
|
22
|
+
#
|
23
|
+
# copy = instance.json
|
24
|
+
# copy[:some] = :value
|
25
|
+
# instance.json[:some] == :value
|
26
|
+
# => true
|
27
|
+
# copy[:some] == :value
|
28
|
+
# => false # fk!
|
29
|
+
#
|
30
|
+
# In order to do (2) and still have State tracking trigger normally, we need to
|
31
|
+
# ensure the Property has a different value other than self when the State
|
32
|
+
# tracking does the comparison. This equates to setting the Property directly
|
33
|
+
# to the "before" value (a clone and thus a different object/value) before
|
34
|
+
# invoking the Resource Property/attribute assignment.
|
35
|
+
#
|
36
|
+
# The cloning of any value might sound expensive, but it's identical in cost to
|
37
|
+
# what you already had to do: assign a cloned copy in order to trigger
|
38
|
+
# dirty-ness (e.g. ::Ardm::Property::Json):
|
39
|
+
#
|
40
|
+
# model.json = model.json.merge({:some=>:value})
|
41
|
+
#
|
42
|
+
# Hooking Core Classes
|
43
|
+
#
|
44
|
+
# We want to hook certain methods on Hash and Array to trigger dirty-ness in the
|
45
|
+
# resource. However, because these are core classes, they are individually
|
46
|
+
# mapped to C primitives and thus cannot be hooked through #send/#__send__. We
|
47
|
+
# have to override each method, but we don't want to write a lot of code.
|
48
|
+
#
|
49
|
+
# Minimally Invasive
|
50
|
+
#
|
51
|
+
# We also want to extend behaviour of existing class instances instead of
|
52
|
+
# impersonating/delegating from a proxy class of our own, or overriding a global
|
53
|
+
# class behaviour. This is the most flexible approach and least prone to error,
|
54
|
+
# since it leaves open the option for consumers to proxy or override global
|
55
|
+
# classes, and is less likely to interfere with method_missing/etc shenanigans.
|
56
|
+
#
|
57
|
+
# Nested Object Mutations
|
58
|
+
#
|
59
|
+
# Since we use {Array,Hash}#hash to compare before & after, and #hash accounts
|
60
|
+
# for/traverses nested structures, no "deep" inspection logic is technically
|
61
|
+
# necessary. However, Resource#dirty? only queries a cache of dirtied
|
62
|
+
# attributes, whose own population strategy is to hook assignment (instead of
|
63
|
+
# interrogating properties on demand). So the approach is still limited to
|
64
|
+
# top-level mutators.
|
65
|
+
#
|
66
|
+
# Maybe consider optional "advisory" Property#dirty? method for Resource#dirty?
|
67
|
+
# that custom properties could use for this purpose.
|
68
|
+
#
|
69
|
+
# TODO: add support for detecting mutations in nested objects, but we can't
|
70
|
+
# catch the assignment from here (yet?).
|
71
|
+
# TODO: ensure we covered all indirectly-mutable classes that DM uses underneath
|
72
|
+
# a property type
|
73
|
+
# TODO: figure out how to hook core class methods on RBX (which do use #send)
|
74
|
+
|
75
|
+
module Ardm
|
76
|
+
class Property
|
77
|
+
module DirtyMinder
|
78
|
+
|
79
|
+
module Hooker
|
80
|
+
MUTATION_METHODS = {
|
81
|
+
::Array => %w{
|
82
|
+
[]= push << shift pop insert unshift delete
|
83
|
+
delete_at replace fill clear
|
84
|
+
slice! reverse! rotate! compact! flatten! uniq!
|
85
|
+
collect! map! sort! sort_by! reject! delete_if!
|
86
|
+
select! shuffle!
|
87
|
+
}.select { |meth| ::Array.instance_methods.any? { |m| m.to_s == meth } },
|
88
|
+
|
89
|
+
::Hash => %w{
|
90
|
+
[]= store delete delete_if replace update
|
91
|
+
delete rehash shift clear
|
92
|
+
merge! reject! select!
|
93
|
+
}.select { |meth| ::Hash.instance_methods.any? { |m| m.to_s == meth } },
|
94
|
+
}
|
95
|
+
|
96
|
+
def self.extended(instance)
|
97
|
+
# FIXME: DirtyMinder is currently unsupported on RBX, because unlike
|
98
|
+
# the other supported Rubies, RBX core class (e.g. Array, Hash)
|
99
|
+
# methods use #send(). In other words, the other Rubies don't use
|
100
|
+
# #send() (they map directly to their C functions).
|
101
|
+
#
|
102
|
+
# The current methodology takes advantage of this by using #send() to
|
103
|
+
# forward method invocations we've hooked. Supporting RBX will
|
104
|
+
# require finding another way, possibly for all Rubies. In the
|
105
|
+
# meantime, something is better than nothing.
|
106
|
+
return if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'rbx'
|
107
|
+
|
108
|
+
return unless type = MUTATION_METHODS.keys.find { |k| instance.kind_of?(k) }
|
109
|
+
instance.extend const_get("#{type}Hooks")
|
110
|
+
end
|
111
|
+
|
112
|
+
MUTATION_METHODS.each do |klass, methods|
|
113
|
+
methods.each do |meth|
|
114
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
115
|
+
module #{klass}Hooks
|
116
|
+
def #{meth}(*)
|
117
|
+
before = self.clone
|
118
|
+
ret = super
|
119
|
+
after = self
|
120
|
+
|
121
|
+
# If the hashes aren't equivalent then we know the Resource
|
122
|
+
# should be dirty. However because we mutated self, normal
|
123
|
+
# State tracking will never trigger, because it will compare the
|
124
|
+
# new value - self - to the Resource's existing property value -
|
125
|
+
# which is also self.
|
126
|
+
#
|
127
|
+
# The solution is to drop 1 level beneath Resource State
|
128
|
+
# tracking and set the value of the property directly to the
|
129
|
+
# previous value (a different object now, because it's a clone).
|
130
|
+
# Then trigger the State tracking like normal.
|
131
|
+
if before.hash != after.hash
|
132
|
+
@property.set(@resource, before)
|
133
|
+
@resource.attribute_set(@property.name, after)
|
134
|
+
end
|
135
|
+
|
136
|
+
ret
|
137
|
+
end
|
138
|
+
end
|
139
|
+
RUBY
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def track(resource, property)
|
144
|
+
@resource, @property = resource, property
|
145
|
+
end
|
146
|
+
|
147
|
+
end # Hooker
|
148
|
+
|
149
|
+
# Catch any direct assignment (#set), and any Resource#reload (set!).
|
150
|
+
def set!(resource, value)
|
151
|
+
# Do not extend non observed value classes
|
152
|
+
if Hooker::MUTATION_METHODS.keys.detect { |klass| value.kind_of?(klass) }
|
153
|
+
hook_value(resource, value) unless value.kind_of? Hooker
|
154
|
+
end
|
155
|
+
super
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def hook_value(resource, value)
|
161
|
+
return if value.kind_of? Hooker
|
162
|
+
|
163
|
+
value.extend Hooker
|
164
|
+
value.track(resource, self)
|
165
|
+
end
|
166
|
+
|
167
|
+
end # DirtyMinder
|
168
|
+
end # Property
|
169
|
+
end # Ardm
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Ardm
|
4
|
+
class Property
|
5
|
+
module Flags
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
accept_options :flags
|
10
|
+
attr_reader :flag_map
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :generated_classes
|
14
|
+
end
|
15
|
+
|
16
|
+
self.generated_classes = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def custom?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
# TODO: document
|
25
|
+
# @api public
|
26
|
+
def [](*values)
|
27
|
+
if klass = generated_classes[values.flatten]
|
28
|
+
klass
|
29
|
+
else
|
30
|
+
klass = ::Class.new(self)
|
31
|
+
klass.flags(values)
|
32
|
+
|
33
|
+
generated_classes[values.flatten] = klass
|
34
|
+
|
35
|
+
klass
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Ardm
|
4
|
+
class Property
|
5
|
+
module ParanoidBase
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
extend ClassMethods
|
10
|
+
instance_variable_set(:@paranoid_properties, {})
|
11
|
+
instance_variable_set(:@paranoid_scopes, [])
|
12
|
+
end
|
13
|
+
|
14
|
+
def paranoid_destroy
|
15
|
+
self.class.paranoid_properties.each do |name, block|
|
16
|
+
attribute_set(name, block.call(self))
|
17
|
+
end
|
18
|
+
save
|
19
|
+
@readonly = true
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def destroy(execute_hooks = true)
|
24
|
+
# NOTE: changed behavior because AR doesn't call hooks on destroying new objects
|
25
|
+
return false if new_record?
|
26
|
+
if execute_hooks
|
27
|
+
run_callbacks :destroy do
|
28
|
+
paranoid_destroy
|
29
|
+
end
|
30
|
+
else
|
31
|
+
super
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods
|
36
|
+
def inherited(model)
|
37
|
+
model.instance_variable_set(:@paranoid_properties, @paranoid_properties.dup)
|
38
|
+
model.instance_variable_set(:@paranoid_scopes, @paranoid_scopes.dup)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
# @api public
|
43
|
+
def with_deleted(&block)
|
44
|
+
with_deleted_scope = self.scoped.with_default_scope
|
45
|
+
paranoid_scopes.each do |cond|
|
46
|
+
with_deleted_scope.where_values.delete(cond)
|
47
|
+
end
|
48
|
+
|
49
|
+
if block_given?
|
50
|
+
with_deleted_scope.scoping(&block)
|
51
|
+
else
|
52
|
+
with_deleted_scope.all
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def paranoid_properties
|
58
|
+
@paranoid_properties
|
59
|
+
end
|
60
|
+
|
61
|
+
def paranoid_scopes
|
62
|
+
@paranoid_scopes
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def set_paranoid_property(name, &block)
|
67
|
+
paranoid_properties[name] = block
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_paranoid_scope(conditions)
|
71
|
+
paranoid_scope = conditions
|
72
|
+
paranoid_scopes << paranoid_scope
|
73
|
+
default_scope { where(paranoid_scope) }
|
74
|
+
end
|
75
|
+
end # module ClassMethods
|
76
|
+
end # module ParanoidBase
|
77
|
+
end # class Property
|
78
|
+
end # module Ardm
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'ardm/property/string'
|
2
|
+
require 'addressable/uri'
|
3
|
+
|
4
|
+
module Ardm
|
5
|
+
class Property
|
6
|
+
class URI < String
|
7
|
+
load_as Addressable::URI
|
8
|
+
|
9
|
+
# Maximum length chosen based on recommendation:
|
10
|
+
# http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-an-url
|
11
|
+
length 2000
|
12
|
+
|
13
|
+
def load(value)
|
14
|
+
Addressable::URI.parse(value) unless value.nil?
|
15
|
+
end
|
16
|
+
|
17
|
+
def dump(value)
|
18
|
+
value.to_s unless value.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
def typecast(value)
|
22
|
+
load(value) unless value.nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
end # class URI
|
26
|
+
end # class Property
|
27
|
+
end # module Ardm
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'ardm/property/string'
|
2
|
+
require 'uuidtools' # must be ~>2.0
|
3
|
+
|
4
|
+
module Ardm
|
5
|
+
class Property
|
6
|
+
# UUID Type
|
7
|
+
# First run at this, because I need it. A few caveats:
|
8
|
+
# * Only works on postgres, using the built-in native uuid type.
|
9
|
+
# To make it work in mysql, you'll have to add a typemap entry to
|
10
|
+
# the mysql_adapter. I think. I don't have mysql handy, so I'm
|
11
|
+
# not going to try. For SQLite, this will have to inherit from the
|
12
|
+
# String primitive
|
13
|
+
# * Won't accept a random default, because of the namespace clash
|
14
|
+
# between this and the UUIDtools gem. Also can't set the default
|
15
|
+
# type to UUID() (postgres-contrib's native generator) and
|
16
|
+
# automigrate, because auto_migrate! tries to make it a string "UUID()"
|
17
|
+
# Feel free to enchance this, and delete these caveats when they're fixed.
|
18
|
+
#
|
19
|
+
# -- Rando Sept 25, 08
|
20
|
+
#
|
21
|
+
# Actually, setting the primitive to "UUID" is not neccessary and causes
|
22
|
+
# a segfault when trying to query uuid's from the database. The primitive
|
23
|
+
# should be a class which has been added to the do driver you are using.
|
24
|
+
# Also, it's only neccessary to add a class to the do drivers to use as a
|
25
|
+
# primitive when a value cannot be represented as a string. A uuid can be
|
26
|
+
# represented as a string, so setting the primitive to String ensures that
|
27
|
+
# the value argument is a String containing the uuid in string form.
|
28
|
+
#
|
29
|
+
# <strike>It is still neccessary to add the UUID entry to the type map for
|
30
|
+
# each different adapter with their respective database primitive.</strike>
|
31
|
+
#
|
32
|
+
# The method that generates the SQL schema from the typemap currently
|
33
|
+
# ignores the size attribute from the type map if the primitive type
|
34
|
+
# is String. The causes the generated SQL statement to contain a size for
|
35
|
+
# a UUID column (e.g. id UUID(50)), which causes a syntax error in postgres.
|
36
|
+
# Until this is resolved, you will have to manually change the column type
|
37
|
+
# to UUID in a migration, if you want to use postgres' built in UUID type.
|
38
|
+
#
|
39
|
+
# -- benburkert Nov 15, 08
|
40
|
+
#
|
41
|
+
class UUID < String
|
42
|
+
load_as UUIDTools::UUID
|
43
|
+
|
44
|
+
length 36
|
45
|
+
|
46
|
+
def dump(value)
|
47
|
+
value.to_s unless value.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
def load(value)
|
51
|
+
if value_loaded?(value)
|
52
|
+
value
|
53
|
+
elsif !value.nil?
|
54
|
+
UUIDTools::UUID.parse(value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def typecast(value)
|
59
|
+
return if value.nil?
|
60
|
+
load(value)
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'ardm/property'
|
2
|
+
require 'ardm/property/string'
|
3
|
+
require 'ardm/property/text'
|
4
|
+
require 'ardm/property/numeric'
|
5
|
+
|
6
|
+
module Ardm
|
7
|
+
class Property
|
8
|
+
unless defined?(Infinity)
|
9
|
+
Infinity = 1.0/0
|
10
|
+
end
|
11
|
+
Ardm::Property.accept_options :auto_validation, :validates, :set, :format, :message, :messages
|
12
|
+
|
13
|
+
module Validation
|
14
|
+
|
15
|
+
# Infer validations for a given property. This will only occur
|
16
|
+
# if the option :auto_validation is either true or left undefined.
|
17
|
+
#
|
18
|
+
# Triggers that generate validator creation
|
19
|
+
#
|
20
|
+
# :required => true
|
21
|
+
# Setting the option :required to true causes a Rule::Presence
|
22
|
+
# to be created for the property
|
23
|
+
#
|
24
|
+
# :length => 20
|
25
|
+
# Setting the option :length causes a Rule::Length to be created
|
26
|
+
# for the property.
|
27
|
+
# If the value is a Integer the Rule will have :maximum => value.
|
28
|
+
# If the value is a Range the Rule will have :within => value.
|
29
|
+
#
|
30
|
+
# :format => :predefined / lambda / Proc
|
31
|
+
# Setting the :format option causes a Rule::Format to be created
|
32
|
+
# for the property
|
33
|
+
#
|
34
|
+
# :set => ["foo", "bar", "baz"]
|
35
|
+
# Setting the :set option causes a Rule::Within to be created
|
36
|
+
# for the property
|
37
|
+
#
|
38
|
+
# Integer type
|
39
|
+
# Using a Integer type causes a Rule::Numericalness to be created
|
40
|
+
# for the property. The Rule's :integer_only option is set to true
|
41
|
+
#
|
42
|
+
# BigDecimal or Float type
|
43
|
+
# Using a Integer type causes a Rule::Numericalness to be created
|
44
|
+
# for the property. The Rule's :integer_only option will be set
|
45
|
+
# to false, and precision/scale will be set to match the Property
|
46
|
+
#
|
47
|
+
#
|
48
|
+
# Messages
|
49
|
+
#
|
50
|
+
# :messages => {..}
|
51
|
+
# Setting :messages hash replaces standard error messages
|
52
|
+
# with custom ones. For instance:
|
53
|
+
# :messages => {:presence => "Field is required",
|
54
|
+
# :format => "Field has invalid format"}
|
55
|
+
# Hash keys are: :presence, :format, :length, :is_unique,
|
56
|
+
# :is_number, :is_primitive
|
57
|
+
#
|
58
|
+
# :message => "Some message"
|
59
|
+
# It is just shortcut if only one validation option is set
|
60
|
+
#
|
61
|
+
# @api private
|
62
|
+
def self.rules_for_property(property)
|
63
|
+
rule_definitions = []
|
64
|
+
|
65
|
+
# all inferred rules should not be skipped when the value is nil
|
66
|
+
# (aside from Rule::Presence/Rule::Absence)
|
67
|
+
opts = { :allow_nil => true }
|
68
|
+
|
69
|
+
if property.options.key?(:validates)
|
70
|
+
opts[:context] = property.options[:validates]
|
71
|
+
end
|
72
|
+
|
73
|
+
rule_definitions << infer_presence( property, opts.dup)
|
74
|
+
rule_definitions << infer_length( property, opts.dup)
|
75
|
+
rule_definitions << infer_format( property, opts.dup)
|
76
|
+
rule_definitions << infer_uniqueness(property, opts.dup)
|
77
|
+
rule_definitions << infer_within( property, opts.dup)
|
78
|
+
rule_definitions << infer_type( property, opts.dup)
|
79
|
+
|
80
|
+
rule_definitions.compact
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# @api private
|
86
|
+
# Skip TrueClass dump because presence is invalid for false, but boolean false is ok for a boolean property.
|
87
|
+
def self.infer_presence(property, options)
|
88
|
+
return if property.allow_blank? || property.serial? || property.dump_as == ::TrueClass
|
89
|
+
|
90
|
+
validation_options = options_with_message(options, property, :presence)
|
91
|
+
|
92
|
+
{presence: validation_options}
|
93
|
+
end
|
94
|
+
|
95
|
+
# @api private
|
96
|
+
def self.infer_length(property, options)
|
97
|
+
# TODO: return unless property.primitive <= String (?)
|
98
|
+
return unless (property.kind_of?(Property::String) ||
|
99
|
+
property.kind_of?(Property::Text))
|
100
|
+
length = property.options.fetch(:length, Property::String.length)
|
101
|
+
|
102
|
+
|
103
|
+
if length.is_a?(Range)
|
104
|
+
if length.last == Infinity
|
105
|
+
raise ArgumentError, "Infinity is not a valid upper bound for a length range"
|
106
|
+
end
|
107
|
+
options[:in] = length
|
108
|
+
else
|
109
|
+
options[:maximum] = length
|
110
|
+
end
|
111
|
+
|
112
|
+
validation_options = options_with_message(options, property, :length)
|
113
|
+
|
114
|
+
{length: validation_options}
|
115
|
+
end
|
116
|
+
|
117
|
+
# @api private
|
118
|
+
def self.infer_format(property, options)
|
119
|
+
return unless property.options.key?(:format)
|
120
|
+
|
121
|
+
options[:with] = property.options[:format]
|
122
|
+
|
123
|
+
validation_options = options_with_message(options, property, :format)
|
124
|
+
|
125
|
+
{format: validation_options}
|
126
|
+
end
|
127
|
+
|
128
|
+
# @api private
|
129
|
+
def self.infer_uniqueness(property, options)
|
130
|
+
return unless property.options.key?(:unique)
|
131
|
+
|
132
|
+
case value = property.options[:unique]
|
133
|
+
when Array, Symbol
|
134
|
+
# TODO: fix this to behave like :unique_index
|
135
|
+
options[:scope] = Array(value)
|
136
|
+
|
137
|
+
validation_options = options_with_message(options, property, :is_unique)
|
138
|
+
{uniqueness: validation_options}
|
139
|
+
when TrueClass
|
140
|
+
validation_options = options_with_message(options, property, :is_unique)
|
141
|
+
{uniqueness: validation_options}
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @api private
|
146
|
+
def self.infer_within(property, options)
|
147
|
+
return unless property.options.key?(:set)
|
148
|
+
|
149
|
+
options[:in] = property.options[:set]
|
150
|
+
options[:message] ||= "must be one of #{options[:in].join(', ')}"
|
151
|
+
|
152
|
+
validation_options = options_with_message(options, property, :within)
|
153
|
+
{inclusion: validation_options}
|
154
|
+
end
|
155
|
+
|
156
|
+
# @api private
|
157
|
+
def self.infer_type(property, options)
|
158
|
+
return if property.respond_to?(:custom?) && property.custom?
|
159
|
+
|
160
|
+
if property.kind_of?(Property::Numeric)
|
161
|
+
options[:greater_than_or_equal_to] = property.min if property.min
|
162
|
+
options[:less_than_or_equal_to] = property.max if property.max
|
163
|
+
end
|
164
|
+
|
165
|
+
if Integer == property.load_as
|
166
|
+
options[:only_integer] = true
|
167
|
+
|
168
|
+
validation_options = options_with_message(options, property, :is_number)
|
169
|
+
{numericality: validation_options}
|
170
|
+
elsif (BigDecimal == property.load_as ||
|
171
|
+
Float == property.load_as)
|
172
|
+
options[:precision] = property.precision
|
173
|
+
options[:scale] = property.scale
|
174
|
+
|
175
|
+
validation_options = options_with_message(options, property, :is_number)
|
176
|
+
{numericality: validation_options}
|
177
|
+
else
|
178
|
+
# We only need this in the case we don't already
|
179
|
+
# have a numeric validator, because otherwise
|
180
|
+
# it will cause duplicate validation errors
|
181
|
+
validation_options = options_with_message(options, property, :is_primitive)
|
182
|
+
# FIXME unsupported in Ardm::Property for now
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# TODO: eliminate this;
|
188
|
+
# mutating one arg based on a non-obvious interaction of the other two...
|
189
|
+
# well, it makes my skin crawl.
|
190
|
+
#
|
191
|
+
# @api private
|
192
|
+
def self.options_with_message(base_options, property, validator_name)
|
193
|
+
options = base_options.clone
|
194
|
+
opts = property.options
|
195
|
+
|
196
|
+
if opts.key?(:messages)
|
197
|
+
options[:message] = opts[:messages][validator_name]
|
198
|
+
elsif opts.key?(:message)
|
199
|
+
options[:message] = opts[:message]
|
200
|
+
end
|
201
|
+
|
202
|
+
options
|
203
|
+
end
|
204
|
+
|
205
|
+
end # module Validation
|
206
|
+
end # module Property
|
207
|
+
end # module Ardm
|
208
|
+
|