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,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'