active_data 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +13 -0
  3. data/.rubocop.yml +56 -0
  4. data/.rubocop_todo.yml +53 -0
  5. data/.rvmrc +1 -1
  6. data/.travis.yml +15 -2
  7. data/Appraisals +1 -1
  8. data/CHANGELOG.md +31 -0
  9. data/Guardfile +8 -8
  10. data/README.md +256 -0
  11. data/Rakefile +2 -4
  12. data/active_data.gemspec +8 -7
  13. data/gemfiles/rails.4.0.gemfile +1 -1
  14. data/gemfiles/rails.4.1.gemfile +1 -1
  15. data/gemfiles/rails.4.2.gemfile +1 -1
  16. data/gemfiles/rails.5.0.gemfile +1 -1
  17. data/gemfiles/rails.5.1.gemfile +14 -0
  18. data/lib/active_data/active_record/associations.rb +18 -13
  19. data/lib/active_data/active_record/nested_attributes.rb +8 -14
  20. data/lib/active_data/base.rb +13 -0
  21. data/lib/active_data/config.rb +4 -4
  22. data/lib/active_data/errors.rb +29 -13
  23. data/lib/active_data/extensions.rb +22 -21
  24. data/lib/active_data/model/associations/base.rb +22 -6
  25. data/lib/active_data/model/associations/embeds_any.rb +17 -0
  26. data/lib/active_data/model/associations/embeds_many.rb +29 -19
  27. data/lib/active_data/model/associations/embeds_one.rb +30 -26
  28. data/lib/active_data/model/associations/nested_attributes.rb +82 -50
  29. data/lib/active_data/model/associations/persistence_adapters/active_record/referenced_proxy.rb +31 -0
  30. data/lib/active_data/model/associations/persistence_adapters/active_record.rb +66 -0
  31. data/lib/active_data/model/associations/persistence_adapters/base.rb +53 -0
  32. data/lib/active_data/model/associations/references_any.rb +41 -0
  33. data/lib/active_data/model/associations/references_many.rb +51 -37
  34. data/lib/active_data/model/associations/references_one.rb +43 -41
  35. data/lib/active_data/model/associations/reflections/base.rb +19 -29
  36. data/lib/active_data/model/associations/reflections/embeds_any.rb +43 -0
  37. data/lib/active_data/model/associations/reflections/embeds_many.rb +3 -13
  38. data/lib/active_data/model/associations/reflections/embeds_one.rb +5 -37
  39. data/lib/active_data/model/associations/reflections/references_any.rb +62 -0
  40. data/lib/active_data/model/associations/reflections/references_many.rb +7 -7
  41. data/lib/active_data/model/associations/reflections/references_one.rb +9 -7
  42. data/lib/active_data/model/associations/reflections/singular.rb +35 -0
  43. data/lib/active_data/model/associations/validations.rb +2 -27
  44. data/lib/active_data/model/associations.rb +12 -10
  45. data/lib/active_data/model/attributes/attribute.rb +10 -10
  46. data/lib/active_data/model/attributes/base.rb +8 -7
  47. data/lib/active_data/model/attributes/localized.rb +4 -4
  48. data/lib/active_data/model/attributes/reference_many.rb +6 -8
  49. data/lib/active_data/model/attributes/reference_one.rb +17 -9
  50. data/lib/active_data/model/attributes/reflections/attribute.rb +2 -2
  51. data/lib/active_data/model/attributes/reflections/base.rb +8 -11
  52. data/lib/active_data/model/attributes/reflections/localized.rb +2 -2
  53. data/lib/active_data/model/attributes/reflections/reference_one.rb +11 -22
  54. data/lib/active_data/model/attributes/reflections/represents.rb +5 -6
  55. data/lib/active_data/model/attributes/represents.rb +6 -5
  56. data/lib/active_data/model/attributes.rb +33 -87
  57. data/lib/active_data/model/callbacks.rb +6 -7
  58. data/lib/active_data/model/conventions.rb +2 -0
  59. data/lib/active_data/model/dirty.rb +4 -4
  60. data/lib/active_data/model/lifecycle.rb +18 -20
  61. data/lib/active_data/model/localization.rb +5 -2
  62. data/lib/active_data/model/persistence.rb +2 -2
  63. data/lib/active_data/model/primary.rb +19 -14
  64. data/lib/active_data/model/representation.rb +81 -0
  65. data/lib/active_data/model/scopes.rb +22 -12
  66. data/lib/active_data/model/validations/associated.rb +3 -2
  67. data/lib/active_data/model/validations/nested.rb +6 -1
  68. data/lib/active_data/model/validations.rb +3 -3
  69. data/lib/active_data/model.rb +2 -1
  70. data/lib/active_data/undefined_class.rb +9 -0
  71. data/lib/active_data/version.rb +1 -1
  72. data/lib/active_data.rb +40 -17
  73. data/spec/lib/active_data/active_record/associations_spec.rb +107 -45
  74. data/spec/lib/active_data/active_record/nested_attributes_spec.rb +1 -2
  75. data/spec/lib/active_data/config_spec.rb +37 -15
  76. data/spec/lib/active_data/model/associations/embeds_many_spec.rb +475 -172
  77. data/spec/lib/active_data/model/associations/embeds_one_spec.rb +353 -96
  78. data/spec/lib/active_data/model/associations/nested_attributes_spec.rb +108 -12
  79. data/spec/lib/active_data/model/associations/persistence_adapters/active_record_spec.rb +58 -0
  80. data/spec/lib/active_data/model/associations/references_many_spec.rb +440 -64
  81. data/spec/lib/active_data/model/associations/references_one_spec.rb +347 -36
  82. data/spec/lib/active_data/model/associations/reflections/embeds_many_spec.rb +8 -7
  83. data/spec/lib/active_data/model/associations/reflections/embeds_one_spec.rb +7 -6
  84. data/spec/lib/active_data/model/associations/reflections/references_many_spec.rb +81 -33
  85. data/spec/lib/active_data/model/associations/reflections/references_one_spec.rb +116 -37
  86. data/spec/lib/active_data/model/associations/validations_spec.rb +27 -43
  87. data/spec/lib/active_data/model/associations_spec.rb +34 -25
  88. data/spec/lib/active_data/model/attributes/attribute_spec.rb +26 -23
  89. data/spec/lib/active_data/model/attributes/base_spec.rb +5 -6
  90. data/spec/lib/active_data/model/attributes/collection_spec.rb +7 -8
  91. data/spec/lib/active_data/model/attributes/dictionary_spec.rb +40 -33
  92. data/spec/lib/active_data/model/attributes/localized_spec.rb +27 -28
  93. data/spec/lib/active_data/model/attributes/reflections/attribute_spec.rb +6 -6
  94. data/spec/lib/active_data/model/attributes/represents_spec.rb +10 -78
  95. data/spec/lib/active_data/model/attributes_spec.rb +150 -45
  96. data/spec/lib/active_data/model/callbacks_spec.rb +69 -70
  97. data/spec/lib/active_data/model/conventions_spec.rb +0 -1
  98. data/spec/lib/active_data/model/dirty_spec.rb +22 -13
  99. data/spec/lib/active_data/model/lifecycle_spec.rb +49 -23
  100. data/spec/lib/active_data/model/persistence_spec.rb +5 -6
  101. data/spec/lib/active_data/model/representation_spec.rb +126 -0
  102. data/spec/lib/active_data/model/scopes_spec.rb +1 -3
  103. data/spec/lib/active_data/model/typecasting_spec.rb +6 -5
  104. data/spec/lib/active_data/model/validations/associated_spec.rb +26 -18
  105. data/spec/lib/active_data/model/validations/nested_spec.rb +89 -18
  106. data/spec/lib/active_data/model_spec.rb +1 -2
  107. data/spec/lib/active_data_spec.rb +0 -1
  108. data/spec/shared/nested_attribute_examples.rb +332 -0
  109. data/spec/spec_helper.rb +3 -0
  110. data/spec/support/model_helpers.rb +2 -2
  111. data/spec/support/muffle_helper.rb +7 -0
  112. metadata +52 -18
  113. data/lib/active_data/model/associations/collection/referenced.rb +0 -26
  114. data/lib/active_data/model/associations/reflections/reference_reflection.rb +0 -45
  115. data/spec/lib/active_data/model/nested_attributes.rb +0 -202
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 78fecd490979a5391a3de607a19c5e4605198af0
4
- data.tar.gz: ebd1d4111fafc03aeb982bdb2965768920f728fd
2
+ SHA256:
3
+ metadata.gz: df62766321d027161f359a1101e7e8a94842386b72188a02d7a3686f679f466e
4
+ data.tar.gz: 0331643a7e25c573a79269df28096971189efc52730c41b58e3922a1ab0c50af
5
5
  SHA512:
6
- metadata.gz: 4d1583b8f09bf37a44343301be1bb802a62f414a8888f323dd13a1b56537986bf140cf5b238c91441ef298b6c0999883930539a3366303e8e4386863e97e357a
7
- data.tar.gz: 3b0d862750cb159de0ab0911a515e80d8a8b30160918d0ab413095932da6f37a44d43c6133bf37943003f44a49977a24c99e09f5561334d07174fbfab9f8491e
6
+ metadata.gz: de160dbcd218f15a9203e29bf6e1fdde66b028fb97cee58b8f2816e6191202909c173da2d8db6ea8fd2db02e91314510b501c358852d3e2fe9b13876d093794c
7
+ data.tar.gz: 20b7783fc647ce010928847da8e5d8c4bd9cfeb0fcb0618114b72186cbcd76e6efa723f79f0f2856f41cd7cb766a5cfa3e6eddd3c34d02b5ce949754a5e83b0b
data/.codeclimate.yml ADDED
@@ -0,0 +1,13 @@
1
+ ---
2
+ engines:
3
+ duplication:
4
+ enabled: true
5
+ config:
6
+ languages:
7
+ - ruby
8
+ rubocop:
9
+ enabled: true
10
+ ratings:
11
+ paths:
12
+ - "**.rb"
13
+ exclude_paths:
data/.rubocop.yml ADDED
@@ -0,0 +1,56 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ Lint/AmbiguousBlockAssociation:
4
+ Enabled: false
5
+
6
+ Lint/EndAlignment:
7
+ EnforcedStyleAlignWith: variable
8
+
9
+ Layout/AccessModifierIndentation:
10
+ EnforcedStyle: outdent
11
+
12
+ Layout/AlignHash:
13
+ EnforcedLastArgumentHashStyle: always_ignore
14
+
15
+ Layout/AlignParameters:
16
+ EnforcedStyle: with_fixed_indentation
17
+
18
+ Layout/CaseIndentation:
19
+ EnforcedStyle: end
20
+
21
+ Layout/IndentArray:
22
+ EnforcedStyle: consistent
23
+
24
+ Layout/IndentHash:
25
+ EnforcedStyle: consistent
26
+
27
+ Layout/IndentHeredoc:
28
+ Enabled: false
29
+
30
+ Layout/MultilineMethodCallIndentation:
31
+ EnforcedStyle: indented
32
+
33
+ Layout/MultilineOperationIndentation:
34
+ EnforcedStyle: indented
35
+
36
+ Layout/SpaceInsideHashLiteralBraces:
37
+ EnforcedStyle: no_space
38
+
39
+ Metrics/BlockLength:
40
+ Exclude:
41
+ - '**/*_spec.rb'
42
+ - '**/*_examples.rb'
43
+ - '**/*.rake'
44
+
45
+ Metrics/ModuleLength:
46
+ Exclude:
47
+ - '**/*_spec.rb'
48
+
49
+ Style/Alias:
50
+ EnforcedStyle: prefer_alias_method
51
+
52
+ Style/AndOr:
53
+ EnforcedStyle: conditionals
54
+
55
+ Style/DoubleNegation:
56
+ Enabled: false
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,53 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2016-10-14 10:15:31 +0700 using RuboCop version 0.44.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 19
10
+ Metrics/AbcSize:
11
+ Max: 54
12
+
13
+ # Offense count: 1
14
+ # Configuration parameters: CountComments.
15
+ Metrics/BlockLength:
16
+ Max: 26
17
+
18
+ # Offense count: 2
19
+ # Configuration parameters: CountComments.
20
+ Metrics/ClassLength:
21
+ Max: 128
22
+
23
+ # Offense count: 4
24
+ Metrics/CyclomaticComplexity:
25
+ Max: 13
26
+
27
+ # Offense count: 904
28
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives.
29
+ # URISchemes: http, https
30
+ Metrics/LineLength:
31
+ Max: 187
32
+
33
+ # Offense count: 21
34
+ # Configuration parameters: CountComments.
35
+ Metrics/MethodLength:
36
+ Max: 43
37
+
38
+ # Offense count: 2
39
+ # Configuration parameters: CountComments.
40
+ Metrics/ModuleLength:
41
+ Max: 114
42
+
43
+ # Offense count: 2
44
+ Metrics/BlockLength:
45
+ Max: 27
46
+
47
+ # Offense count: 4
48
+ Metrics/PerceivedComplexity:
49
+ Max: 16
50
+
51
+ # Offense count: 75
52
+ Style/Documentation:
53
+ Enabled: false
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm use --create 2.3@active_data
1
+ rvm use --create 2.4@active_data
data/.travis.yml CHANGED
@@ -1,8 +1,8 @@
1
1
  sudo: false
2
2
 
3
3
  rvm:
4
- - 2.2.5
5
- - 2.3.1
4
+ - 2.2.8
5
+ - 2.3.5
6
6
  - rbx
7
7
 
8
8
  gemfile:
@@ -10,7 +10,20 @@ gemfile:
10
10
  - gemfiles/rails.4.1.gemfile
11
11
  - gemfiles/rails.4.2.gemfile
12
12
  - gemfiles/rails.5.0.gemfile
13
+ - gemfiles/rails.5.1.gemfile
13
14
 
14
15
  matrix:
15
16
  allow_failures:
16
17
  - rvm: rbx
18
+ include:
19
+ - rvm: 2.4.2
20
+ gemfile: gemfiles/rails.5.0.gemfile
21
+ - rvm: 2.4.2
22
+ gemfile: gemfiles/rails.5.1.gemfile
23
+
24
+ before_install:
25
+ - gem update --system --no-doc
26
+
27
+ script:
28
+ - bundle exec rspec
29
+ - bundle exec rubocop
data/Appraisals CHANGED
@@ -1,4 +1,4 @@
1
- %w(4.0 4.1 4.2 5.0).each do |version|
1
+ %w[4.0 4.1 4.2 5.0 5.1].each do |version|
2
2
  appraise "rails.#{version}" do
3
3
  gem 'activesupport', "~> #{version}.0"
4
4
  gem 'activemodel', "~> #{version}.0"
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # master
2
+
3
+ # Version 1.1.0
4
+
5
+ ## Incompatible changes
6
+
7
+ * Represented attributes are not provided by default, to add them, `include ActiveData::Model::Representation` (#46)
8
+
9
+ * `include ActiveData::Model::Associations::Validations` is not included by default anymore, to get `validate_ancestry!`, `valid_ancestry?` and `invalid_ancestry?` methods back you need to include this module manually
10
+
11
+ ## Changes
12
+
13
+ * Introduce persistence adapters for associations (#24, #51)
14
+
15
+ * `ActionController::Parameters` support (#43)
16
+
17
+ * Nested attributes simple method overriding (#41)
18
+
19
+ * Persistence for `references` associations (#28, #32)
20
+
21
+ * Support `update_only` option on collection nested attributes (#30)
22
+
23
+ * `embedder` accessor for embedded associations
24
+
25
+ * Dynamic scopes for `references` associations (#27)
26
+
27
+ ## Bugfixes
28
+
29
+ * Fixed multiple validations on represented attributes and associations (#44)
30
+
31
+ * Proper boolean attributes defaults (#31, #33)
data/Guardfile CHANGED
@@ -32,8 +32,8 @@
32
32
  # * zeus: 'zeus rspec' (requires the server to be started separately)
33
33
  # * 'just' rspec: 'rspec'
34
34
 
35
- guard :rspec, cmd: "bundle exec rspec" do
36
- require "guard/rspec/dsl"
35
+ guard :rspec, cmd: 'bundle exec rspec' do
36
+ require 'guard/rspec/dsl'
37
37
  dsl = Guard::RSpec::Dsl.new(self)
38
38
 
39
39
  # Feel free to open issues for suggestions and improvements
@@ -49,15 +49,15 @@ guard :rspec, cmd: "bundle exec rspec" do
49
49
  dsl.watch_spec_files_for(ruby.lib_files)
50
50
 
51
51
  # Rails files
52
- rails = dsl.rails(view_extensions: %w(erb haml slim))
52
+ rails = dsl.rails(view_extensions: %w[erb haml slim])
53
53
  dsl.watch_spec_files_for(rails.app_files)
54
54
  dsl.watch_spec_files_for(rails.views)
55
55
 
56
56
  watch(rails.controllers) do |m|
57
57
  [
58
- rspec.spec.("routing/#{m[1]}_routing"),
59
- rspec.spec.("controllers/#{m[1]}_controller"),
60
- rspec.spec.("acceptance/#{m[1]}")
58
+ rspec.spec.call("routing/#{m[1]}_routing"),
59
+ rspec.spec.call("controllers/#{m[1]}_controller"),
60
+ rspec.spec.call("acceptance/#{m[1]}")
61
61
  ]
62
62
  end
63
63
 
@@ -67,11 +67,11 @@ guard :rspec, cmd: "bundle exec rspec" do
67
67
  watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
68
68
 
69
69
  # Capybara features specs
70
- watch(rails.view_dirs) { |m| rspec.spec.("features/#{m[1]}") }
70
+ watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
71
71
 
72
72
  # Turnip features and steps
73
73
  watch(%r{^spec/acceptance/(.+)\.feature$})
74
74
  watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
75
- Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
75
+ Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance'
76
76
  end
77
77
  end
data/README.md CHANGED
@@ -134,22 +134,278 @@ ActiveData has modular architecture, so it is required to include modules to obt
134
134
 
135
135
  ### Attributes
136
136
 
137
+ ActiveData provides several types of attributes and typecasts each attribute to its defined type upon initialization.
137
138
 
139
+ ```ruby
140
+ class Book
141
+ include ActiveData::Model
142
+
143
+ attribute :title, String
144
+ collection :author_ids, Integer
145
+ end
146
+ ```
138
147
 
139
148
  #### Attribute
149
+
150
+ ```ruby
151
+ attribute :full_name, String, default: 'John Talbot'
152
+ ```
153
+
154
+ By default, if type for attribute is not set, it is defined with `Object` type, so it would be a great idea to specify type for every attribute explicitly.
155
+
156
+ Type is necessary for attribute typecasting. Here is the list of pre-defined basic typecasters:
157
+
158
+ ```irb
159
+ [1] pry(main)> ActiveData._typecasters.keys
160
+ => ["Object", "String", "Array", "Hash", "Date", "DateTime", "Time", "ActiveSupport::TimeZone", "BigDecimal", "Float", "Integer", "Boolean", "ActiveData::UUID"]
161
+ ```
162
+
163
+ In addition, you can provide any class type when defining the attribute, but in that case you will be able to only assign instances of that specific class or value nil:
164
+
165
+ ```ruby
166
+ attribute :template, MyCustomTemplateType
167
+ ```
168
+
169
+ ##### Defaults
170
+
171
+ It is possible to provide default values for attributes and they will act in the same way as AR or Mongoid default values:
172
+
173
+ ```ruby
174
+ attribute :check, Boolean, default: false # Simply false by default
175
+ attribute :today, Date, default: ->{ Time.zone.now.to_date } # Dynamic default value
176
+ attribute :today_wday, Integer, default: ->{ today.wday } # Default is evaluated in instance context
177
+ attribute :today_wday, Integer, default: ->(instance) { instance.today.wday } # The same as previous, but instance provided explicitly
178
+ ```
179
+
180
+ ##### Enums
181
+
182
+ Enums restrict the scope of possible values for attribute. If assigned value is not included in provided list - then it turns to nil:
183
+
184
+ ```ruby
185
+ attribute :direction, String, enum: %w[north south east west]
186
+ ```
187
+
188
+ ##### Normalizers
189
+
190
+ Normalizers are applied last, modifying typecast value. It is possible to provide a list of normalizers, they will be applied in the order. It is possible to pre-define normalizers to DRY code:
191
+
192
+ ```ruby
193
+ ActiveData.normalizer(:trim) do |value, options, _attribute|
194
+ value.first(options[:length] || 2)
195
+ end
196
+
197
+ attribute :title, String, normalizers: [->(value) { value.strip }, trim: {length: 80}]
198
+ ```
199
+
200
+ ##### Readonly
201
+
202
+ ```ruby
203
+ attribute :name, String, readonly: true # Readonly forever
204
+ attribute :name, String, readonly: ->{ true } # Conditionally readonly
205
+ attribute :name, String, readonly: ->(instance) { instance.subject.present? } # Explicit instance
206
+ ```
207
+
140
208
  #### Collection
209
+
210
+ Collection is simply an array of equally-typed values:
211
+
212
+ ```ruby
213
+ class Panda
214
+ include ActiveData::Model
215
+
216
+ collection :ids, Integer
217
+ end
218
+ ```
219
+
220
+ Collection typecasts each value to specified type and also no matter what are you going to pass - it will be an array.
221
+
222
+ ```irb
223
+ [1] pry(main)> Panda.new
224
+ => #<Panda ids: []>
225
+ [2] pry(main)> Panda.new(ids: 42)
226
+ => #<Panda ids: [42]>
227
+ [3] pry(main)> Panda.new(ids: [42, '33'])
228
+ => #<Panda ids: [42, 33]>
229
+ ```
230
+
231
+ Default and enum modifiers are applied to every value, normalizer will be applied to the whole array.
232
+
141
233
  #### Dictionary
234
+
235
+ Dictionary field is a hash of specified type values with string keys:
236
+
237
+ ```ruby
238
+ class Foo
239
+ include ActiveData::Model
240
+
241
+ dictionary :ordering, String
242
+ end
243
+ ```
244
+
245
+ ```irb
246
+ [1] pry(main)> Foo.new
247
+ => #<Foo ordering: {}>
248
+ [2] pry(main)> Foo.new(ordering: {name: :desc})
249
+ => #<Foo ordering: {"name"=>"desc"}>
250
+ ```
251
+
252
+ Keys list might be restricted with `:keys` option, defaults and enums are applied to every value, normalizers are applied to the whole hash.
253
+
142
254
  #### Localized
255
+
256
+ Localized is similar to how Globalize 3 attributes work.
257
+
258
+ ```ruby
259
+ localized :title, String
260
+ ```
261
+
143
262
  #### Represents
144
263
 
264
+ Represents provides an easy way to expose model attributes through an interface.
265
+ It will automatically set passed value to the represented object **before validation**.
266
+ You can use any ActiveRecord, ActiveModel or ActiveData object as a target of representation.
267
+ A type of an attribute will be taken from it.
268
+ If there is no type, it will be `Object` by default. You can set the type explicitly by passing the `type: TypeClass` option.
269
+ Represents will also add automatic validation of the target object.
270
+
271
+ ```ruby
272
+ class Person
273
+ include ActiveData::Model
274
+
275
+ attribute :name, String
276
+ end
277
+
278
+ class Doctor
279
+ include ActiveData::Model
280
+ include ActiveData::Model::Representation
281
+
282
+ attribute :person, Object
283
+ represents :name, of: :person
284
+ end
285
+
286
+ person = Person.new(name: 'Walter Bishop')
287
+ # => #<Person name: "Walter Bishop">
288
+ Doctor.new(person: person).name
289
+ # => "Walter Bishop"
290
+ Doctor.new(person: person, name: 'Dr. Walter Bishop').name
291
+ # => "Dr. Walter Bishop"
292
+ person.name
293
+ # => "Dr. Walter Bishop"
294
+ ```
295
+
145
296
  ### Associations
146
297
 
298
+ ActiveData provides a set of associations. There are two types of them: referenced and embedded. The closest example of referenced association is AR `belongs_to` and as for embedded ones - Mongoid's embedded. Also these associations support `accepts_nested_attributes` call.
299
+
147
300
  #### EmbedsOne
301
+
302
+ ```ruby
303
+ embeds_one :profile
304
+ ```
305
+
306
+ Defines singular embedded object. Might be defined inline:
307
+
308
+ ```ruby
309
+ embeds_one :profile do
310
+ attribute :first_name, String
311
+ attribute :last_name, String
312
+ end
313
+ ```
314
+
315
+ Possible options:
316
+
317
+ * `:class_name` - association class name
318
+ * `:validate` - true or false
319
+ * `:default` - default value for association: attributes hash or instance of defined class
320
+
148
321
  #### EmbedsMany
322
+
323
+ ```ruby
324
+ embeds_many :tags
325
+ ```
326
+
327
+ Defines collection of embedded objects. Might be defined inline:
328
+
329
+ ```ruby
330
+ embeds_many :tags do
331
+ attribute :identifier, String
332
+ end
333
+ ```
334
+
335
+ * `:class_name` - association class name
336
+ * `:validate` - true or false
337
+ * `:default` - default value for association: attributes hash collection or instances of defined class
338
+
149
339
  #### ReferencesOne
340
+
341
+ ```ruby
342
+ references_one :user
343
+ ```
344
+
345
+ This will provide several methods to the object: `#user`, `#user=`, `#user_id` and `#user_id=`, just as would occur with an ActiveRecord association.
346
+
347
+ Possible options:
348
+
349
+ * `:class_name` - association class name
350
+ * `:primary_key` - associated object primary key (`:id` by default):
351
+
352
+ ```ruby
353
+ references_one :user, primary_key: :name
354
+ ```
355
+
356
+ This will create the following methods: `#user`, `#user=`, `#user_name` and `#user_name=`
357
+
358
+ * `:reference_key` - redefines `#user_id` and `#user_id=` method names completely.
359
+ * `:validate` - true or false
360
+ * `:default` - default value for association: reference or object itself
361
+
150
362
  #### ReferencesMany
363
+
364
+ ```ruby
365
+ references_many :users
366
+ ```
367
+
368
+ This will provide several methods to the object: `#users`, `#users=`, `#user_ids` and `#user_ids=` just as an ActiveRecord relation does.
369
+
370
+ Possible options:
371
+
372
+ * `:class_name` - association class name
373
+ * `:primary_key` - associated object primary key (`:id` by default):
374
+
375
+ ```ruby
376
+ references_many :users, primary_key: :name
377
+ ```
378
+
379
+ This will create the following methods: `#users`, `#users=`, `#user_names` and `#user_names=`
380
+
381
+ * `:reference_key` - redefines `#user_ids` and `#user_ids=` method names completely.
382
+ * `:validate` - true or false
383
+ * `:default` - default value for association: reference collection or objects themselves
384
+
151
385
  #### Interacting with ActiveRecord
152
386
 
387
+ ### Persistence Adapters
388
+
389
+ Adapter definition syntax:
390
+ ```ruby
391
+ class Mongoid::Document
392
+ # anything that have similar interface to
393
+ # ActiveData::Model::Associations::PersistenceAdapters::Base
394
+ def self.active_data_persistence_adapter
395
+ MongoidAdapter
396
+ end
397
+ end
398
+ ```
399
+ Where
400
+ `ClassName` - name of model class or one of ancestors
401
+ `data_source` - name of data source class
402
+ `primary_key` - key to search data
403
+ `scope_proc` - additional proc for filtering
404
+
405
+ All required interface for adapters described in `ActiveData::Model::Associations::PersistenceAdapters::Base`.
406
+
407
+ Adapter for ActiveRecord is `ActiveData::Model::Associations::PersistenceAdapters::ActiveRecord`. So, all AR models will use `PersistenceAdapters::ActiveRecord` by default.
408
+
153
409
  ### Primary
154
410
 
155
411
  ### Persistence
data/Rakefile CHANGED
@@ -1,8 +1,6 @@
1
- #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
3
-
1
+ require 'bundler/gem_tasks'
4
2
  require 'rspec/core/rake_task'
5
3
 
6
4
  RSpec::Core::RakeTask.new(:spec)
7
5
 
8
- task :default => :spec
6
+ task default: :spec
data/active_data.gemspec CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  require File.expand_path('../lib/active_data/version', __FILE__)
3
2
 
4
3
  Gem::Specification.new do |gem|
@@ -8,23 +7,25 @@ Gem::Specification.new do |gem|
8
7
  gem.summary = 'Working with hashes in AR style'
9
8
  gem.homepage = ''
10
9
 
11
- gem.files = `git ls-files`.split($\)
12
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
10
+ gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
13
12
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
13
  gem.name = 'active_data'
15
14
  gem.require_paths = ['lib']
16
15
  gem.version = ActiveData::VERSION
17
16
 
18
- gem.add_development_dependency 'rake'
17
+ gem.add_development_dependency 'activerecord', '>= 4.0'
19
18
  gem.add_development_dependency 'appraisal'
19
+ gem.add_development_dependency 'database_cleaner'
20
+ gem.add_development_dependency 'rake'
20
21
  gem.add_development_dependency 'rspec'
21
22
  gem.add_development_dependency 'rspec-its'
23
+ gem.add_development_dependency 'rubocop'
22
24
  gem.add_development_dependency 'rubysl', '~> 2.0' if RUBY_ENGINE == 'rbx'
23
25
  gem.add_development_dependency 'sqlite3'
24
- gem.add_development_dependency 'database_cleaner'
25
- gem.add_development_dependency 'activerecord', '>= 4.0'
26
26
  gem.add_development_dependency 'uuidtools'
27
- gem.add_runtime_dependency 'activesupport', '>= 4.0'
27
+
28
28
  gem.add_runtime_dependency 'activemodel', '>= 4.0'
29
+ gem.add_runtime_dependency 'activesupport', '>= 4.0'
29
30
  gem.add_runtime_dependency 'tzinfo'
30
31
  end
@@ -11,4 +11,4 @@ group :test do
11
11
  gem "guard-rspec"
12
12
  end
13
13
 
14
- gemspec :path => "../"
14
+ gemspec path: "../"
@@ -11,4 +11,4 @@ group :test do
11
11
  gem "guard-rspec"
12
12
  end
13
13
 
14
- gemspec :path => "../"
14
+ gemspec path: "../"
@@ -11,4 +11,4 @@ group :test do
11
11
  gem "guard-rspec"
12
12
  end
13
13
 
14
- gemspec :path => "../"
14
+ gemspec path: "../"
@@ -11,4 +11,4 @@ group :test do
11
11
  gem "guard-rspec"
12
12
  end
13
13
 
14
- gemspec :path => "../"
14
+ gemspec path: "../"
@@ -0,0 +1,14 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activesupport", "~> 5.1.0"
6
+ gem "activemodel", "~> 5.1.0"
7
+ gem "activerecord", "~> 5.1.0"
8
+
9
+ group :test do
10
+ gem "guard"
11
+ gem "guard-rspec"
12
+ end
13
+
14
+ gemspec path: "../"
@@ -1,15 +1,26 @@
1
1
  module ActiveData
2
2
  module ActiveRecord
3
3
  module Associations
4
+ READER = lambda do |ref, object|
5
+ value = object.read_attribute(ref.name)
6
+ if value.present?
7
+ value.is_a?(String) ? JSON.parse(value) : value
8
+ end
9
+ end
10
+
11
+ WRITER = lambda do |ref, object, value|
12
+ object.send(:write_attribute, ref.name, value ? value.to_json : nil)
13
+ end
14
+
4
15
  module Reflections
5
16
  class EmbedsOne < ActiveData::Model::Associations::Reflections::EmbedsOne
6
- def is_a? klass
17
+ def is_a?(klass)
7
18
  super || klass == ::ActiveRecord::Reflection::AssociationReflection
8
19
  end
9
20
  end
10
21
 
11
22
  class EmbedsMany < ActiveData::Model::Associations::Reflections::EmbedsMany
12
- def is_a? klass
23
+ def is_a?(klass)
13
24
  super || klass == ::ActiveRecord::Reflection::AssociationReflection
14
25
  end
15
26
  end
@@ -18,17 +29,11 @@ module ActiveData
18
29
  extend ActiveSupport::Concern
19
30
 
20
31
  included do
21
- { embeds_many: Reflections::EmbedsMany, embeds_one: Reflections::EmbedsOne }.each do |(name, reflection_class)|
22
- define_singleton_method name do |name, options = {}, &block|
23
- reflection = reflection_class.build(self, self, name, options.reverse_merge(
24
- read: ->(reflection, object) {
25
- value = object.read_attribute(reflection.name)
26
- JSON.parse(value) if value.present?
27
- },
28
- write: ->(reflection, object, value) {
29
- object.send(:write_attribute, reflection.name, value ? value.to_json : nil)
30
- }
31
- ), &block)
32
+ {embeds_many: Reflections::EmbedsMany, embeds_one: Reflections::EmbedsOne}.each do |(method, reflection_class)|
33
+ define_singleton_method method do |name, options = {}, &block|
34
+ reflection = reflection_class.build(self, self, name,
35
+ options.reverse_merge(read: READER, write: WRITER),
36
+ &block)
32
37
  if ::ActiveRecord::Reflection.respond_to? :add_reflection
33
38
  ::ActiveRecord::Reflection.add_reflection self, reflection.name, reflection
34
39
  else