nested_record 1.0.0.beta → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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