ardm 0.0.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 +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
|
+
|