nested_record 1.0.0.beta → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6708f834d18f3aeb71c1c05238bd610d63d25cd04aa2019a7e08c78bc122e6cc
4
- data.tar.gz: 41aa9e91f4b29e186331e5a242901c7f860fc10b1fdb42cf1cb0b00cebf17483
3
+ metadata.gz: 5710483535afba122d37dded3a1cf743254f6a2ea43e40ad126adf8a98bf8ed3
4
+ data.tar.gz: 6219144c936fae21346f49380082b773e44c6e1bbed670de53f6b96a5d4200ec
5
5
  SHA512:
6
- metadata.gz: 36d195b559a3d9c0e4cdc24234dffadc9fcc7e0decfab35e98e153d3a427b1a85c1a48d4803ff0f8fa6e1c94ea81178fe6c163625276bcaeb72b187aabe05687
7
- data.tar.gz: 628ca798a9e9a704ac8d4c340d4124cba83013ac721c229a46b758452109e775f2a485971a6c0c80f9cf63eb3eff670869a0b35c60d3c2a3dac400c440facf89
6
+ metadata.gz: dbfb5c90d3a43c9a0671af39bc3969ca544deb1ba58809800d018621d4dccded78efdba763dfb425299236cb8fd631dfe6b30786fdab45d1fe6b712070afd0fd
7
+ data.tar.gz: b18afab1feb7f6e29cac1cfec75a7b4176c3a35bf3de85a6b411d4a6501617f363c5e757d04cd009094002cba78c45682f189ad9235908598db04e65aac3e0da
data/.gitignore CHANGED
@@ -1,4 +1,4 @@
1
- /.bundle/
1
+ .bundle/
2
2
  /.yardoc
3
3
  /_yardoc/
4
4
  /coverage/
@@ -6,6 +6,8 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /Gemfile.lock
10
+ /gemfiles/*.lock
9
11
 
10
12
  # rspec failure tracking
11
13
  .rspec_status
@@ -5,4 +5,7 @@ cache: bundler
5
5
  rvm:
6
6
  - 2.5
7
7
  - 2.6
8
+ gemfile:
9
+ - gemfiles/rails_5.2.gemfile
10
+ - gemfiles/rails_6.0.gemfile
8
11
  before_install: gem install bundler -v 2.0.1
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 5.2.3"
4
+
5
+ gemspec :path => "../"
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rails", "~> 6.0.0"
4
+
5
+ gemspec :path => "../"
@@ -1,6 +1,8 @@
1
1
  module NestedRecord
2
2
  require 'nested_record/version'
3
3
 
4
+ require 'forwardable'
5
+
4
6
  require 'active_record'
5
7
  require 'active_support/dependencies'
6
8
  require 'active_support/concern'
@@ -8,9 +10,14 @@ module NestedRecord
8
10
  require 'nested_record/macro'
9
11
  require 'nested_record/base'
10
12
  require 'nested_record/collection'
13
+ require 'nested_record/collection_proxy'
11
14
  require 'nested_record/setup'
15
+ require 'nested_record/nested_accessors_setup'
16
+ require 'nested_record/primary_key_check'
12
17
  require 'nested_record/methods'
13
18
  require 'nested_record/type'
14
19
  require 'nested_record/errors'
15
20
  require 'nested_record/lookup_const'
21
+ require 'nested_record/macro_recorder'
22
+ require 'nested_record/concern'
16
23
  end
@@ -4,6 +4,7 @@ class NestedRecord::Base
4
4
  include ActiveModel::Model
5
5
  include ActiveModel::Attributes
6
6
  include ActiveModel::Dirty
7
+ include ActiveModel::Validations::Callbacks
7
8
  include NestedRecord::Macro
8
9
 
9
10
  class << self
@@ -246,7 +247,17 @@ class NestedRecord::Base
246
247
  end
247
248
 
248
249
  def read_attribute(attr)
249
- @attributes.fetch_value(attr.to_s)
250
+ attribute(attr)
251
+ end
252
+
253
+ def query_attribute(attr)
254
+ value = read_attribute(attr)
255
+
256
+ case value
257
+ when true then true
258
+ when false, nil then false
259
+ else !value.blank?
260
+ end
250
261
  end
251
262
 
252
263
  def match?(attrs)
@@ -256,6 +267,8 @@ class NestedRecord::Base
256
267
  is_a? others
257
268
  when :_instance_of?, '_instance_of?'
258
269
  instance_of? others
270
+ when :_not_equal?, '_not_equal?'
271
+ !equal?(others)
259
272
  else
260
273
  ours = read_attribute(attr)
261
274
  if others.is_a? Array
@@ -268,4 +281,11 @@ class NestedRecord::Base
268
281
  end
269
282
 
270
283
  define_model_callbacks :initialize, only: :after
284
+ attribute_method_suffix '?'
285
+
286
+ private
287
+
288
+ def attribute?(attr)
289
+ query_attribute(attr)
290
+ end
271
291
  end
@@ -42,6 +42,7 @@ class NestedRecord::Collection
42
42
 
43
43
  def build(attributes = {})
44
44
  record_class.new(attributes).tap do |obj|
45
+ yield obj if block_given?
45
46
  self << obj
46
47
  end
47
48
  end
@@ -110,14 +111,19 @@ class NestedRecord::Collection
110
111
  RUBY
111
112
  end
112
113
 
114
+ def exists?(attrs)
115
+ attrs = attrs.stringify_keys
116
+ any? { |obj| obj.match?(attrs) }
117
+ end
118
+
113
119
  def find_by(attrs)
114
120
  attrs = attrs.stringify_keys
115
121
  find { |obj| obj.match?(attrs) }
116
122
  end
117
123
 
118
- def find_or_initialize_by(attrs)
124
+ def find_or_initialize_by(attrs, &block)
119
125
  attrs = attrs.stringify_keys
120
- find_by(attrs) || build(attrs)
126
+ find_by(attrs) || build(attrs, &block)
121
127
  end
122
128
 
123
129
  private
@@ -0,0 +1,68 @@
1
+ class NestedRecord::CollectionProxy
2
+ extend Forwardable
3
+ include Enumerable
4
+
5
+ class << self
6
+ def subclass_for(setup)
7
+ Class.new(self) do
8
+ methods = setup.collection_class.public_instance_methods
9
+ methods -= NestedRecord::Collection.public_instance_methods
10
+ methods -= NestedRecord::CollectionProxy.public_instance_methods(false)
11
+ def_delegators :__collection__, *methods unless methods.empty?
12
+ @setup = setup
13
+ end
14
+ end
15
+
16
+ def __nested_record_setup__
17
+ @setup
18
+ end
19
+ end
20
+
21
+ def initialize(owner)
22
+ @owner = owner
23
+ end
24
+
25
+ def method_missing(method_name, *args, &block)
26
+ collection = __collection__
27
+ if collection.respond_to? method_name
28
+ collection.public_send(method_name, *args, &block)
29
+ else
30
+ super
31
+ end
32
+ end
33
+
34
+ def respond_to_missing?(method_name, _)
35
+ super || __collection__.respond_to?(method_name)
36
+ end
37
+
38
+ def build(attributes = {})
39
+ __collection__.build(attributes) do |record|
40
+ ensure_primary! record
41
+ yield record if block_given?
42
+ end
43
+ end
44
+
45
+ def __build__(attributes)
46
+ __collection__.build(attributes)
47
+ end
48
+
49
+ def find_or_initialize_by(attributes)
50
+ __collection__.find_or_initialize_by(attributes) do |record|
51
+ ensure_primary! record
52
+ yield record if block_given?
53
+ end
54
+ end
55
+
56
+ def_delegators :__collection__, *(NestedRecord::Collection.public_instance_methods(false) - public_instance_methods(false))
57
+
58
+ def __collection__
59
+ @owner.read_attribute(self.class.__nested_record_setup__.name)
60
+ end
61
+
62
+ private
63
+
64
+ def ensure_primary!(record)
65
+ check = self.class.__nested_record_setup__.primary_check(record.read_attribute('type'))
66
+ check&.perform!(__collection__, record)
67
+ end
68
+ end
@@ -0,0 +1,14 @@
1
+ module NestedRecord::Concern
2
+ extend Forwardable
3
+
4
+ def_delegators :macro_recorder, *(NestedRecord::MacroRecorder::MACROS - [:include])
5
+
6
+ def included(mod_or_class)
7
+ super
8
+ macro_recorder.apply_to(mod_or_class)
9
+ end
10
+
11
+ def macro_recorder
12
+ @macro_recorder ||= NestedRecord::MacroRecorder.new
13
+ end
14
+ end
@@ -3,4 +3,5 @@ module NestedRecord
3
3
  class TypeMismatchError < Error; end
4
4
  class InvalidTypeError < Error; end
5
5
  class ConfigurationError < Error; end
6
+ class PrimaryKeyError < Error; end
6
7
  end
@@ -11,5 +11,9 @@ module NestedRecord::Macro
11
11
  def has_one_nested(name, **options, &block)
12
12
  NestedRecord::Setup::HasOne.new(self, name, **options, &block)
13
13
  end
14
+
15
+ def nested_accessors(from:, **options, &block)
16
+ NestedRecord::NestedAccessorsSetup.new(self, from, **options, &block)
17
+ end
14
18
  end
15
19
  end
@@ -0,0 +1,44 @@
1
+ class NestedRecord::MacroRecorder
2
+ def initialize
3
+ @macros = []
4
+ end
5
+
6
+ attr_reader :macros
7
+
8
+ MACROS = %i[
9
+ include
10
+ attribute
11
+ def_primary_uuid
12
+ primary_key
13
+ has_one_nested
14
+ has_many_nested
15
+ subtype subtypes
16
+ collection_methods
17
+ validate validates validates! validates_with validates_each
18
+ validates_absence_of
19
+ validates_acceptance_of
20
+ validates_confirmation_of
21
+ validates_exclusion_of
22
+ validates_format_of
23
+ validates_inclusion_of
24
+ validates_length_of
25
+ validates_numericality_of
26
+ validates_presence_of
27
+ validates_size_of
28
+ after_initialize
29
+ before_validation after_validation
30
+ ].freeze.each do |meth|
31
+ define_method(meth) do |*args, &block|
32
+ @macros << [meth, args, block]
33
+ end
34
+ end
35
+
36
+ def apply_to(mod_or_class)
37
+ macros = @macros
38
+ mod_or_class.module_eval do
39
+ macros.each do |meth, args, block|
40
+ public_send(meth, *args, &block)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -5,12 +5,14 @@ class NestedRecord::Methods < Module
5
5
 
6
6
  def define(name)
7
7
  method_name = public_send("#{name}_method_name")
8
- method_body = public_send("#{name}_method_body")
8
+ method_body = (bodym = public_method("#{name}_method_body")).call
9
9
  case method_body
10
10
  when Proc
11
11
  define_method(method_name, &method_body)
12
12
  when String
13
- module_eval <<~RUBY
13
+ location = bodym.source_location
14
+ location[1] += 1
15
+ module_eval <<~RUBY, *location
14
16
  def #{method_name}
15
17
  #{method_body}
16
18
  end
@@ -20,6 +22,10 @@ class NestedRecord::Methods < Module
20
22
  end
21
23
  end
22
24
 
25
+ def reader_method_name
26
+ @setup.name
27
+ end
28
+
23
29
  def writer_method_name
24
30
  :"#{@setup.name}="
25
31
  end
@@ -2,6 +2,7 @@ class NestedRecord::Methods
2
2
  class Many < self
3
3
  def initialize(setup)
4
4
  super
5
+ define :reader
5
6
  define :writer
6
7
  define :rewrite_attributes
7
8
  define :upsert_attributes
@@ -13,13 +14,30 @@ class NestedRecord::Methods
13
14
  :"validate_associated_records_for_#{@setup.name}"
14
15
  end
15
16
 
17
+ def reader_method_body
18
+ setup = @setup
19
+ ivar = :"@_#{@setup.name}_collection_proxy"
20
+ proc do
21
+ instance_variable_get(ivar) || instance_variable_set(ivar, setup.collection_proxy_class.new(self))
22
+ end
23
+ end
24
+
16
25
  def writer_method_body
17
26
  setup = @setup
18
27
  proc do |records|
19
28
  collection_class = setup.collection_class
20
- return super(records.dup) if records.is_a? collection_class
21
- collection = collection_class.new
22
- records.each { |obj| collection << obj }
29
+ if records.is_a? collection_class
30
+ collection = records.dup
31
+ else
32
+ collection = collection_class.new
33
+ records.each { |record| collection << record }
34
+ end
35
+ collection.group_by { |record| setup.primary_check(record.read_attribute('type')) }.each do |check, records|
36
+ next unless check
37
+ records.each do |record|
38
+ check.perform!(collection, record)
39
+ end
40
+ end
23
41
  super(collection)
24
42
  end
25
43
  end
@@ -39,29 +57,17 @@ class NestedRecord::Methods
39
57
  attributes = attributes.stringify_keys
40
58
  next if setup.reject_if_proc&.call(attributes)
41
59
 
42
- if (pkey_attributes = setup.primary_key)
43
- klass = setup.record_class
44
- else
45
- klass = setup.record_class.find_subtype(attributes['type'])
46
- while !(pkey_attributes = klass.primary_key) && (klass < NestedRecord::Base)
47
- klass = klass.superclass
48
- end
49
- unless pkey_attributes
50
- raise NestedRecord::ConfigurationError, 'You should specify a primary_key when using :upsert strategy'
51
- end
60
+ pkey_check = setup.primary_check(attributes['type'])
61
+ unless pkey_check
62
+ raise NestedRecord::ConfigurationError, 'You should specify a primary_key when using :upsert strategy'
52
63
  end
53
- key = { _is_a?: klass }
54
- pkey_attributes.each do |name|
55
- value = attributes[name]
56
- if (type = klass.type_for_attribute(name))
57
- value = type.cast(value)
58
- end
59
- key[name] = value
60
- end
61
- if (record = collection.find_by(key))
64
+
65
+ pkey = pkey_check.build_pkey(attributes)
66
+
67
+ if (record = collection.find_by(pkey))
62
68
  record.assign_attributes(attributes)
63
69
  else
64
- collection.build(attributes)
70
+ collection.__build__(attributes)
65
71
  end
66
72
  end
67
73
  end
@@ -0,0 +1,66 @@
1
+ class NestedRecord::NestedAccessorsSetup
2
+ def initialize(owner, name, class_name: false, default: {}, &block)
3
+ raise ArgumentError, 'block is required for .nested_accessors_in' unless block
4
+
5
+ recorder = NestedRecord::MacroRecorder.new
6
+ recorder.instance_eval(&block)
7
+
8
+ @has_one_setup = owner.has_one_nested(name, class_name: class_name, default: default, attributes_writer: { strategy: :rewrite }) do
9
+ recorder.apply_to(self)
10
+ end
11
+
12
+ @extension = Module.new
13
+
14
+ macros = [
15
+ recorder,
16
+ *recorder.macros.select do |macro, args, _|
17
+ macro == :include && args.first.is_a?(NestedRecord::Concern)
18
+ end.map! { |_, args, _| args.first.macro_recorder }
19
+ ].flat_map(&:macros)
20
+
21
+ macros.each do |macro, args, _block|
22
+ case macro
23
+ when :attribute
24
+ attr_name = args.first
25
+ delegate(attr_name)
26
+ delegate("#{attr_name}?")
27
+ delegate1("#{attr_name}=")
28
+ when :has_one_nested
29
+ assoc_name = args.first
30
+ delegate(assoc_name)
31
+ delegate("#{assoc_name}!")
32
+ delegate1("#{assoc_name}=")
33
+ delegate1("#{assoc_name}_attributes=")
34
+ when :has_many_nested
35
+ assoc_name = args.first
36
+ delegate(assoc_name)
37
+ delegate1("#{assoc_name}=")
38
+ delegate1("#{assoc_name}_attributes=")
39
+ end
40
+ end
41
+
42
+ owner.include(@extension)
43
+ end
44
+
45
+ def name
46
+ @has_one_setup.name
47
+ end
48
+
49
+ private
50
+
51
+ def delegate(meth)
52
+ @extension.class_eval <<~RUBY, __FILE__, __LINE__ + 1
53
+ def #{meth}
54
+ #{name}!.#{meth}
55
+ end
56
+ RUBY
57
+ end
58
+
59
+ def delegate1(meth)
60
+ @extension.class_eval <<~RUBY, __FILE__, __LINE__ + 1
61
+ def #{meth}(arg)
62
+ #{name}!.#{meth}(arg)
63
+ end
64
+ RUBY
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ class NestedRecord::PrimaryKeyCheck
2
+ def initialize(klass, pkey_attributes)
3
+ @klass = klass
4
+ @pkey_attributes = pkey_attributes
5
+ @params = [klass, pkey_attributes]
6
+ end
7
+
8
+ attr_reader :params
9
+
10
+ def hash
11
+ params.hash
12
+ end
13
+
14
+ def ==(other)
15
+ self.class === other && params == other.params
16
+ end
17
+ alias eql? ==
18
+
19
+ def build_pkey(obj)
20
+ pkey = { _is_a?: @klass }
21
+ if obj.is_a? @klass
22
+ pkey[:_not_equal?] = obj
23
+ @pkey_attributes.each do |name|
24
+ pkey[name] = obj.read_attribute(name)
25
+ end
26
+ elsif obj.respond_to? :[]
27
+ @pkey_attributes.each do |name|
28
+ value = obj[name]
29
+ if (type = @klass.type_for_attribute(name))
30
+ value = type.cast(value)
31
+ end
32
+ pkey[name] = value
33
+ end
34
+ else
35
+ fail
36
+ end
37
+ pkey
38
+ end
39
+
40
+ def perform!(collection, obj)
41
+ pkey = build_pkey(obj)
42
+ raise NestedRecord::PrimaryKeyError if collection.exists?(pkey)
43
+ end
44
+ end
@@ -8,53 +8,10 @@ class NestedRecord::Setup
8
8
  @owner = owner
9
9
  @name = name
10
10
 
11
- if block
12
- case (cn = options.fetch(:class_name) { false })
13
- when true
14
- cn = name.to_s.camelize
15
- cn = cn.singularize if self.is_a?(HasMany)
16
- class_name = cn
17
- when false
18
- class_name = nil
19
- when String, Symbol
20
- class_name = cn.to_s
21
- else
22
- raise NestedRecord::ConfigurationError, "Bad :class_name option #{cn.inspect}"
23
- end
24
- @record_class = Class.new(NestedRecord::Base, &block)
25
- @owner.const_set(class_name, @record_class) if class_name
26
- else
27
- if options.key? :class_name
28
- case (cn = options.fetch(:class_name))
29
- when String, Symbol
30
- @record_class = cn.to_s
31
- else
32
- raise NestedRecord::ConfigurationError, "Bad :class_name option #{cn.inspect}"
33
- end
34
- else
35
- cn = name.to_s.camelize
36
- cn = cn.singularize if self.is_a?(HasMany)
37
- @record_class = cn
38
- end
39
- end
40
-
41
- case (aw = options.fetch(:attributes_writer) { {} })
42
- when Hash
43
- @attributes_writer_opts = aw
44
- when true, false
45
- @attributes_writer_opts = {}
46
- when Symbol
47
- @attributes_writer_opts = { strategy: aw }
48
- else
49
- raise NestedRecord::ConfigurationError, "Bad :attributes_writer option #{aw.inspect}"
50
- end
51
- @reject_if_proc = @attributes_writer_opts[:reject_if]
52
-
53
- @methods_extension = build_methods_extension
54
-
55
- @owner.attribute @name, type, default: default_value
56
- @owner.include @methods_extension
57
- @owner.validate @methods_extension.validation_method_name
11
+ setup_association_attribute
12
+ setup_record_class(&block)
13
+ setup_attributes_writer_opts
14
+ setup_methods_extension
58
15
  end
59
16
 
60
17
  def record_class
@@ -66,6 +23,7 @@ class NestedRecord::Setup
66
23
 
67
24
  def primary_key
68
25
  return @primary_key if defined? @primary_key
26
+
69
27
  @primary_key = Array(@options[:primary_key])
70
28
  if @primary_key.empty?
71
29
  @primary_key = nil
@@ -77,6 +35,7 @@ class NestedRecord::Setup
77
35
 
78
36
  def attributes_writer_strategy
79
37
  return unless @options.fetch(:attributes_writer) { true }
38
+
80
39
  case (strategy = @attributes_writer_opts.fetch(:strategy) { :upsert })
81
40
  when :rewrite, :upsert
82
41
  return strategy
@@ -85,8 +44,92 @@ class NestedRecord::Setup
85
44
  end
86
45
  end
87
46
 
47
+ def primary_check(type)
48
+ if (pkey_attributes = primary_key)
49
+ klass = record_class
50
+ else
51
+ klass = record_class.find_subtype(type)
52
+ while !(pkey_attributes = klass.primary_key) && (klass < NestedRecord::Base)
53
+ klass = klass.superclass
54
+ end
55
+ end
56
+ # TODO: cache this
57
+ NestedRecord::PrimaryKeyCheck.new(klass, pkey_attributes) if pkey_attributes
58
+ end
59
+
88
60
  private
89
61
 
62
+ def setup_association_attribute
63
+ @owner.attribute name, type, default: default_value
64
+ end
65
+
66
+ def setup_record_class(&block)
67
+ if block
68
+ define_local_record_class(&block)
69
+ else
70
+ link_existing_record_class
71
+ end
72
+ end
73
+
74
+ def define_local_record_class(&block)
75
+ case (cn = @options.fetch(:class_name) { false })
76
+ when true
77
+ class_name = infer_record_class_name
78
+ when false
79
+ class_name = nil
80
+ when String, Symbol
81
+ class_name = cn.to_s
82
+ else
83
+ raise NestedRecord::ConfigurationError, "Bad :class_name option #{cn.inspect}"
84
+ end
85
+ @record_class = Class.new(NestedRecord::Base, &block)
86
+ @owner.const_set(class_name, @record_class) if class_name
87
+ end
88
+
89
+ def link_existing_record_class
90
+ if @options.key? :class_name
91
+ case (cn = @options.fetch(:class_name))
92
+ when String, Symbol
93
+ @record_class = cn.to_s
94
+ else
95
+ raise NestedRecord::ConfigurationError, "Bad :class_name option #{cn.inspect}"
96
+ end
97
+ else
98
+ @record_class = infer_record_class_name
99
+ end
100
+ end
101
+
102
+ def infer_record_class_name
103
+ cn = name.to_s.camelize
104
+ cn = cn.singularize if self.is_a?(HasMany)
105
+ cn
106
+ end
107
+
108
+ def setup_attributes_writer_opts
109
+ case (aw = @options.fetch(:attributes_writer) { {} })
110
+ when Hash
111
+ @attributes_writer_opts = aw
112
+ when true, false
113
+ @attributes_writer_opts = {}
114
+ when Symbol
115
+ @attributes_writer_opts = { strategy: aw }
116
+ else
117
+ raise NestedRecord::ConfigurationError, "Bad :attributes_writer option #{aw.inspect}"
118
+ end
119
+ @reject_if_proc = @attributes_writer_opts[:reject_if]
120
+ end
121
+
122
+ def setup_methods_extension
123
+ methods_extension = build_methods_extension
124
+ @owner.include methods_extension
125
+ @owner.const_set methods_extension_module_name, methods_extension
126
+ @owner.validate methods_extension.validation_method_name
127
+ end
128
+
129
+ def methods_extension_module_name
130
+ @methods_extension_module_name ||= :"NestedRecord_#{self.class.name.demodulize}_#{name.to_s.camelize}"
131
+ end
132
+
90
133
  class HasMany < self
91
134
  def type
92
135
  @type ||= NestedRecord::Type::Many.new(self)
@@ -96,10 +139,23 @@ class NestedRecord::Setup
96
139
  record_class.collection_class
97
140
  end
98
141
 
142
+ def collection_proxy_class
143
+ return @owner.const_get(collection_proxy_class_name, false) if @owner.const_defined?(collection_proxy_class_name, false)
144
+
145
+ @owner.const_set(
146
+ collection_proxy_class_name,
147
+ ::NestedRecord::CollectionProxy.subclass_for(self)
148
+ )
149
+ end
150
+
151
+ def collection_proxy_class_name
152
+ @collection_proxy_class_name ||= :"NestedRecord_#{self.class.name.demodulize}_#{name.to_s.camelize}_CollectionProxy"
153
+ end
154
+
99
155
  private
100
156
 
101
157
  def default_value
102
- []
158
+ @options.fetch(:default) { [] }
103
159
  end
104
160
 
105
161
  def build_methods_extension
@@ -115,7 +171,7 @@ class NestedRecord::Setup
115
171
  private
116
172
 
117
173
  def default_value
118
- nil
174
+ @options.fetch(:default) { nil }
119
175
  end
120
176
 
121
177
  def build_methods_extension
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NestedRecord
4
- VERSION = '1.0.0.beta'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -28,5 +28,5 @@ Gem::Specification.new do |spec|
28
28
  spec.add_development_dependency "pry"
29
29
  spec.add_development_dependency "pry-byebug"
30
30
 
31
- spec.add_dependency "rails", "~> 5.2"
31
+ spec.add_dependency "rails", ">= 5.2", "<= 6.0"
32
32
  end
@@ -257,6 +257,33 @@ RSpec.describe NestedRecord::Base do
257
257
  end
258
258
  end
259
259
 
260
+ describe '#query_attribute' do
261
+ it 'returns true if boolean attribute is true' do
262
+ expect(Foo.new(z: true).query_attribute(:z)).to eq true
263
+ end
264
+
265
+ it 'returns true if boolean attribute is false' do
266
+ expect(Foo.new(z: false).query_attribute(:z)).to eq false
267
+ end
268
+
269
+ it 'returns true if string attribute is non-blank' do
270
+ expect(Foo.new(x: 'a').query_attribute(:x)).to eq true
271
+ end
272
+
273
+ it 'returns false if string attribute is blank' do
274
+ expect(Foo.new(x: '').query_attribute(:x)).to eq false
275
+ end
276
+
277
+ it 'works with suffix ? version' do
278
+ foo = Foo.new(x: '1', z: false)
279
+ expect(foo.x?).to eq true
280
+ expect(foo.z?).to eq false
281
+ foo = Foo.new(x: '', z: true)
282
+ expect(foo.x?).to eq false
283
+ expect(foo.z?).to eq true
284
+ end
285
+ end
286
+
260
287
  describe '#match?' do
261
288
  let(:record) { Foo.new(x: 'aa', y: 123, z: true) }
262
289
 
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe NestedRecord::Concern do
4
+ nested_concern(:Y) do
5
+ attribute :y, :integer
6
+ has_many_nested :ys, attributes_writer: { strategy: :rewrite } do
7
+ attribute :yy
8
+ end
9
+ end
10
+ nested_model(:XY) do
11
+ attribute :x, :string
12
+ include Y
13
+ end
14
+
15
+ it 'adds attributes to the class where it is included' do
16
+ expect(XY.new(x: 'foo', y: 123, ys_attributes: [{ yy: 'yy' }])).to match an_object_having_attributes(
17
+ x: 'foo',
18
+ y: 123,
19
+ ys: [
20
+ an_object_having_attributes(yy: 'yy')
21
+ ]
22
+ )
23
+ end
24
+
25
+ context 'with nested concerns' do
26
+ nested_concern(:YZ) do
27
+ include Y
28
+ attribute :z, :boolean
29
+ end
30
+ nested_model(:XYZ) do
31
+ attribute :x, :string
32
+ include YZ
33
+ end
34
+
35
+ it 'adds attributes to the class where it is included' do
36
+ expect(XYZ.new(x: 'foo', y: 123, z: true, ys_attributes: [{ yy: 'yy' }])).to match an_object_having_attributes(
37
+ x: 'foo',
38
+ y: 123,
39
+ z: true,
40
+ ys: [
41
+ an_object_having_attributes(yy: 'yy')
42
+ ]
43
+ )
44
+ end
45
+ end
46
+ end
@@ -325,6 +325,27 @@ RSpec.describe NestedRecord do
325
325
  foo.bars = []
326
326
  expect(foo.bars).to be_empty
327
327
  end
328
+
329
+ it 'raises an error if primary key is violated' do
330
+ foo = Foo.new
331
+ expect { foo.bars = [Bar.new(id: 'wow'), Bar.new(id: 'wow')] }.to raise_error(NestedRecord::PrimaryKeyError)
332
+ end
333
+ end
334
+
335
+ describe 'build method' do
336
+ it 'builds a new record' do
337
+ foo = Foo.new
338
+ foo.bars.build(x: 'xx')
339
+ expect(foo.bars).to match [
340
+ an_object_having_attributes(x: 'xx')
341
+ ]
342
+ end
343
+
344
+ it 'raises an error if primary key is violated' do
345
+ foo = Foo.new
346
+ foo.bars.build(id: 'wow')
347
+ expect { foo.bars.build(id: 'wow') }.to raise_error(NestedRecord::PrimaryKeyError)
348
+ end
328
349
  end
329
350
 
330
351
  describe 'attributes writer' do
@@ -632,4 +653,109 @@ RSpec.describe NestedRecord do
632
653
  end
633
654
  end
634
655
  end
656
+
657
+ describe 'nested_accessors' do
658
+ active_model(:Foo) do
659
+ nested_accessors from: :bar, class_name: true do
660
+ attribute :x, :integer
661
+ has_one_nested :one, class_name: true do
662
+ attribute :y, :integer
663
+ end
664
+ has_many_nested :things, class_name: true, attributes_writer: { strategy: :rewrite } do
665
+ attribute :z, :integer
666
+ end
667
+ end
668
+ end
669
+
670
+ it 'delegates attributes writers to the inner attribute' do
671
+ foo = Foo.new(
672
+ x: 1,
673
+ one_attributes: { y: 2 },
674
+ things_attributes: [{ z: 3 }, { z: 4 }]
675
+ )
676
+ expect(foo.bar).to match an_object_having_attributes(
677
+ x: 1,
678
+ one: an_object_having_attributes(
679
+ y: 2
680
+ ),
681
+ things: [
682
+ an_object_having_attributes(z: 3),
683
+ an_object_having_attributes(z: 4)
684
+ ]
685
+ )
686
+ end
687
+
688
+ it 'delegates readers to the inner attribute' do
689
+ foo = Foo.new(
690
+ x: 1,
691
+ one_attributes: { y: 2 },
692
+ things_attributes: [{ z: 3 }, { z: 4 }]
693
+ )
694
+ expect(foo).to match an_object_having_attributes(
695
+ x: 1,
696
+ one: an_object_having_attributes(
697
+ y: 2
698
+ ),
699
+ things: [
700
+ an_object_having_attributes(z: 3),
701
+ an_object_having_attributes(z: 4)
702
+ ]
703
+ )
704
+ end
705
+
706
+ it 'delegates writers for associations' do
707
+ foo = Foo.new(x: 1)
708
+ foo.one = Foo::Bar::One.new(y: 2)
709
+ foo.things = [Foo::Bar::Thing.new(z: 3), Foo::Bar::Thing.new(z: 4)]
710
+ expect(foo.bar).to match an_object_having_attributes(
711
+ x: 1,
712
+ one: an_object_having_attributes(
713
+ y: 2
714
+ ),
715
+ things: [
716
+ an_object_having_attributes(z: 3),
717
+ an_object_having_attributes(z: 4)
718
+ ]
719
+ )
720
+ end
721
+
722
+ context 'with concerns' do
723
+ nested_concern(:X) { attribute :x, :integer }
724
+ nested_concern(:Y) do
725
+ has_one_nested :one, class_name: true do
726
+ attribute :y, :integer
727
+ end
728
+ end
729
+ nested_concern(:Z) do
730
+ has_many_nested :things, class_name: true, attributes_writer: { strategy: :rewrite } do
731
+ attribute :z, :integer
732
+ end
733
+ end
734
+ active_model(:FooXYZ) do
735
+ nested_accessors from: :bar, class_name: true do
736
+ include X
737
+ include Y
738
+ include Z
739
+ end
740
+ end
741
+
742
+ it 'delegates methods from included modules' do
743
+ foo = FooXYZ.new(
744
+ x: 1,
745
+ one_attributes: { y: 2 },
746
+ things_attributes: [{ z: 3 }, { z: 4 }]
747
+ )
748
+ expect(foo).to match an_object_having_attributes(
749
+ x: 1,
750
+ one: an_object_having_attributes(
751
+ y: 2
752
+ ),
753
+ things: [
754
+ an_object_having_attributes(z: 3),
755
+ an_object_having_attributes(z: 4)
756
+ ]
757
+ )
758
+ end
759
+ end
760
+ end
635
761
  end
@@ -19,5 +19,5 @@ RSpec.configure do |config|
19
19
  config.include TestModel::Build
20
20
  config.include TestModel::Erase
21
21
 
22
- config.after(:example) { erase_test_models }
22
+ config.after(:example) { erase_test_consts }
23
23
  end
@@ -5,18 +5,33 @@ module TestModel
5
5
  end
6
6
  end
7
7
 
8
+ def nested_concern(name, &block)
9
+ let!(:"concern_#{name.to_s.gsub('::', '_')}") do
10
+ test_consts = (@test_consts ||= [])
11
+ namespace, sname = test_const_dig_name!(name)
12
+ Module.new do
13
+ extend NestedRecord::Concern
14
+
15
+ namespace.const_set(sname, self)
16
+ test_consts << self
17
+
18
+ class_eval(&block) if block
19
+ end
20
+ end
21
+ end
22
+
8
23
  def nested_model(name, superclass = NestedRecord::Base, &block)
9
24
  let!(:"model_#{name.to_s.gsub('::', '_')}") do
10
- test_models = (@test_models ||= [])
25
+ test_consts = (@test_consts ||= [])
11
26
  if superclass.is_a?(Symbol) || superclass.is_a?(String)
12
27
  sclass = public_send("model_#{superclass.to_s.gsub('::', '_')}")
13
28
  else
14
29
  sclass = superclass
15
30
  end
16
- namespace, sname = test_model_dig_name!(name)
31
+ namespace, sname = test_const_dig_name!(name)
17
32
  Class.new(sclass) do
18
33
  namespace.const_set(sname, self)
19
- test_models << self
34
+ test_consts << self
20
35
 
21
36
  class_eval(&block) if block
22
37
  end
@@ -25,32 +40,36 @@ module TestModel
25
40
 
26
41
  module Build
27
42
  def new_active_model(name, &block)
28
- test_models = (@test_models ||= [])
43
+ test_consts = (@test_consts ||= [])
29
44
 
30
- namespace, sname = test_model_dig_name!(name)
45
+ namespace, sname = test_const_dig_name!(name)
31
46
 
32
47
  Class.new do
33
48
  namespace.const_set(sname, self)
34
- test_models << self
49
+ test_consts << self
35
50
 
36
51
  include ActiveModel::Model
37
52
  include ActiveModel::Attributes
38
53
  include NestedRecord::Macro
39
54
 
55
+ def read_attribute(attr)
56
+ attribute(attr)
57
+ end
58
+
40
59
  class_eval(&block) if block
41
60
  end
42
61
  end
43
62
  end
44
63
 
45
64
  module Erase
46
- def erase_test_models
47
- Array(@test_models).reverse_each do |klass|
65
+ def erase_test_consts
66
+ Array(@test_consts).reverse_each do |klass|
48
67
  ActiveSupport::Dependencies.remove_constant(klass.name)
49
68
  end
50
69
  ActiveSupport::Dependencies.clear
51
70
  end
52
71
 
53
- def test_model_dig_name!(name)
72
+ def test_const_dig_name!(name)
54
73
  parts = name.to_s.split('::')
55
74
  name = parts.pop
56
75
  namespace = Object
@@ -60,7 +79,7 @@ module TestModel
60
79
  namespace = namespace.const_get(part, false)
61
80
  else
62
81
  sub = Module.new
63
- (@test_models ||= []) << sub
82
+ (@test_consts ||= []) << sub
64
83
  namespace = namespace.const_set(part, sub)
65
84
  end
66
85
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nested_record
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.beta
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Kochnev
@@ -84,16 +84,22 @@ dependencies:
84
84
  name: rails
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '5.2'
90
+ - - "<="
91
+ - !ruby/object:Gem::Version
92
+ version: '6.0'
90
93
  type: :runtime
91
94
  prerelease: false
92
95
  version_requirements: !ruby/object:Gem::Requirement
93
96
  requirements:
94
- - - "~>"
97
+ - - ">="
95
98
  - !ruby/object:Gem::Version
96
99
  version: '5.2'
100
+ - - "<="
101
+ - !ruby/object:Gem::Version
102
+ version: '6.0'
97
103
  description:
98
104
  email:
99
105
  - hashtable@yandex.ru
@@ -105,21 +111,27 @@ files:
105
111
  - ".rspec"
106
112
  - ".travis.yml"
107
113
  - Gemfile
108
- - Gemfile.lock
109
114
  - LICENSE.txt
110
115
  - README.md
111
116
  - Rakefile
112
117
  - bin/console
113
118
  - bin/setup
119
+ - gemfiles/rails_5.2.gemfile
120
+ - gemfiles/rails_6.0.gemfile
114
121
  - lib/nested_record.rb
115
122
  - lib/nested_record/base.rb
116
123
  - lib/nested_record/collection.rb
124
+ - lib/nested_record/collection_proxy.rb
125
+ - lib/nested_record/concern.rb
117
126
  - lib/nested_record/errors.rb
118
127
  - lib/nested_record/lookup_const.rb
119
128
  - lib/nested_record/macro.rb
129
+ - lib/nested_record/macro_recorder.rb
120
130
  - lib/nested_record/methods.rb
121
131
  - lib/nested_record/methods/many.rb
122
132
  - lib/nested_record/methods/one.rb
133
+ - lib/nested_record/nested_accessors_setup.rb
134
+ - lib/nested_record/primary_key_check.rb
123
135
  - lib/nested_record/setup.rb
124
136
  - lib/nested_record/type.rb
125
137
  - lib/nested_record/type/many.rb
@@ -128,6 +140,7 @@ files:
128
140
  - nested_record.gemspec
129
141
  - spec/nested_record/base_spec.rb
130
142
  - spec/nested_record/collection_spec.rb
143
+ - spec/nested_record/concern_spec.rb
131
144
  - spec/nested_record/type/many_spec.rb
132
145
  - spec/nested_record/type/one_spec.rb
133
146
  - spec/nested_record_spec.rb
@@ -148,9 +161,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
161
  version: '0'
149
162
  required_rubygems_version: !ruby/object:Gem::Requirement
150
163
  requirements:
151
- - - ">"
164
+ - - ">="
152
165
  - !ruby/object:Gem::Version
153
- version: 1.3.1
166
+ version: '0'
154
167
  requirements: []
155
168
  rubyforge_project:
156
169
  rubygems_version: 2.7.6
@@ -1,152 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- nested_record (1.0.0.beta)
5
- rails (~> 5.2)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- actioncable (5.2.3)
11
- actionpack (= 5.2.3)
12
- nio4r (~> 2.0)
13
- websocket-driver (>= 0.6.1)
14
- actionmailer (5.2.3)
15
- actionpack (= 5.2.3)
16
- actionview (= 5.2.3)
17
- activejob (= 5.2.3)
18
- mail (~> 2.5, >= 2.5.4)
19
- rails-dom-testing (~> 2.0)
20
- actionpack (5.2.3)
21
- actionview (= 5.2.3)
22
- activesupport (= 5.2.3)
23
- rack (~> 2.0)
24
- rack-test (>= 0.6.3)
25
- rails-dom-testing (~> 2.0)
26
- rails-html-sanitizer (~> 1.0, >= 1.0.2)
27
- actionview (5.2.3)
28
- activesupport (= 5.2.3)
29
- builder (~> 3.1)
30
- erubi (~> 1.4)
31
- rails-dom-testing (~> 2.0)
32
- rails-html-sanitizer (~> 1.0, >= 1.0.3)
33
- activejob (5.2.3)
34
- activesupport (= 5.2.3)
35
- globalid (>= 0.3.6)
36
- activemodel (5.2.3)
37
- activesupport (= 5.2.3)
38
- activerecord (5.2.3)
39
- activemodel (= 5.2.3)
40
- activesupport (= 5.2.3)
41
- arel (>= 9.0)
42
- activestorage (5.2.3)
43
- actionpack (= 5.2.3)
44
- activerecord (= 5.2.3)
45
- marcel (~> 0.3.1)
46
- activesupport (5.2.3)
47
- concurrent-ruby (~> 1.0, >= 1.0.2)
48
- i18n (>= 0.7, < 2)
49
- minitest (~> 5.1)
50
- tzinfo (~> 1.1)
51
- arel (9.0.0)
52
- builder (3.2.3)
53
- byebug (11.0.1)
54
- coderay (1.1.2)
55
- concurrent-ruby (1.1.5)
56
- crass (1.0.5)
57
- diff-lcs (1.3)
58
- erubi (1.9.0)
59
- globalid (0.4.2)
60
- activesupport (>= 4.2.0)
61
- i18n (1.7.0)
62
- concurrent-ruby (~> 1.0)
63
- loofah (2.3.1)
64
- crass (~> 1.0.2)
65
- nokogiri (>= 1.5.9)
66
- mail (2.7.1)
67
- mini_mime (>= 0.1.1)
68
- marcel (0.3.3)
69
- mimemagic (~> 0.3.2)
70
- method_source (0.9.2)
71
- mimemagic (0.3.3)
72
- mini_mime (1.0.2)
73
- mini_portile2 (2.4.0)
74
- minitest (5.13.0)
75
- nio4r (2.5.2)
76
- nokogiri (1.10.4)
77
- mini_portile2 (~> 2.4.0)
78
- pry (0.12.2)
79
- coderay (~> 1.1.0)
80
- method_source (~> 0.9.0)
81
- pry-byebug (3.7.0)
82
- byebug (~> 11.0)
83
- pry (~> 0.10)
84
- rack (2.0.7)
85
- rack-test (1.1.0)
86
- rack (>= 1.0, < 3)
87
- rails (5.2.3)
88
- actioncable (= 5.2.3)
89
- actionmailer (= 5.2.3)
90
- actionpack (= 5.2.3)
91
- actionview (= 5.2.3)
92
- activejob (= 5.2.3)
93
- activemodel (= 5.2.3)
94
- activerecord (= 5.2.3)
95
- activestorage (= 5.2.3)
96
- activesupport (= 5.2.3)
97
- bundler (>= 1.3.0)
98
- railties (= 5.2.3)
99
- sprockets-rails (>= 2.0.0)
100
- rails-dom-testing (2.0.3)
101
- activesupport (>= 4.2.0)
102
- nokogiri (>= 1.6)
103
- rails-html-sanitizer (1.3.0)
104
- loofah (~> 2.3)
105
- railties (5.2.3)
106
- actionpack (= 5.2.3)
107
- activesupport (= 5.2.3)
108
- method_source
109
- rake (>= 0.8.7)
110
- thor (>= 0.19.0, < 2.0)
111
- rake (10.5.0)
112
- rspec (3.8.0)
113
- rspec-core (~> 3.8.0)
114
- rspec-expectations (~> 3.8.0)
115
- rspec-mocks (~> 3.8.0)
116
- rspec-core (3.8.1)
117
- rspec-support (~> 3.8.0)
118
- rspec-expectations (3.8.4)
119
- diff-lcs (>= 1.2.0, < 2.0)
120
- rspec-support (~> 3.8.0)
121
- rspec-mocks (3.8.1)
122
- diff-lcs (>= 1.2.0, < 2.0)
123
- rspec-support (~> 3.8.0)
124
- rspec-support (3.8.2)
125
- sprockets (4.0.0)
126
- concurrent-ruby (~> 1.0)
127
- rack (> 1, < 3)
128
- sprockets-rails (3.2.1)
129
- actionpack (>= 4.0)
130
- activesupport (>= 4.0)
131
- sprockets (>= 3.0.0)
132
- thor (0.20.3)
133
- thread_safe (0.3.6)
134
- tzinfo (1.2.5)
135
- thread_safe (~> 0.1)
136
- websocket-driver (0.7.1)
137
- websocket-extensions (>= 0.1.0)
138
- websocket-extensions (0.1.4)
139
-
140
- PLATFORMS
141
- ruby
142
-
143
- DEPENDENCIES
144
- bundler (~> 2.0)
145
- nested_record!
146
- pry
147
- pry-byebug
148
- rake (~> 10.0)
149
- rspec (~> 3.0)
150
-
151
- BUNDLED WITH
152
- 2.0.1