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