nested_record 0.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.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NestedRecord::Collection
4
+ class << self
5
+ attr_reader :record_class
6
+ end
7
+
8
+ include Enumerable
9
+
10
+ def initialize
11
+ @ary = []
12
+ end
13
+
14
+ def initialize_dup(orig)
15
+ @ary = orig.to_ary
16
+ end
17
+
18
+ def as_json
19
+ @ary.as_json
20
+ end
21
+
22
+ def each
23
+ return to_enum(:each) unless block_given?
24
+ @ary.each(&proc)
25
+ end
26
+
27
+ def to_ary
28
+ @ary.dup
29
+ end
30
+
31
+ def ==(other)
32
+ @ary == other.to_ary
33
+ end
34
+
35
+ def <<(obj)
36
+ unless obj.kind_of?(record_class)
37
+ raise NestedRecord::TypeMismatchError, "#{obj.inspect} should be a #{record_class}"
38
+ end
39
+ @ary << obj
40
+ self
41
+ end
42
+
43
+ def build(attributes = {})
44
+ record_class.new(attributes).tap do |obj|
45
+ self << obj
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ @ary.inspect
51
+ end
52
+
53
+ def empty?
54
+ @ary.empty?
55
+ end
56
+
57
+ def clear
58
+ @ary.clear
59
+ self
60
+ end
61
+
62
+ def length
63
+ @ary.length
64
+ end
65
+
66
+ def select!
67
+ return to_enum(:select!) unless block_given?
68
+ @ary.select!(&proc)
69
+ self
70
+ end
71
+
72
+ def reject!
73
+ return to_enum(:reject!) unless block_given?
74
+ @ary.reject!(&proc)
75
+ self
76
+ end
77
+
78
+ def reject_by!(attrs)
79
+ return to_enum(:reject_by!) unless block_given?
80
+ attrs = attrs.stringify_keys
81
+ reject! { |obj| obj.match?(attrs) }
82
+ end
83
+
84
+ def find_by(attrs)
85
+ attrs = attrs.stringify_keys
86
+ find { |obj| obj.match?(attrs) }
87
+ end
88
+
89
+ def find_or_initialize_by(attrs)
90
+ attrs = attrs.stringify_keys
91
+ find_by(attrs) || build(attrs)
92
+ end
93
+
94
+ private
95
+
96
+ def record_class
97
+ self.class.record_class
98
+ end
99
+ end
@@ -0,0 +1,5 @@
1
+ module NestedRecord
2
+ class Error < StandardError; end
3
+ class TypeMismatchError < Error; end
4
+ class InvalidTypeError < Error; end
5
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NestedRecord
4
+ # Derived from Rails source: https://github.com/rails/rails/blob/98a57aa5f610bc66af31af409c72173cdeeb3c9e/activerecord/lib/active_record/inheritance.rb#L181-L207
5
+
6
+ def self.lookup_const(owner, type_name)
7
+ if type_name.start_with?('::')
8
+ ActiveSupport::Dependencies.constantize(type_name)
9
+ else
10
+ candidates = []
11
+ owner.name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
12
+ candidates << type_name
13
+
14
+ candidates.each do |candidate|
15
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
16
+ return constant if candidate == constant.to_s
17
+ end
18
+
19
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NestedRecord::Macro
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ def has_many_nested(name, **options, &block)
8
+ NestedRecord::Setup::HasMany.new(self, name, **options, &block)
9
+ end
10
+
11
+ def has_one_nested(name, **options)
12
+ NestedRecord::Setup::HasOne.new(self, name, **options)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NestedRecord::Setup
4
+ def initialize(owner, name, **options, &extension)
5
+ @options = options
6
+ @owner = owner
7
+ @record_class = options[:class_name] || name.to_s.classify
8
+ @name = name
9
+ @extension = extension
10
+
11
+ define_methods
12
+
13
+ @owner.attribute @name, type, default: default_value
14
+ @owner.validate validation_method_name
15
+ end
16
+
17
+ def record_class
18
+ if @record_class.is_a? String
19
+ @record_class = NestedRecord.lookup_const(@owner, @record_class)
20
+ end
21
+ @record_class
22
+ end
23
+
24
+ private
25
+
26
+ def writer_method_name
27
+ @writer_method_name ||= :"#{@name}="
28
+ end
29
+
30
+ def attributes_writer_method_name
31
+ @attributes_writer_method_name ||= :"#{@name}_attributes="
32
+ end
33
+
34
+ def validation_method_name
35
+ @validation_method_name ||= :"validate_associated_records_for_#{@name}"
36
+ end
37
+
38
+ def define_methods
39
+ define_writer_method
40
+ define_attributes_writer_method
41
+ define_validation_method
42
+ end
43
+
44
+ class HasMany < self
45
+ def initialize(*)
46
+ super
47
+ if (attributes_writer_opts = @options[:attributes_writer]).is_a? Hash
48
+ @reject_if_proc = attributes_writer_opts[:reject_if]
49
+ end
50
+ end
51
+
52
+ def type
53
+ @type ||= NestedRecord::Type::HasMany.new(self)
54
+ end
55
+
56
+ def collection_class_name
57
+ @collection_class_name ||= :"NestedRecord_Many#{@name.to_s.camelize}"
58
+ end
59
+
60
+ def collection_class
61
+ return @owner.const_get(collection_class_name, false) if @owner.const_defined?(collection_class_name, false)
62
+ extension = @extension
63
+ collection_superclass = record_class.collection_class
64
+ @owner.const_set(
65
+ collection_class_name,
66
+ Class.new(collection_superclass) do
67
+ @record_class = collection_superclass.record_class
68
+ include Module.new(&extension) if extension
69
+ end
70
+ )
71
+ end
72
+
73
+ def reject?(attributes)
74
+ @reject_if_proc&.call(attributes)
75
+ end
76
+
77
+ private
78
+
79
+ def default_value
80
+ []
81
+ end
82
+
83
+ def define_writer_method
84
+ setup = self
85
+ @owner.define_method(writer_method_name) do |records|
86
+ collection_class = setup.collection_class
87
+ return super(records.dup) if records.is_a? collection_class
88
+ collection = collection_class.new
89
+ records.each do |obj|
90
+ collection << obj
91
+ end
92
+ super(collection)
93
+ end
94
+ end
95
+
96
+ def define_attributes_writer_method
97
+ return unless @options.fetch(:attributes_writer) { true }
98
+ setup = self
99
+ writer_method = writer_method_name
100
+ @owner.define_method(attributes_writer_method_name) do |data|
101
+ attributes_collection =
102
+ if data.is_a? Hash
103
+ data.values
104
+ else
105
+ data
106
+ end
107
+ collection = setup.collection_class.new
108
+ attributes_collection.each do |attributes|
109
+ attributes = attributes.stringify_keys
110
+ next if setup.reject?(attributes)
111
+ collection.build(attributes)
112
+ end
113
+ public_send(writer_method, collection)
114
+ end
115
+ end
116
+
117
+ def define_validation_method
118
+ setup = self
119
+ name = @name
120
+ @owner.define_method(validation_method_name) do
121
+ collection = public_send(name)
122
+ collection.map do |record|
123
+ next true if record.valid?
124
+ record.errors.each do |attribute, message|
125
+ error_attribute = "#{name}.#{attribute}"
126
+ errors[error_attribute] << message
127
+ errors[error_attribute].uniq!
128
+ end
129
+ record.errors.details.each_key do |attribute|
130
+ error_attribute = "#{name}.#{attribute}"
131
+ record.errors.details[attribute].each do |error|
132
+ errors.details[error_attribute] << error
133
+ errors.details[error_attribute].uniq!
134
+ end
135
+ end
136
+ false
137
+ end.all?
138
+ end
139
+ end
140
+ end
141
+
142
+ class HasOne < self
143
+ def define_methods
144
+ define_writer_method
145
+ define_build_method
146
+ define_attributes_writer_method
147
+ define_validation_method
148
+ define_bang_method
149
+ end
150
+
151
+ def type
152
+ @type ||= NestedRecord::Type::HasOne.new(self)
153
+ end
154
+
155
+ private
156
+
157
+ def default_value
158
+ nil
159
+ end
160
+
161
+ def build_method_name
162
+ :"build_#{@name}"
163
+ end
164
+
165
+ def bang_method_name
166
+ :"#{@name}!"
167
+ end
168
+
169
+ def define_writer_method
170
+ setup = self
171
+ @owner.define_method(writer_method_name) do |record|
172
+ unless record.nil? || record.kind_of?(setup.record_class)
173
+ raise NestedRecord::TypeMismatchError, "#{record.inspect} should be a #{setup.record_class}"
174
+ end
175
+ super(record)
176
+ end
177
+ end
178
+
179
+ def define_attributes_writer_method
180
+ return unless @options.fetch(:attributes_writer) { true }
181
+ @owner.alias_method attributes_writer_method_name, build_method_name
182
+ end
183
+
184
+ def define_validation_method
185
+ setup = self
186
+ name = @name
187
+ @owner.define_method(validation_method_name) do
188
+ record = public_send(name)
189
+ return true unless record
190
+ return true if record.valid?
191
+
192
+ record.errors.each do |attribute, message|
193
+ error_attribute = "#{name}.#{attribute}"
194
+ errors.details[error_attribute] << message
195
+ errors.details[error_attribute].uniq!
196
+ end
197
+ record.errors.details.each_key do |attribute|
198
+ error_attribute = "#{name}.#{attribute}"
199
+ record.errors.details[attribute].each do |error|
200
+ errors.details[error_attribute] << error
201
+ errors.details[error_attribute].uniq!
202
+ end
203
+ end
204
+ false
205
+ end
206
+ end
207
+
208
+ def define_build_method
209
+ setup = self
210
+ writer_method = writer_method_name
211
+ @owner.define_method(build_method_name) do |attributes = {}|
212
+ record = setup.record_class.new(attributes)
213
+ public_send(writer_method, record)
214
+ end
215
+ end
216
+
217
+ def define_bang_method
218
+ @owner.class_eval <<~RUBY
219
+ def #{bang_method_name}
220
+ #{@name} || #{build_method_name}
221
+ end
222
+ RUBY
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class NestedRecord::Type < ActiveRecord::Type::Json
4
+ def initialize(setup)
5
+ @setup = setup
6
+ end
7
+
8
+ def cast(data)
9
+ cast_value(data)
10
+ end
11
+
12
+ def deserialize(value)
13
+ cast_value(super)
14
+ end
15
+
16
+ def serialize(obj)
17
+ super(obj&.as_json)
18
+ end
19
+
20
+ private
21
+
22
+ def record_class
23
+ @setup.record_class
24
+ end
25
+
26
+ class HasMany < self
27
+ private
28
+
29
+ def collection_class
30
+ @setup.collection_class
31
+ end
32
+
33
+ def cast_value(data)
34
+ return unless data
35
+ collection = collection_class.new
36
+ data.each do |obj|
37
+ if obj.is_a? Hash
38
+ collection << record_class.instantiate(obj)
39
+ elsif obj.kind_of?(record_class)
40
+ collection << obj
41
+ else
42
+ raise "Cannot cast #{obj.inspect}"
43
+ end
44
+ end
45
+ collection
46
+ end
47
+ end
48
+
49
+ class HasOne < self
50
+ private
51
+
52
+ def cast_value(obj)
53
+ return unless obj
54
+
55
+ if obj.is_a? Hash
56
+ record_class.instantiate(obj)
57
+ elsif obj.kind_of?(record_class)
58
+ obj
59
+ else
60
+ raise "Cannot cast #{obj.inspect}"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NestedRecord
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,32 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "nested_record/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "nested_record"
8
+ spec.version = NestedRecord::VERSION
9
+ spec.authors = ["Vladimir Kochnev"]
10
+ spec.email = ["hashtable@yandex.ru"]
11
+
12
+ spec.summary = %q{ActiveModel mapper for JSON fields}
13
+ spec.homepage = "https://github.com/marshall-lee/nested_record"
14
+ spec.license = "MIT"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test)/}) }
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 2.0"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "pry"
29
+ spec.add_development_dependency "pry-byebug"
30
+
31
+ spec.add_dependency "rails", "~> 5.2"
32
+ end