active_data 1.0.0 → 1.1.0

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 (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