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,891 @@
1
+ require 'bigdecimal'
2
+ require 'time'
3
+ require 'coercible'
4
+
5
+
6
+ module Ardm
7
+ class Property
8
+ autoload :CommaSeparatedList, 'ardm/property/comma_separated_list'
9
+ autoload :Csv, 'ardm/property/csv'
10
+ autoload :BCryptHash, 'ardm/property/bcrypt_hash'
11
+ autoload :Enum, 'ardm/property/enum'
12
+ autoload :EpochTime, 'ardm/property/epoch_time'
13
+ autoload :FilePath, 'ardm/property/file_path'
14
+ autoload :Flag, 'ardm/property/flag'
15
+ autoload :IPAddress, 'ardm/property/ip_address'
16
+ autoload :Json, 'ardm/property/json'
17
+ autoload :Regexp, 'ardm/property/regexp'
18
+ autoload :ParanoidBoolean, 'ardm/property/paranoid_boolean'
19
+ autoload :ParanoidDateTime, 'ardm/property/paranoid_datetime'
20
+ autoload :Slug, 'ardm/property/slug'
21
+ autoload :UUID, 'ardm/property/uuid'
22
+ autoload :URI, 'ardm/property/uri'
23
+ autoload :Yaml, 'ardm/property/yaml'
24
+ autoload :APIKey, 'ardm/property/api_key'
25
+ end
26
+ end
27
+
28
+ module Ardm
29
+ # = Properties
30
+ # Properties for a model are not derived from a database structure, but
31
+ # instead explicitly declared inside your model class definitions. These
32
+ # properties then map (or, if using automigrate, generate) fields in your
33
+ # database.
34
+ #
35
+ # If you are coming to Ardm from another ORM framework, such as
36
+ # ActiveRecord, this may be a fundamental difference in thinking to you.
37
+ # However, there are several advantages to defining your properties in your
38
+ # models:
39
+ #
40
+ # * information about your model is centralized in one place: rather than
41
+ # having to dig out migrations, xml or other configuration files.
42
+ # * use of mixins can be applied to model properties: better code reuse
43
+ # * having information centralized in your models, encourages you and the
44
+ # developers on your team to take a model-centric view of development.
45
+ # * it provides the ability to use Ruby's access control functions.
46
+ # * and, because Ardm only cares about properties explicitly defined
47
+ # in your models, Ardm plays well with legacy databases, and shares
48
+ # databases easily with other applications.
49
+ #
50
+ # == Declaring Properties
51
+ # Inside your class, you call the property method for each property you want
52
+ # to add. The only two required arguments are the name and type, everything
53
+ # else is optional.
54
+ #
55
+ # class Post < ActiveRecord::Base
56
+ # property :title, String, :required => true # Cannot be null
57
+ # property :publish, Boolean, :default => false # Default value for new records is false
58
+ # end
59
+ #
60
+ # By default, Ardm supports the following primitive (Ruby) types
61
+ # also called core properties:
62
+ #
63
+ # * Boolean
64
+ # * Class (datastore primitive is the same as String. Used for Inheritance)
65
+ # * Date
66
+ # * DateTime
67
+ # * Decimal
68
+ # * Float
69
+ # * Integer
70
+ # * Object (marshalled out during serialization)
71
+ # * String (default length is 50)
72
+ # * Text (limit of 65k characters by default)
73
+ # * Time
74
+ #
75
+ # == Limiting Access
76
+ # Property access control is uses the same terminology Ruby does. Properties
77
+ # are public by default, but can also be declared private or protected as
78
+ # needed (via the :accessor option).
79
+ #
80
+ # class Post < ActiveRecord::Base
81
+ # property :title, String, :accessor => :private # Both reader and writer are private
82
+ # property :body, Text, :accessor => :protected # Both reader and writer are protected
83
+ # end
84
+ #
85
+ # Access control is also analogous to Ruby attribute readers and writers, and can
86
+ # be declared using :reader and :writer, in addition to :accessor.
87
+ #
88
+ # class Post < ActiveRecord::Base
89
+ # property :title, String, :writer => :private # Only writer is private
90
+ # property :tags, String, :reader => :protected # Only reader is protected
91
+ # end
92
+ #
93
+ # == Overriding Accessors
94
+ # The reader/writer for any property can be overridden in the same manner that Ruby
95
+ # attr readers/writers can be. After the property is defined, just add your custom
96
+ # reader or writer:
97
+ #
98
+ # class Post < ActiveRecord::Base
99
+ # property :title, String
100
+ #
101
+ # def title=(new_title)
102
+ # raise ArgumentError if new_title != 'Lee is l337'
103
+ # super(new_title)
104
+ # end
105
+ # end
106
+ #
107
+ # Calling super ensures that any validators defined for the property are kept active.
108
+ #
109
+ # == Lazy Loading
110
+ # By default, some properties are not loaded when an object is fetched in
111
+ # Ardm. These lazily loaded properties are fetched on demand when their
112
+ # accessor is called for the first time (as it is often unnecessary to
113
+ # instantiate -every- property -every- time an object is loaded). For
114
+ # instance, Ardm::Property::Text fields are lazy loading by default,
115
+ # although you can over-ride this behavior if you wish:
116
+ #
117
+ # Example:
118
+ #
119
+ # class Post < ActiveRecord::Base
120
+ # property :title, String # Loads normally
121
+ # property :body, Text # Is lazily loaded by default
122
+ # end
123
+ #
124
+ # If you want to over-ride the lazy loading on any field you can set it to a
125
+ # context or false to disable it with the :lazy option. Contexts allow
126
+ # multiple lazy properties to be loaded at one time. If you set :lazy to
127
+ # true, it is placed in the :default context
128
+ #
129
+ # class Post < ActiveRecord::Base
130
+ # property :title, String # Loads normally
131
+ # property :body, Text, :lazy => false # The default is now over-ridden
132
+ # property :comment, String, :lazy => [ :detailed ] # Loads in the :detailed context
133
+ # property :author, String, :lazy => [ :summary, :detailed ] # Loads in :summary & :detailed context
134
+ # end
135
+ #
136
+ # Delaying the request for lazy-loaded attributes even applies to objects
137
+ # accessed through associations. In a sense, Ardm anticipates that
138
+ # you will likely be iterating over objects in associations and rolls all
139
+ # of the load commands for lazy-loaded properties into one request from
140
+ # the database.
141
+ #
142
+ # Example:
143
+ #
144
+ # Widget.get(1).components
145
+ # # loads when the post object is pulled from database, by default
146
+ #
147
+ # Widget.get(1).components.first.body
148
+ # # loads the values for the body property on all objects in the
149
+ # # association, rather than just this one.
150
+ #
151
+ # Widget.get(1).components.first.comment
152
+ # # loads both comment and author for all objects in the association
153
+ # # since they are both in the :detailed context
154
+ #
155
+ # == Keys
156
+ # Properties can be declared as primary or natural keys on a table.
157
+ # You should a property as the primary key of the table:
158
+ #
159
+ # Examples:
160
+ #
161
+ # property :id, Serial # auto-incrementing key
162
+ # property :legacy_pk, String, :key => true # 'natural' key
163
+ #
164
+ # This is roughly equivalent to ActiveRecord's <tt>set_primary_key</tt>,
165
+ # though non-integer data types may be used, thus Ardm supports natural
166
+ # keys. When a property is declared as a natural key, accessing the object
167
+ # using the indexer syntax <tt>Class[key]</tt> remains valid.
168
+ #
169
+ # User.get(1)
170
+ # # when :id is the primary key on the users table
171
+ # User.get('bill')
172
+ # # when :name is the primary (natural) key on the users table
173
+ #
174
+ # == Indices
175
+ # You can add indices for your properties by using the <tt>:index</tt>
176
+ # option. If you use <tt>true</tt> as the option value, the index will be
177
+ # automatically named. If you want to name the index yourself, use a symbol
178
+ # as the value.
179
+ #
180
+ # property :last_name, String, :index => true
181
+ # property :first_name, String, :index => :name
182
+ #
183
+ # You can create multi-column composite indices by using the same symbol in
184
+ # all the columns belonging to the index. The columns will appear in the
185
+ # index in the order they are declared.
186
+ #
187
+ # property :last_name, String, :index => :name
188
+ # property :first_name, String, :index => :name
189
+ # # => index on (last_name, first_name)
190
+ #
191
+ # If you want to make the indices unique, use <tt>:unique_index</tt> instead
192
+ # of <tt>:index</tt>
193
+ #
194
+ # == Inferred Validations
195
+ # If you require the dm-validations plugin, auto-validations will
196
+ # automatically be mixed-in in to your model classes: validation rules that
197
+ # are inferred when properties are declared with specific column restrictions.
198
+ #
199
+ # class Post < ActiveRecord::Base
200
+ # property :title, String, :length => 250, :min => 0, :max => 250
201
+ # # => infers 'validates_length :title'
202
+ #
203
+ # property :title, String, :required => true
204
+ # # => infers 'validates_present :title'
205
+ #
206
+ # property :email, String, :format => :email_address
207
+ # # => infers 'validates_format :email, :with => :email_address'
208
+ #
209
+ # property :title, String, :length => 255, :required => true
210
+ # # => infers both 'validates_length' as well as 'validates_present'
211
+ # # better: property :title, String, :length => 1..255
212
+ # end
213
+ #
214
+ # This functionality is available with the dm-validations gem. For more information
215
+ # about validations, check the documentation for dm-validations.
216
+ #
217
+ # == Default Values
218
+ # To set a default for a property, use the <tt>:default</tt> key. The
219
+ # property will be set to the value associated with that key the first time
220
+ # it is accessed, or when the resource is saved if it hasn't been set with
221
+ # another value already. This value can be a static value, such as 'hello'
222
+ # but it can also be a proc that will be evaluated when the property is read
223
+ # before its value has been set. The property is set to the return of the
224
+ # proc. The proc is passed two values, the resource the property is being set
225
+ # for and the property itself.
226
+ #
227
+ # property :display_name, String, :default => lambda { |resource, property| resource.login }
228
+ #
229
+ # Word of warning. Don't try to read the value of the property you're setting
230
+ # the default for in the proc. An infinite loop will ensue.
231
+ #
232
+ # == Embedded Values (not implemented yet)
233
+ # As an alternative to extraneous has_one relationships, consider using an
234
+ # EmbeddedValue.
235
+ #
236
+ # == Property options reference
237
+ #
238
+ # :accessor if false, neither reader nor writer methods are
239
+ # created for this property
240
+ #
241
+ # :reader if false, reader method is not created for this property
242
+ #
243
+ # :writer if false, writer method is not created for this property
244
+ #
245
+ # :lazy if true, property value is only loaded when on first read
246
+ # if false, property value is always loaded
247
+ # if a symbol, property value is loaded with other properties
248
+ # in the same group
249
+ #
250
+ # :default default value of this property
251
+ #
252
+ # :allow_nil if true, property may have a nil value on save
253
+ #
254
+ # :key name of the key associated with this property.
255
+ #
256
+ # :field field in the data-store which the property corresponds to
257
+ #
258
+ # :length string field length
259
+ #
260
+ # :format format for autovalidation. Use with dm-validations plugin.
261
+ #
262
+ # :index if true, index is created for the property. If a Symbol, index
263
+ # is named after Symbol value instead of being based on property name.
264
+ #
265
+ # :unique_index true specifies that index on this property should be unique
266
+ #
267
+ # :auto_validation if true, automatic validation is performed on the property
268
+ #
269
+ # :validates validation context. Use together with dm-validations.
270
+ #
271
+ # :unique if true, property column is unique. Properties of type Serial
272
+ # are unique by default.
273
+ #
274
+ # :precision Indicates the number of significant digits. Usually only makes sense
275
+ # for float type properties. Must be >= scale option value. Default is 10.
276
+ #
277
+ # :scale The number of significant digits to the right of the decimal point.
278
+ # Only makes sense for float type properties. Must be > 0.
279
+ # Default is nil for Float type and 10 for BigDecimal
280
+ #
281
+ # == Overriding default Property options
282
+ #
283
+ # There is the ability to reconfigure a Property and it's subclasses by explicitly
284
+ # setting a value in the Property, eg:
285
+ #
286
+ # # set all String properties to have a default length of 255
287
+ # Ardm::Property::String.length(255)
288
+ #
289
+ # # set all Boolean properties to not allow nil (force true or false)
290
+ # Ardm::Property::Boolean.allow_nil(false)
291
+ #
292
+ # # set all properties to be required by default
293
+ # Ardm::Property.required(true)
294
+ #
295
+ # # turn off auto-validation for all properties by default
296
+ # Ardm::Property.auto_validation(false)
297
+ #
298
+ # # set all mutator methods to be private by default
299
+ # Ardm::Property.writer(:private)
300
+ #
301
+ # Please note that this has no effect when a subclass has explicitly
302
+ # defined it's own option. For example, setting the String length to
303
+ # 255 will not affect the Text property even though it inherits from
304
+ # String, because it sets it's own default length to 65535.
305
+ #
306
+ # == Misc. Notes
307
+ # * Properties declared as strings will default to a length of 50, rather than
308
+ # 255 (typical max varchar column size). To overload the default, pass
309
+ # <tt>:length => 255</tt> or <tt>:length => 0..255</tt>. Since Ardm
310
+ # does not introspect for properties, this means that legacy database tables
311
+ # may need their <tt>String</tt> columns defined with a <tt>:length</tt> so
312
+ # that DM does not apply an un-needed length validation, or allow overflow.
313
+ # * You may declare a Property with the data-type of <tt>Class</tt>.
314
+ # see SingleTableInheritance for more on how to use <tt>Class</tt> columns.
315
+ class Property
316
+ include Ardm::Assertions
317
+ include Subject
318
+ extend Equalizer
319
+
320
+ equalize :model, :name, :options
321
+
322
+ module Undefined; end
323
+
324
+ PRIMITIVES = [
325
+ TrueClass,
326
+ ::String,
327
+ ::Float,
328
+ ::Integer,
329
+ ::BigDecimal,
330
+ ::DateTime,
331
+ ::Date,
332
+ ::Time,
333
+ ::Class
334
+ ].to_set.freeze
335
+
336
+ OPTIONS = [
337
+ :load_as, :dump_as, :coercion_method,
338
+ :accessor, :reader, :writer,
339
+ :lazy, :default, :key, :field,
340
+ :index, :unique_index,
341
+ :unique, :allow_nil, :allow_blank, :required
342
+ ]
343
+
344
+ # Possible :visibility option values
345
+ VISIBILITY_OPTIONS = [ :public, :protected, :private ].to_set.freeze
346
+
347
+ # Invalid property names
348
+ INVALID_NAMES = %w[]#(::ActiveRecord::Base.instance_methods +
349
+ #::ActiveRecord::Base.private_instance_methods
350
+ #).map { |name| name.to_s }
351
+
352
+ attr_reader :load_as, :dump_as, :coercion_method,
353
+ :model, :name, :instance_variable_name,
354
+ :reader_visibility, :writer_visibility, :options,
355
+ :default, :allow_nil, :allow_blank, :required
356
+
357
+ alias_method :load_class, :load_as
358
+ alias_method :dump_class, :dump_as
359
+
360
+ class << self
361
+ # @api semipublic
362
+ def determine_class(type)
363
+ return type if type < Ardm::Property::Object
364
+ find_class(demodulize(type.name))
365
+ end
366
+
367
+ def demodulize(name)
368
+ name.to_s.gsub(/^.*::/,'')
369
+ end
370
+
371
+ # @api private
372
+ def demodulized_names
373
+ @demodulized_names ||= {}
374
+ end
375
+
376
+ # @api semipublic
377
+ def find_class(name)
378
+ klass = demodulized_names[name]
379
+ klass ||= const_get(name) if const_defined?(name)
380
+ klass
381
+ end
382
+
383
+ # @api public
384
+ def descendants
385
+ @descendants ||= DescendantSet.new
386
+ end
387
+
388
+ # @api private
389
+ def inherited(descendant)
390
+ # Descendants is a tree rooted in Ardm::Property that tracks
391
+ # inheritance. We pre-calculate each comparison value (demodulized
392
+ # class name) to achieve a Hash[]-time lookup, rather than walk the
393
+ # entire descendant tree and calculate names on-demand (expensive,
394
+ # redundant).
395
+ #
396
+ # Since the algorithm relegates property class name lookups to a flat
397
+ # namespace, we need to ensure properties defined outside of DM don't
398
+ # override built-ins (Serial, String, etc) by merely defining a property
399
+ # of a same name. We avoid this by only ever adding to the lookup
400
+ # table. Given that DM loads its own property classes first, we can
401
+ # assume that their names are "reserved" when added to the table.
402
+ #
403
+ # External property authors who want to provide "replacements" for
404
+ # builtins (e.g. in a non-DM-supported adapter) should follow the
405
+ # convention of wrapping those properties in a module, and include'ing
406
+ # the module on the model class directly. This bypasses the DM-hooked
407
+ # const_missing lookup that would normally check this table.
408
+ descendants << descendant
409
+
410
+ Property.demodulized_names[demodulize(descendant.name)] ||= descendant
411
+
412
+ # inherit accepted options
413
+ descendant.accepted_options.concat(accepted_options)
414
+
415
+ # inherit the option values
416
+ options.each { |key, value| descendant.send(key, value) }
417
+
418
+ super
419
+ end
420
+
421
+ # @api public
422
+ def accepted_options
423
+ @accepted_options ||= []
424
+ end
425
+
426
+ # @api public
427
+ def accept_options(*args)
428
+ accepted_options.concat(args)
429
+
430
+ # create methods for each new option
431
+ args.each do |property_option|
432
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
433
+ def self.#{property_option}(value = Undefined) # def self.unique(value = Undefined)
434
+ return @#{property_option} if value.equal?(Undefined) # return @unique if value.equal?(Undefined)
435
+ descendants.each do |descendant| # descendants.each do |descendant|
436
+ unless descendant.instance_variable_defined?(:@#{property_option}) # unless descendant.instance_variable_defined?(:@unique)
437
+ descendant.#{property_option}(value) # descendant.unique(value)
438
+ end # end
439
+ end # end
440
+ @#{property_option} = value # @unique = value
441
+ end # end
442
+ RUBY
443
+ end
444
+
445
+ descendants.each { |descendant| descendant.accepted_options.concat(args) }
446
+ end
447
+
448
+ # Gives all the options set on this property
449
+ #
450
+ # @return [Hash] with all options and their values set on this property
451
+ #
452
+ # @api public
453
+ def options
454
+ options = {}
455
+ accepted_options.each do |name|
456
+ options[name] = send(name) if instance_variable_defined?("@#{name}")
457
+ end
458
+ options
459
+ end
460
+
461
+ # @api deprecated
462
+ def primitive(*args)
463
+ warn "Ardm::Property.primitive is deprecated, use .load_as instead (#{caller.first})"
464
+ load_as(*args)
465
+ end
466
+ end
467
+
468
+ accept_options *Property::OPTIONS
469
+
470
+ # A hook to allow properties to extend or modify the model it's bound to.
471
+ # Implementations are not supposed to modify the state of the property
472
+ # class, and should produce no side-effects on the property instance.
473
+ def bind
474
+ # no op
475
+ end
476
+
477
+ # Supplies the field in the data-store which the property corresponds to
478
+ #
479
+ # @return [String] name of field in data-store
480
+ #
481
+ # @api semipublic
482
+ def field
483
+ # defer setting the field with the adapter specific naming
484
+ # conventions until after the adapter has been setup
485
+ @field ||= model.field_naming_convention.call(self).freeze
486
+ end
487
+
488
+ # Returns true if property is unique. Serial properties and keys
489
+ # are unique by default.
490
+ #
491
+ # @return [Boolean]
492
+ # true if property has uniq index defined, false otherwise
493
+ #
494
+ # @api public
495
+ def unique?
496
+ !!@unique
497
+ end
498
+
499
+ # Returns index name if property has index.
500
+ #
501
+ # @return [Boolean, Symbol, Array]
502
+ # returns true if property is indexed by itself
503
+ # returns a Symbol if the property is indexed with other properties
504
+ # returns an Array if the property belongs to multiple indexes
505
+ # returns false if the property does not belong to any indexes
506
+ #
507
+ # @api public
508
+ attr_reader :index
509
+
510
+ # Returns true if property has unique index. Serial properties and
511
+ # keys are unique by default.
512
+ #
513
+ # @return [Boolean, Symbol, Array]
514
+ # returns true if property is indexed by itself
515
+ # returns a Symbol if the property is indexed with other properties
516
+ # returns an Array if the property belongs to multiple indexes
517
+ # returns false if the property does not belong to any indexes
518
+ #
519
+ # @api public
520
+ attr_reader :unique_index
521
+
522
+ # Returns whether or not the property is to be lazy-loaded
523
+ #
524
+ # @return [Boolean]
525
+ # true if the property is to be lazy-loaded
526
+ #
527
+ # @api public
528
+ def lazy?
529
+ @lazy
530
+ end
531
+
532
+ # Returns whether or not the property is a key or a part of a key
533
+ #
534
+ # @return [Boolean]
535
+ # true if the property is a key or a part of a key
536
+ #
537
+ # @api public
538
+ def key?
539
+ @key
540
+ end
541
+
542
+ # Returns whether or not the property is "serial" (auto-incrementing)
543
+ #
544
+ # @return [Boolean]
545
+ # whether or not the property is "serial"
546
+ #
547
+ # @api public
548
+ def serial?
549
+ @serial
550
+ end
551
+
552
+ # Returns whether or not the property must be non-nil and non-blank
553
+ #
554
+ # @return [Boolean]
555
+ # whether or not the property is required
556
+ #
557
+ # @api public
558
+ def required?
559
+ @required
560
+ end
561
+
562
+ # Returns whether or not the property can accept 'nil' as it's value
563
+ #
564
+ # @return [Boolean]
565
+ # whether or not the property can accept 'nil'
566
+ #
567
+ # @api public
568
+ def allow_nil?
569
+ @allow_nil
570
+ end
571
+
572
+ # Returns whether or not the property can be a blank value
573
+ #
574
+ # @return [Boolean]
575
+ # whether or not the property can be blank
576
+ #
577
+ # @api public
578
+ def allow_blank?
579
+ @allow_blank
580
+ end
581
+
582
+ # Standardized reader method for the property
583
+ #
584
+ # @param [Resource] resource
585
+ # model instance for which this property is to be loaded
586
+ #
587
+ # @return [Object]
588
+ # the value of this property for the provided instance
589
+ #
590
+ # @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
591
+ #
592
+ # @api private
593
+ def get(resource)
594
+ get!(resource)
595
+ end
596
+
597
+ # Fetch the ivar value in the resource
598
+ #
599
+ # @param [Resource] resource
600
+ # model instance for which this property is to be unsafely loaded
601
+ #
602
+ # @return [Object]
603
+ # current @ivar value of this property in +resource+
604
+ #
605
+ # @api private
606
+ def get!(resource)
607
+ #resource.instance_variable_get(instance_variable_name)
608
+ val = resource.send :read_attribute, field
609
+ if val.nil?
610
+ set_default_value(resource)
611
+ else
612
+ val
613
+ end
614
+ end
615
+
616
+ def set_default_value(resource)
617
+ return if loaded?(resource) || !default?
618
+ set(resource, default_for(resource))
619
+ end
620
+
621
+ # Provides a standardized setter method for the property
622
+ #
623
+ # @param [Resource] resource
624
+ # the resource to get the value from
625
+ # @param [Object] value
626
+ # the value to set in the resource
627
+ #
628
+ # @return [Object]
629
+ # +value+ after being typecasted according to this property's primitive
630
+ #
631
+ # @raise [ArgumentError] "+resource+ should be a Resource, but was ...."
632
+ #
633
+ # @api private
634
+ def set(resource, value)
635
+ set!(resource, typecast(value))
636
+ end
637
+
638
+ # Set the ivar value in the resource
639
+ #
640
+ # @param [Resource] resource
641
+ # the resource to set
642
+ # @param [Object] value
643
+ # the value to set in the resource
644
+ #
645
+ # @return [Object]
646
+ # the value set in the resource
647
+ #
648
+ # @api private
649
+ def set!(resource, value)
650
+ #resource.instance_variable_set(instance_variable_name, value)
651
+ resource.send :write_attribute, field, value
652
+ resource.send :read_attribute, field
653
+ end
654
+
655
+ # Check if the attribute corresponding to the property is loaded
656
+ #
657
+ # @param [Resource] resource
658
+ # model instance for which the attribute is to be tested
659
+ #
660
+ # @return [Boolean]
661
+ # true if the attribute is loaded in the resource
662
+ #
663
+ # @api private
664
+ def loaded?(resource)
665
+ resource.send(:read_attribute, field) != nil
666
+ #resource.instance_variable_defined?(instance_variable_name)
667
+ #true
668
+ end
669
+
670
+ # @api private
671
+ def properties
672
+ @properties ||= model.properties
673
+ end
674
+
675
+ # @api semipublic
676
+ def typecast(value)
677
+ @coercer ||= Coercible::Coercer.new
678
+ if Array === value
679
+ value.map { |v| typecast(v) }
680
+ else
681
+ @coercer[value.class].send(coercion_method, value)
682
+ end
683
+ rescue Coercible::UnsupportedCoercion
684
+ value
685
+ end
686
+
687
+ # Test the value to see if it is a valid value for this Property
688
+ #
689
+ # @param [Object] loaded_value
690
+ # the value to be tested
691
+ #
692
+ # @return [Boolean]
693
+ # true if the value is valid
694
+ #
695
+ # @api semipulic
696
+ def valid?(value, negated = false)
697
+ dumped_value = dump(value)
698
+
699
+ if required? && dumped_value.nil?
700
+ negated || false
701
+ else
702
+ value_dumped?(dumped_value) || (dumped_value.nil? && (allow_nil? || negated))
703
+ end
704
+ end
705
+
706
+ # Asserts value is valid
707
+ #
708
+ # @param [Object] loaded_value
709
+ # the value to be tested
710
+ #
711
+ # @return [Boolean]
712
+ # true if the value is valid
713
+ #
714
+ # @raise [Property::InvalidValueError]
715
+ # if value is not valid
716
+ def assert_valid_value(value)
717
+ unless valid?(value)
718
+ raise Property::InvalidValueError.new(self,value)
719
+ end
720
+ true
721
+ end
722
+
723
+ # Returns a concise string representation of the property instance.
724
+ #
725
+ # @return [String]
726
+ # Concise string representation of the property instance.
727
+ #
728
+ # @api public
729
+ def inspect
730
+ "#<#{self.class.name} @model=#{model.inspect} @name=#{name.inspect}>"
731
+ end
732
+
733
+ # Test a value to see if it matches the primitive type
734
+ #
735
+ # @param [Object] value
736
+ # value to test
737
+ #
738
+ # @return [Boolean]
739
+ # true if the value is the correct type
740
+ #
741
+ # @api semipublic
742
+ def primitive?(value)
743
+ warn "#primitive? is deprecated, use #value_dumped? instead (#{caller.first})"
744
+ value_dumped?(value)
745
+ end
746
+
747
+ def primitive
748
+ warn "#primitive is deprecated, use #dump_as instead (#{caller.first})"
749
+ dump_as
750
+ end
751
+
752
+ # @api semipublic
753
+ def value_dumped?(value)
754
+ value.kind_of?(dump_as)
755
+ end
756
+
757
+ # @api semipublic
758
+ def value_loaded?(value)
759
+ value.kind_of?(load_as)
760
+ end
761
+
762
+ protected
763
+
764
+ # @api semipublic
765
+ def initialize(model, name, options = {})
766
+ options = options.to_hash.dup
767
+
768
+ if INVALID_NAMES.include?(name.to_s) || (kind_of?(Boolean) && INVALID_NAMES.include?("#{name}?"))
769
+ raise ArgumentError,
770
+ "+name+ was #{name.inspect}, which cannot be used as a property name since it collides with an existing method or a query option"
771
+ end
772
+
773
+ assert_valid_options(options)
774
+
775
+ predefined_options = self.class.options
776
+
777
+ @model = model
778
+ @name = name.to_s.chomp('?').to_sym
779
+ @options = predefined_options.merge(options).freeze
780
+ @instance_variable_name = "@#{@name}".freeze
781
+ @coercion_method = @options.fetch(:coercion_method)
782
+
783
+ @load_as = self.class.load_as
784
+ @dump_as = self.class.dump_as
785
+
786
+ @field = @options[:field].freeze unless @options[:field].nil?
787
+ @default = @options[:default]
788
+
789
+ @serial = @options.fetch(:serial, false)
790
+ @key = @options.fetch(:key, @serial)
791
+ @unique = @options.fetch(:unique, @key ? :key : false)
792
+ @required = @options.fetch(:required, @key)
793
+ @allow_nil = @options.fetch(:allow_nil, !@required)
794
+ @allow_blank = @options.fetch(:allow_blank, !@required)
795
+ @index = @options.fetch(:index, false)
796
+ @unique_index = @options.fetch(:unique_index, @unique)
797
+ @lazy = @options.fetch(:lazy, false) && !@key
798
+
799
+ determine_visibility
800
+
801
+ bind
802
+ end
803
+
804
+ # @api private
805
+ def assert_valid_options(options)
806
+ keys = options.keys
807
+
808
+ if (unknown_keys = keys - self.class.accepted_options).any?
809
+ raise ArgumentError, "options #{unknown_keys.map { |key| key.inspect }.join(' and ')} are unknown"
810
+ end
811
+
812
+ options.each do |key, value|
813
+ boolean_value = value == true || value == false
814
+
815
+ case key
816
+ when :field
817
+ assert_kind_of "options[:#{key}]", value, ::String
818
+
819
+ when :default
820
+ if value.nil?
821
+ raise ArgumentError, "options[:#{key}] must not be nil"
822
+ end
823
+
824
+ when :serial, :key, :allow_nil, :allow_blank, :required, :auto_validation
825
+ unless boolean_value
826
+ raise ArgumentError, "options[:#{key}] must be either true or false"
827
+ end
828
+
829
+ if key == :required && (keys.include?(:allow_nil) || keys.include?(:allow_blank))
830
+ raise ArgumentError, 'options[:required] cannot be mixed with :allow_nil or :allow_blank'
831
+ end
832
+
833
+ when :index, :unique_index, :unique, :lazy
834
+ unless boolean_value || value.kind_of?(Symbol) || (value.kind_of?(Array) && value.any? && value.all? { |val| val.kind_of?(Symbol) })
835
+ raise ArgumentError, "options[:#{key}] must be either true, false, a Symbol or an Array of Symbols"
836
+ end
837
+
838
+ when :length
839
+ assert_kind_of "options[:#{key}]", value, Range, ::Integer
840
+
841
+ when :size, :precision, :scale
842
+ assert_kind_of "options[:#{key}]", value, ::Integer
843
+
844
+ when :reader, :writer, :accessor
845
+ assert_kind_of "options[:#{key}]", value, Symbol
846
+
847
+ unless VISIBILITY_OPTIONS.include?(value)
848
+ raise ArgumentError, "options[:#{key}] must be #{VISIBILITY_OPTIONS.join(' or ')}"
849
+ end
850
+ end
851
+ end
852
+ end
853
+
854
+ # Assert given visibility value is supported.
855
+ #
856
+ # Will raise ArgumentError if this Property's reader and writer
857
+ # visibilities are not included in VISIBILITY_OPTIONS.
858
+ #
859
+ # @return [undefined]
860
+ #
861
+ # @raise [ArgumentError] "property visibility must be :public, :protected, or :private"
862
+ #
863
+ # @api private
864
+ def determine_visibility
865
+ default_accessor = @options.fetch(:accessor, :public)
866
+
867
+ @reader_visibility = @options.fetch(:reader, default_accessor)
868
+ @writer_visibility = @options.fetch(:writer, default_accessor)
869
+ end
870
+ end # class Property
871
+ end
872
+
873
+ require 'ardm/property/invalid_value_error'
874
+ require 'ardm/property/object'
875
+ require 'ardm/property/string'
876
+ require 'ardm/property/binary'
877
+ require 'ardm/property/text'
878
+ require 'ardm/property/numeric'
879
+ require 'ardm/property/float'
880
+ require 'ardm/property/decimal'
881
+ require 'ardm/property/boolean'
882
+ require 'ardm/property/integer'
883
+ require 'ardm/property/serial'
884
+ require 'ardm/property/date'
885
+ require 'ardm/property/datetime'
886
+ require 'ardm/property/time'
887
+ require 'ardm/property/class'
888
+ require 'ardm/property/discriminator'
889
+ require 'ardm/property/lookup'
890
+ require 'ardm/property_set'
891
+ require 'ardm/property/validation'