ardm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (179) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +35 -0
  3. data/Gemfile +13 -0
  4. data/LICENSE +21 -0
  5. data/README.md +70 -0
  6. data/Rakefile +4 -0
  7. data/ardm.gemspec +29 -0
  8. data/db/.gitignore +1 -0
  9. data/lib/ardm/active_record/associations.rb +80 -0
  10. data/lib/ardm/active_record/base.rb +49 -0
  11. data/lib/ardm/active_record/dirty.rb +25 -0
  12. data/lib/ardm/active_record/hooks.rb +31 -0
  13. data/lib/ardm/active_record/inheritance.rb +37 -0
  14. data/lib/ardm/active_record/is/state_machine.rb +21 -0
  15. data/lib/ardm/active_record/is.rb +22 -0
  16. data/lib/ardm/active_record/not_found.rb +7 -0
  17. data/lib/ardm/active_record/predicate_builder/array_handler.rb +31 -0
  18. data/lib/ardm/active_record/predicate_builder/rails3.rb +147 -0
  19. data/lib/ardm/active_record/predicate_builder/rails4.rb +139 -0
  20. data/lib/ardm/active_record/predicate_builder/relation_handler.rb +15 -0
  21. data/lib/ardm/active_record/predicate_builder.rb +19 -0
  22. data/lib/ardm/active_record/property.rb +357 -0
  23. data/lib/ardm/active_record/query.rb +108 -0
  24. data/lib/ardm/active_record/record.rb +70 -0
  25. data/lib/ardm/active_record/relation.rb +83 -0
  26. data/lib/ardm/active_record/repository.rb +38 -0
  27. data/lib/ardm/active_record/serialization.rb +164 -0
  28. data/lib/ardm/active_record/storage_names.rb +28 -0
  29. data/lib/ardm/active_record/validations.rb +111 -0
  30. data/lib/ardm/active_record.rb +43 -0
  31. data/lib/ardm/data_mapper/not_found.rb +5 -0
  32. data/lib/ardm/data_mapper/record.rb +41 -0
  33. data/lib/ardm/data_mapper.rb +5 -0
  34. data/lib/ardm/env.rb +5 -0
  35. data/lib/ardm/property/api_key.rb +30 -0
  36. data/lib/ardm/property/bcrypt_hash.rb +31 -0
  37. data/lib/ardm/property/binary.rb +23 -0
  38. data/lib/ardm/property/boolean.rb +29 -0
  39. data/lib/ardm/property/class.rb +19 -0
  40. data/lib/ardm/property/comma_separated_list.rb +28 -0
  41. data/lib/ardm/property/csv.rb +35 -0
  42. data/lib/ardm/property/date.rb +12 -0
  43. data/lib/ardm/property/datetime.rb +12 -0
  44. data/lib/ardm/property/decimal.rb +38 -0
  45. data/lib/ardm/property/discriminator.rb +65 -0
  46. data/lib/ardm/property/enum.rb +51 -0
  47. data/lib/ardm/property/epoch_time.rb +38 -0
  48. data/lib/ardm/property/file_path.rb +25 -0
  49. data/lib/ardm/property/flag.rb +65 -0
  50. data/lib/ardm/property/float.rb +18 -0
  51. data/lib/ardm/property/integer.rb +24 -0
  52. data/lib/ardm/property/invalid_value_error.rb +22 -0
  53. data/lib/ardm/property/ip_address.rb +35 -0
  54. data/lib/ardm/property/json.rb +49 -0
  55. data/lib/ardm/property/lookup.rb +29 -0
  56. data/lib/ardm/property/numeric.rb +40 -0
  57. data/lib/ardm/property/object.rb +36 -0
  58. data/lib/ardm/property/paranoid_boolean.rb +18 -0
  59. data/lib/ardm/property/paranoid_datetime.rb +17 -0
  60. data/lib/ardm/property/regexp.rb +22 -0
  61. data/lib/ardm/property/serial.rb +16 -0
  62. data/lib/ardm/property/slug.rb +29 -0
  63. data/lib/ardm/property/string.rb +40 -0
  64. data/lib/ardm/property/support/dirty_minder.rb +169 -0
  65. data/lib/ardm/property/support/flags.rb +41 -0
  66. data/lib/ardm/property/support/paranoid_base.rb +78 -0
  67. data/lib/ardm/property/text.rb +11 -0
  68. data/lib/ardm/property/time.rb +12 -0
  69. data/lib/ardm/property/uri.rb +27 -0
  70. data/lib/ardm/property/uuid.rb +65 -0
  71. data/lib/ardm/property/validation.rb +208 -0
  72. data/lib/ardm/property/yaml.rb +38 -0
  73. data/lib/ardm/property.rb +891 -0
  74. data/lib/ardm/property_set.rb +152 -0
  75. data/lib/ardm/query/expression.rb +85 -0
  76. data/lib/ardm/query/ext/symbol.rb +37 -0
  77. data/lib/ardm/query/operator.rb +64 -0
  78. data/lib/ardm/record.rb +1 -0
  79. data/lib/ardm/support/assertions.rb +8 -0
  80. data/lib/ardm/support/deprecate.rb +12 -0
  81. data/lib/ardm/support/descendant_set.rb +89 -0
  82. data/lib/ardm/support/equalizer.rb +48 -0
  83. data/lib/ardm/support/ext/array.rb +22 -0
  84. data/lib/ardm/support/ext/blank.rb +25 -0
  85. data/lib/ardm/support/ext/hash.rb +67 -0
  86. data/lib/ardm/support/ext/module.rb +47 -0
  87. data/lib/ardm/support/ext/object.rb +57 -0
  88. data/lib/ardm/support/ext/string.rb +24 -0
  89. data/lib/ardm/support/ext/try_dup.rb +12 -0
  90. data/lib/ardm/support/hook.rb +405 -0
  91. data/lib/ardm/support/lazy_array.rb +451 -0
  92. data/lib/ardm/support/local_object_space.rb +13 -0
  93. data/lib/ardm/support/logger.rb +201 -0
  94. data/lib/ardm/support/mash.rb +176 -0
  95. data/lib/ardm/support/naming_conventions.rb +90 -0
  96. data/lib/ardm/support/ordered_set.rb +380 -0
  97. data/lib/ardm/support/subject.rb +33 -0
  98. data/lib/ardm/support/subject_set.rb +250 -0
  99. data/lib/ardm/version.rb +3 -0
  100. data/lib/ardm.rb +56 -0
  101. data/spec/fixtures/api_user.rb +11 -0
  102. data/spec/fixtures/article.rb +22 -0
  103. data/spec/fixtures/bookmark.rb +14 -0
  104. data/spec/fixtures/invention.rb +5 -0
  105. data/spec/fixtures/network_node.rb +23 -0
  106. data/spec/fixtures/person.rb +17 -0
  107. data/spec/fixtures/software_package.rb +22 -0
  108. data/spec/fixtures/ticket.rb +12 -0
  109. data/spec/fixtures/tshirt.rb +15 -0
  110. data/spec/integration/api_key_spec.rb +25 -0
  111. data/spec/integration/bcrypt_hash_spec.rb +45 -0
  112. data/spec/integration/comma_separated_list_spec.rb +85 -0
  113. data/spec/integration/dirty_minder_spec.rb +197 -0
  114. data/spec/integration/enum_spec.rb +79 -0
  115. data/spec/integration/epoch_time_spec.rb +59 -0
  116. data/spec/integration/file_path_spec.rb +158 -0
  117. data/spec/integration/flag_spec.rb +72 -0
  118. data/spec/integration/ip_address_spec.rb +151 -0
  119. data/spec/integration/json_spec.rb +70 -0
  120. data/spec/integration/slug_spec.rb +65 -0
  121. data/spec/integration/uri_spec.rb +136 -0
  122. data/spec/integration/uuid_spec.rb +102 -0
  123. data/spec/integration/yaml_spec.rb +85 -0
  124. data/spec/public/property/binary_spec.rb +41 -0
  125. data/spec/public/property/boolean_spec.rb +30 -0
  126. data/spec/public/property/class_spec.rb +28 -0
  127. data/spec/public/property/date_spec.rb +22 -0
  128. data/spec/public/property/date_time_spec.rb +22 -0
  129. data/spec/public/property/decimal_spec.rb +23 -0
  130. data/spec/public/property/discriminator_spec.rb +133 -0
  131. data/spec/public/property/float_spec.rb +22 -0
  132. data/spec/public/property/integer_spec.rb +22 -0
  133. data/spec/public/property/object_spec.rb +103 -0
  134. data/spec/public/property/serial_spec.rb +22 -0
  135. data/spec/public/property/string_spec.rb +22 -0
  136. data/spec/public/property/text_spec.rb +23 -0
  137. data/spec/public/property/time_spec.rb +22 -0
  138. data/spec/public/property_spec.rb +316 -0
  139. data/spec/rcov.opts +6 -0
  140. data/spec/schema.rb +86 -0
  141. data/spec/semipublic/property/binary_spec.rb +14 -0
  142. data/spec/semipublic/property/boolean_spec.rb +48 -0
  143. data/spec/semipublic/property/class_spec.rb +36 -0
  144. data/spec/semipublic/property/date_spec.rb +44 -0
  145. data/spec/semipublic/property/date_time_spec.rb +47 -0
  146. data/spec/semipublic/property/decimal_spec.rb +83 -0
  147. data/spec/semipublic/property/discriminator_spec.rb +22 -0
  148. data/spec/semipublic/property/float_spec.rb +83 -0
  149. data/spec/semipublic/property/integer_spec.rb +83 -0
  150. data/spec/semipublic/property/lookup_spec.rb +27 -0
  151. data/spec/semipublic/property/serial_spec.rb +14 -0
  152. data/spec/semipublic/property/string_spec.rb +14 -0
  153. data/spec/semipublic/property/text_spec.rb +30 -0
  154. data/spec/semipublic/property/time_spec.rb +49 -0
  155. data/spec/semipublic/property_spec.rb +51 -0
  156. data/spec/shared/flags_shared_spec.rb +36 -0
  157. data/spec/shared/identity_function_group.rb +5 -0
  158. data/spec/shared/public_property_spec.rb +229 -0
  159. data/spec/shared/semipublic_property_spec.rb +159 -0
  160. data/spec/spec.opts +4 -0
  161. data/spec/spec_helper.rb +58 -0
  162. data/spec/unit/bcrypt_hash_spec.rb +154 -0
  163. data/spec/unit/csv_spec.rb +139 -0
  164. data/spec/unit/dirty_minder_spec.rb +64 -0
  165. data/spec/unit/enum_spec.rb +125 -0
  166. data/spec/unit/epoch_time_spec.rb +72 -0
  167. data/spec/unit/file_path_spec.rb +75 -0
  168. data/spec/unit/flag_spec.rb +114 -0
  169. data/spec/unit/ip_address_spec.rb +109 -0
  170. data/spec/unit/json_spec.rb +127 -0
  171. data/spec/unit/paranoid_boolean_spec.rb +142 -0
  172. data/spec/unit/paranoid_datetime_spec.rb +149 -0
  173. data/spec/unit/regexp_spec.rb +62 -0
  174. data/spec/unit/uri_spec.rb +64 -0
  175. data/spec/unit/yaml_spec.rb +111 -0
  176. data/tasks/spec.rake +40 -0
  177. data/tasks/yard.rake +9 -0
  178. data/tasks/yardstick.rake +19 -0
  179. 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,11 @@
1
+ require 'ardm/property/string'
2
+
3
+ module Ardm
4
+ class Property
5
+ class Text < String
6
+ length 65535
7
+ lazy true
8
+
9
+ end # class Text
10
+ end # class Property
11
+ end # module Ardm
@@ -0,0 +1,12 @@
1
+ require 'ardm/property/object'
2
+
3
+ module Ardm
4
+ class Property
5
+ class Time < Object
6
+ load_as ::Time
7
+ dump_as ::Time
8
+ coercion_method :to_time
9
+
10
+ end # class Time
11
+ end # class Property
12
+ 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
+