nested_record 0.1.0

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