sumaki 0.2.0 → 0.3.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: 4b7187ea951d6b5b682b054303d696ce2c65e205519c151aed52a7e6a542cdf7
4
- data.tar.gz: 70631c497947954364961b3fde444bcc56be61b534687826872cab5efa139e86
3
+ metadata.gz: 4cc5d96c3a979b46ace35c6bcae70136875b415af2dbe971f45731ff69aa61ff
4
+ data.tar.gz: dbae61d4d18b0c6e3034620ea3dbe77467b4133da5df1eb45b9d89c927d98504
5
5
  SHA512:
6
- metadata.gz: a913803fdde6db3a69bce4ad967048673c6d3b089d05265029d21e09074b817d45cb7b3c2f1570cc50e3ec034bdb85420eb7f38a0b6625803d2054bbb4fad733
7
- data.tar.gz: 355746b7f56600a79023a056aaaee86a599f9b5951b371b8b6a600086428cf392528f8447bc03f71f3173a91cd939b661d1134cb80cae4d99c993053e9973987
6
+ metadata.gz: 9a7ecf9d761bf8b6f8ff2b5236090f83105795dcb78e45d27c109a7377ead5ed323a38e51a6b3be5996bedd95917e7676e13894dd16517bac180de6ea589be6f
7
+ data.tar.gz: 1c3624fdf113f3d260e58e00f68c96f51abcdac1fe1f55e7f30ec93a3b62159b3a051618a3bfe4b9c791f66499d6160429083c73eb90d2661d97e027c74c0f16
data/.rubocop.yml CHANGED
@@ -5,12 +5,12 @@ AllCops:
5
5
 
6
6
  Lint/ConstantDefinitionInBlock:
7
7
  Exclude:
8
- - 'spec/sumaki/model/association_spec.rb'
8
+ - 'spec/sumaki/model/associations_spec.rb'
9
9
  - 'spec/sumaki/model/attribute_spec.rb'
10
10
  - 'spec/sumaki/model/enum_spec.rb'
11
11
 
12
12
  RSpec/LeakyConstantDeclaration:
13
13
  Exclude:
14
- - 'spec/sumaki/model/association_spec.rb'
14
+ - 'spec/sumaki/model/associations_spec.rb'
15
15
  - 'spec/sumaki/model/attribute_spec.rb'
16
16
  - 'spec/sumaki/model/enum_spec.rb'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sumaki (0.1.0)
4
+ sumaki (0.3.0)
5
5
  minenum
6
6
 
7
7
  GEM
@@ -14,14 +14,14 @@ GEM
14
14
  diff-lcs (1.5.1)
15
15
  docile (1.4.0)
16
16
  io-console (0.7.2)
17
- irb (1.12.0)
18
- rdoc
17
+ irb (1.13.1)
18
+ rdoc (>= 4.0.0)
19
19
  reline (>= 0.4.2)
20
20
  json (2.7.2)
21
21
  language_server-protocol (3.17.0.3)
22
22
  minenum (0.1.0)
23
23
  parallel (1.24.0)
24
- parser (3.3.0.5)
24
+ parser (3.3.1.0)
25
25
  ast (~> 2.4.1)
26
26
  racc
27
27
  psych (5.1.2)
@@ -31,10 +31,11 @@ GEM
31
31
  rake (13.2.1)
32
32
  rdoc (6.6.3.1)
33
33
  psych (>= 4.0.0)
34
- regexp_parser (2.9.0)
35
- reline (0.5.2)
34
+ regexp_parser (2.9.2)
35
+ reline (0.5.7)
36
36
  io-console (~> 0.5)
37
- rexml (3.2.6)
37
+ rexml (3.2.8)
38
+ strscan (>= 3.0.9)
38
39
  rspec (3.13.0)
39
40
  rspec-core (~> 3.13.0)
40
41
  rspec-expectations (~> 3.13.0)
@@ -44,11 +45,11 @@ GEM
44
45
  rspec-expectations (3.13.0)
45
46
  diff-lcs (>= 1.2.0, < 2.0)
46
47
  rspec-support (~> 3.13.0)
47
- rspec-mocks (3.13.0)
48
+ rspec-mocks (3.13.1)
48
49
  diff-lcs (>= 1.2.0, < 2.0)
49
50
  rspec-support (~> 3.13.0)
50
51
  rspec-support (3.13.1)
51
- rubocop (1.63.3)
52
+ rubocop (1.63.5)
52
53
  json (~> 2.3)
53
54
  language_server-protocol (>= 3.17.0)
54
55
  parallel (~> 1.10)
@@ -59,13 +60,13 @@ GEM
59
60
  rubocop-ast (>= 1.31.1, < 2.0)
60
61
  ruby-progressbar (~> 1.7)
61
62
  unicode-display_width (>= 2.4.0, < 3.0)
62
- rubocop-ast (1.31.2)
63
- parser (>= 3.3.0.4)
63
+ rubocop-ast (1.31.3)
64
+ parser (>= 3.3.1.0)
64
65
  rubocop-capybara (2.20.0)
65
66
  rubocop (~> 1.41)
66
67
  rubocop-factory_bot (2.25.1)
67
68
  rubocop (~> 1.41)
68
- rubocop-rspec (2.29.1)
69
+ rubocop-rspec (2.29.2)
69
70
  rubocop (~> 1.40)
70
71
  rubocop-capybara (~> 2.17)
71
72
  rubocop-factory_bot (~> 2.22)
@@ -80,6 +81,7 @@ GEM
80
81
  simplecov-html (0.12.3)
81
82
  simplecov_json_formatter (0.1.4)
82
83
  stringio (3.1.0)
84
+ strscan (3.1.0)
83
85
  unicode-display_width (2.5.0)
84
86
 
85
87
  PLATFORMS
data/README.md CHANGED
@@ -93,12 +93,22 @@ class Anime
93
93
  field :title
94
94
  field :url
95
95
  end
96
+ ```
96
97
 
98
+ ```ruby
99
+ # Read the field values
97
100
  anime = Anime.new({ title: 'The Vampire Dies in No Time', url: 'https://sugushinu-anime.jp/' })
98
101
  anime.title #=> 'The Vampire Dies in No Time'
99
102
  anime.url #=> 'https://sugushinu-anime.jp/'
100
103
  ```
101
104
 
105
+ ```ruby
106
+ # Write the field value
107
+ anime = Anime.new({})
108
+ anime.title = 'The Vampire Dies in No Time'
109
+ anime.title #=> 'The Vampire Dies in No Time'
110
+ ```
111
+
102
112
  If the data contains attributes not declared in the field, it raises no error and is simply ignored.
103
113
 
104
114
  ### Access to the sub object
@@ -114,23 +124,31 @@ class Book
114
124
  class Company
115
125
  include Sumaki::Model
116
126
  field :name
117
- field :prefecture
118
127
  end
119
128
  end
129
+ ```
120
130
 
131
+ ```ruby
121
132
  data = {
122
133
  title: 'The Ronaldo Chronicles',
123
134
  company: {
124
135
  name: 'Autumn Books',
125
- prefecture: 'Tokyo'
126
136
  }
127
137
  }
128
138
 
129
- comic = Book.new(data)
130
- comic.company.name #=> 'Autumn Books'
139
+ # Read from the sub object
140
+ book = Book.new(data)
141
+ book.company.name #=> 'Autumn Books'
142
+ ```
143
+
144
+ ```ruby
145
+ # Build a sub object
146
+ book = Book.new({})
147
+ book.build_company(name: 'Autumn Books')
148
+ book.company #=> #<Book::Company:0x000073a618e31e80 name: "Autumn Books">
131
149
  ```
132
150
 
133
- sub object is wrapped with the class inferred from the field name under the original class.
151
+ Sub object is wrapped with the class inferred from the field name under the original class.
134
152
 
135
153
  This can be changed by specifying the class to wrap.
136
154
 
@@ -172,7 +190,9 @@ class Company
172
190
  field :name
173
191
  end
174
192
  end
193
+ ```
175
194
 
195
+ ```ruby
176
196
  data = {
177
197
  name: 'The Ronaldo Vampire Hunter Agency',
178
198
  member: [
@@ -182,11 +202,19 @@ data = {
182
202
  ]
183
203
  }
184
204
 
205
+ # Read from the sub object
185
206
  company = Company.new(data)
186
207
  company.member.size #=> 3
187
208
  company.member[2].name #=> 'John'
188
209
  ```
189
210
 
211
+ ```ruby
212
+ # Build a sub object
213
+ company = Company.new({})
214
+ company.member.build(name: 'John')
215
+ company.member[0].name #=> 'John'
216
+ ```
217
+
190
218
  The `class_name` option can also be used to specify the class to wrap.
191
219
 
192
220
  ### Access to the parent object
@@ -227,14 +255,25 @@ class Character
227
255
  field :name
228
256
  enum :type, { vampire: 1, vampire_hunter: 2, familier: 3, editor: 4 }
229
257
  end
258
+ ```
230
259
 
260
+ ```ruby
231
261
  data = {
232
262
  name: 'John',
233
263
  type: 3
234
264
  }
235
265
 
266
+ # Read the enum
236
267
  character = Character.new(data)
237
- character.type #=> :familier
268
+ character.type.name #=> :familier
269
+ character.type.familier? #=> true
270
+ ```
271
+
272
+ ```ruby
273
+ # Write the enum value
274
+ character = Character.new({})
275
+ character.type = 1
276
+ character.type.name #=> :vampire
238
277
  ```
239
278
 
240
279
 
@@ -7,6 +7,22 @@ module Sumaki
7
7
  def get(data, key)
8
8
  data[key]
9
9
  end
10
+
11
+ def set(data, key, value)
12
+ data[key] = value
13
+ end
14
+
15
+ def build_singular(data, name)
16
+ data[name] = {}
17
+ end
18
+
19
+ def build_repeated_element(_data, _name)
20
+ {}
21
+ end
22
+
23
+ def apply_repeated(data, name, objects)
24
+ data[name] = objects
25
+ end
10
26
  end
11
27
  end
12
28
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sumaki
4
+ module Model
5
+ module Associations
6
+ module Association
7
+ class Singular # :nodoc:
8
+ def initialize(owner, reflection)
9
+ @owner = owner
10
+ @reflection = reflection
11
+ end
12
+
13
+ def model
14
+ @model ||= begin
15
+ object = @owner.get(@reflection.name)
16
+ object.nil? ? nil : @reflection.model_class.new(object, parent: @owner)
17
+ end
18
+ end
19
+
20
+ def build_model(attrs = {})
21
+ assoc = @owner.object_accessor.build_singular(@reflection.name)
22
+
23
+ model = @reflection.model_class.new(assoc, parent: @owner)
24
+ model.assign(attrs)
25
+
26
+ @model = model
27
+ end
28
+ end
29
+
30
+ class Repeated # :nodoc:
31
+ def initialize(owner, reflection)
32
+ @owner = owner
33
+ @reflection = reflection
34
+ end
35
+
36
+ def collection
37
+ @collection ||= begin
38
+ objects_or_value = @owner.get(@reflection.name)
39
+ models = if objects_or_value.is_a?(Array)
40
+ model_class = @reflection.model_class
41
+ objects_or_value.map { |object| model_class.new(object, parent: @owner) }
42
+ else
43
+ []
44
+ end
45
+
46
+ @reflection.collection_class.new(models, owner: @owner)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Sumaki
6
+ module Model
7
+ module Associations
8
+ class Collection # :nodoc:
9
+ include Enumerable
10
+ extend Forwardable
11
+
12
+ singleton_class.attr_accessor :reflection
13
+
14
+ def_delegators :@models, :each, :[]
15
+ def_delegators :@models, :inspect, :pretty_print
16
+ def_delegators 'self.class', :reflection
17
+
18
+ def self.build_subclass(reflection)
19
+ subclass = Class.new(self)
20
+ subclass.reflection = reflection
21
+ subclass
22
+ end
23
+
24
+ def initialize(models = [], owner:)
25
+ @models = models
26
+ @owner = owner
27
+ end
28
+
29
+ def build(attrs = {})
30
+ object = @owner.object_accessor.build_repeated_element(reflection.name)
31
+ model = reflection.model_class.new(object, parent: @owner)
32
+ model.assign(attrs)
33
+
34
+ self << model
35
+
36
+ model
37
+ end
38
+
39
+ def <<(...)
40
+ r = @models.<<(...)
41
+ apply
42
+ r
43
+ end
44
+
45
+ private
46
+
47
+ def apply
48
+ @owner.object_accessor.apply_repeated(reflection.name, @models)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'collection'
4
+
5
+ module Sumaki
6
+ module Model
7
+ module Associations
8
+ module Reflection
9
+ class Base # :nodoc:
10
+ def initialize(owner_class, name, class_name: nil)
11
+ @owner_class = owner_class
12
+ @name = name
13
+ @class_name = class_name
14
+ end
15
+
16
+ def name = @name.to_sym
17
+
18
+ def model_class
19
+ @model_class ||= begin
20
+ basename = @class_name&.to_s || classify(@name.to_s)
21
+ klass = if @owner_class.const_defined?(basename)
22
+ @owner_class.const_get(basename)
23
+ else
24
+ @owner_class.const_set(basename, Class.new { include Model })
25
+ end
26
+ klass.parent = @owner_class
27
+ klass
28
+ end
29
+ end
30
+
31
+ def association_for(model)
32
+ self.class.association_class.new(model, self)
33
+ end
34
+
35
+ private
36
+
37
+ def classify(str)
38
+ str.gsub(/([a-z\d]+)_?/) { |_| Regexp.last_match(1).capitalize }
39
+ end
40
+ end
41
+
42
+ class Singular < Base # :nodoc:
43
+ def self.association_class = Association::Singular
44
+ end
45
+
46
+ class Repeated < Base # :nodoc:
47
+ def self.association_class = Association::Repeated
48
+
49
+ def collection_class
50
+ @collection_class ||= begin
51
+ class_name = "#{model_class.name[/(\w+)$/]}Collection"
52
+
53
+ if @owner_class.const_defined?(class_name)
54
+ @owner_class.const_get(class_name)
55
+ else
56
+ @owner_class.const_set(class_name, Collection.build_subclass(self))
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'associations/reflection'
4
+ require_relative 'associations/association'
5
+
6
+ module Sumaki
7
+ module Model
8
+ # = Sumaki::Model::Associations
9
+ module Associations
10
+ def self.included(base)
11
+ base.extend ClassMethods
12
+ base.include InstanceMethods
13
+ end
14
+
15
+ module AccessorAdder
16
+ module Singular # :nodoc:
17
+ def add(model_class, methods_module, reflection)
18
+ add_getter(methods_module, reflection.name)
19
+ add_builder(methods_module, reflection.name)
20
+
21
+ model_class.reflections[reflection.name] = reflection
22
+ end
23
+
24
+ private
25
+
26
+ def add_getter(methods_module, name)
27
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
28
+ def #{name} # def author
29
+ association(:#{name}).model # association(:author).model
30
+ end # end
31
+ RUBY
32
+ end
33
+
34
+ def add_builder(methods_module, name)
35
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
36
+ def build_#{name}(attrs = {}) # def build_author(attrs = {})
37
+ association(:#{name}).build_model(attrs) # association(:author).build_model(attrs)
38
+ end # end
39
+ RUBY
40
+ end
41
+
42
+ module_function :add, :add_getter, :add_builder
43
+ end
44
+
45
+ module Repeated # :nodoc:
46
+ def add(model_class, methods_module, reflection)
47
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
48
+ def #{reflection.name} # def book
49
+ association(:#{reflection.name}).collection # association(:book).collection
50
+ end # end
51
+ RUBY
52
+
53
+ model_class.reflections[reflection.name] = reflection
54
+ end
55
+ module_function :add
56
+ end
57
+ end
58
+
59
+ module ClassMethods # :nodoc:
60
+ # Access to the sub object.
61
+ #
62
+ # class Book
63
+ # include Sumaki::Model
64
+ # singular :company
65
+ #
66
+ # class Company
67
+ # include Sumaki::Model
68
+ # end
69
+ # end
70
+ #
71
+ # data = {
72
+ # title: 'The Ronaldo Chronicles',
73
+ # company: {
74
+ # name: 'Autumn Books',
75
+ # }
76
+ # }
77
+ # book = Book.new(data)
78
+ # book.company.class #=> Book::Company
79
+ #
80
+ # Sub object can also be created.
81
+ #
82
+ # book = Book.new({})
83
+ # book.build_company(name: 'Autumn Books')
84
+ # book.company #=> #<Book::Company:0x000073a618e31e80 name: "Autumn Books">
85
+ #
86
+ # == Options
87
+ #
88
+ # [:class_name]
89
+ # Specify the name of the class to wrap. Use this if the name of the class
90
+ # to wrap is not inferred from the nested field names.
91
+ def singular(name, class_name: nil)
92
+ reflection = Reflection::Singular.new(self, name, class_name: class_name)
93
+ AccessorAdder::Singular.add(self, association_methods_module, reflection)
94
+ end
95
+
96
+ # Access to the repeated sub objects
97
+ #
98
+ # class Company
99
+ # include Sumaki::Model
100
+ # repeated :member
101
+ #
102
+ # class Member
103
+ # include Sumaki::Model
104
+ # end
105
+ # end
106
+ #
107
+ # data = {
108
+ # name: 'The Ronaldo Vampire Hunter Agency',
109
+ # member: [
110
+ # { name: 'Ronaldo' },
111
+ # { name: 'Draluc' },
112
+ # { name: 'John' }
113
+ # ]
114
+ # }
115
+ # company = Company.new(data)
116
+ # company.member[2].class #=> Company::Member
117
+ #
118
+ # Sub object can also be created.
119
+ #
120
+ # company = Company.new({})
121
+ # company.member.build(name: 'John')
122
+ # company.member[0].name #=> 'John'
123
+ #
124
+ # == Options
125
+ #
126
+ # [:class_name]
127
+ # Specify the name of the class to wrap. Use this if the name of the class
128
+ # to wrap is not inferred from the nested field names.
129
+ def repeated(name, class_name: nil)
130
+ reflection = Reflection::Repeated.new(self, name, class_name: class_name)
131
+ AccessorAdder::Repeated.add(self, association_methods_module, reflection)
132
+ end
133
+
134
+ def reflections
135
+ @reflections ||= {}
136
+ end
137
+
138
+ private
139
+
140
+ def association_methods_module
141
+ @association_methods_module ||= begin
142
+ mod = Module.new
143
+ include mod
144
+ mod
145
+ end
146
+ end
147
+ end
148
+
149
+ module InstanceMethods # :nodoc:
150
+ private
151
+
152
+ def association(name)
153
+ @associations ||= {}
154
+ @associations[name.to_sym] ||= self.class.reflections[name.to_sym].association_for(self)
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -6,6 +6,31 @@ module Sumaki
6
6
  module Attribute
7
7
  def self.included(base)
8
8
  base.extend ClassMethods
9
+ base.include InstanceMethods
10
+ end
11
+
12
+ module AccessorAdder # :nodoc:
13
+ def add(methods_module, field_name)
14
+ add_getter(methods_module, field_name)
15
+ add_setter(methods_module, field_name)
16
+ end
17
+
18
+ def add_getter(methods_module, field_name)
19
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
20
+ def #{field_name} # def title
21
+ get(:'#{field_name}') # get(:'title')
22
+ end # end
23
+ RUBY
24
+ end
25
+
26
+ def add_setter(methods_module, field_name)
27
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
28
+ def #{field_name}=(value) # def title=(value)
29
+ set(:'#{field_name}', value) # set(:'title', value)
30
+ end # end
31
+ RUBY
32
+ end
33
+ module_function :add, :add_getter, :add_setter
9
34
  end
10
35
 
11
36
  module ClassMethods # :nodoc:
@@ -20,12 +45,19 @@ module Sumaki
20
45
  # anime = Anime.new({ title: 'The Vampire Dies in No Time', url: 'https://sugushinu-anime.jp/' })
21
46
  # anime.title #=> 'The Vampire Dies in No Time'
22
47
  # anime.url #=> 'https://sugushinu-anime.jp/'
48
+ #
49
+ # The Field value cam be set.
50
+ #
51
+ # anime = Anime.new({})
52
+ # anime.title = 'The Vampire Dies in No Time'
53
+ # anime.title #=> 'The Vampire Dies in No Time'
23
54
  def field(name)
24
- attribute_methods_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
25
- def #{name} # def title
26
- get(:'#{name}') # get(:'title')
27
- end # end
28
- RUBY
55
+ field_names << name.to_sym
56
+ AccessorAdder.add(attribute_methods_module, name)
57
+ end
58
+
59
+ def field_names
60
+ @field_names ||= []
29
61
  end
30
62
 
31
63
  private
@@ -38,6 +70,12 @@ module Sumaki
38
70
  end
39
71
  end
40
72
  end
73
+
74
+ module InstanceMethods # :nodoc:
75
+ def fields
76
+ self.class.field_names.map.with_object({}) { |e, r| r[e] = public_send(e) }
77
+ end
78
+ end
41
79
  end
42
80
  end
43
81
  end
@@ -15,7 +15,11 @@ module Sumaki
15
15
  def get(model, name)
16
16
  model.get(name)
17
17
  end
18
- module_function :get
18
+
19
+ def set(model, name, value)
20
+ model.set(name, value)
21
+ end
22
+ module_function :get, :set
19
23
  end
20
24
 
21
25
  module ClassMethods # :nodoc:
@@ -36,6 +40,12 @@ module Sumaki
36
40
  # character.type.name #=> :familier
37
41
  # character.type.familier? #=> true
38
42
  # character.type.vampire? #=> false
43
+ #
44
+ # Enum can also be set.
45
+ #
46
+ # character = Character.new({})
47
+ # character.type = 1
48
+ # character.type.name #=> :vampire
39
49
  def enum(name, values)
40
50
  super(name, values, adapter: EnumAttrAccessor)
41
51
  end
@@ -49,11 +59,6 @@ module Sumaki
49
59
  mod
50
60
  end
51
61
  end
52
-
53
- # TODO: remove this
54
- def classify(key)
55
- key.gsub(/([a-z\d]+)_?/) { |_| Regexp.last_match(1).capitalize }
56
- end
57
62
  end
58
63
  end
59
64
  end
data/lib/sumaki/model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'model/attribute'
4
- require_relative 'model/association'
4
+ require_relative 'model/associations'
5
5
  require_relative 'model/enum'
6
6
 
7
7
  module Sumaki
@@ -164,7 +164,7 @@ module Sumaki
164
164
  base.include InstanceMethods
165
165
 
166
166
  base.include Attribute
167
- base.include Association
167
+ base.include Associations
168
168
  base.include Enum
169
169
  end
170
170
 
@@ -177,6 +177,33 @@ module Sumaki
177
177
  end
178
178
  end
179
179
 
180
+ class ObjectAccessor # :nodoc:
181
+ def initialize(object, adapter)
182
+ @object = object
183
+ @adapter = adapter
184
+ end
185
+
186
+ def get(name)
187
+ @adapter.get(@object, name)
188
+ end
189
+
190
+ def set(name, value)
191
+ @adapter.set(@object, name, value)
192
+ end
193
+
194
+ def build_singular(name)
195
+ @adapter.build_singular(@object, name)
196
+ end
197
+
198
+ def build_repeated_element(name)
199
+ @adapter.build_repeated_element(@object, name)
200
+ end
201
+
202
+ def apply_repeated(name, models)
203
+ @adapter.apply_repeated(@object, name, models.map(&:object))
204
+ end
205
+ end
206
+
180
207
  module InstanceMethods # :nodoc:
181
208
  attr_reader :object, :parent
182
209
 
@@ -184,6 +211,43 @@ module Sumaki
184
211
  @object = object
185
212
  @parent = parent
186
213
  end
214
+
215
+ def object_accessor
216
+ @object_accessor ||= ObjectAccessor.new(object, self.class.adapter)
217
+ end
218
+
219
+ def get(name)
220
+ object_accessor.get(name)
221
+ end
222
+
223
+ def set(name, value)
224
+ object_accessor.set(name, value)
225
+ end
226
+
227
+ def assign(attrs)
228
+ attrs.each do |attr, value|
229
+ public_send(:"#{attr}=", value)
230
+ end
231
+ end
232
+
233
+ def inspect
234
+ inspection = fields
235
+ .map { |name, value| "#{name}: #{value.inspect}" }
236
+ .join(', ')
237
+ "#<#{self.class.name} #{inspection}>"
238
+ end
239
+
240
+ def pretty_print(pp)
241
+ pp.object_address_group(self) do
242
+ pp.seplist(fields) do |field, value|
243
+ pp.breakable
244
+ pp.group(1) do
245
+ pp.text "#{field}: "
246
+ pp.pp value
247
+ end
248
+ end
249
+ end
250
+ end
187
251
  end
188
252
  end
189
253
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sumaki
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sumaki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Loose Coupling
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-23 00:00:00.000000000 Z
11
+ date: 2024-05-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minenum
@@ -48,7 +48,10 @@ files:
48
48
  - lib/sumaki/adapter/hash.rb
49
49
  - lib/sumaki/config.rb
50
50
  - lib/sumaki/model.rb
51
- - lib/sumaki/model/association.rb
51
+ - lib/sumaki/model/associations.rb
52
+ - lib/sumaki/model/associations/association.rb
53
+ - lib/sumaki/model/associations/collection.rb
54
+ - lib/sumaki/model/associations/reflection.rb
52
55
  - lib/sumaki/model/attribute.rb
53
56
  - lib/sumaki/model/enum.rb
54
57
  - lib/sumaki/version.rb
@@ -1,116 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sumaki
4
- module Model
5
- # = Sumaki::Model::Association
6
- module Association
7
- def self.included(base)
8
- base.extend ClassMethods
9
- base.include InstanceMethods
10
-
11
- base.instance_variable_set(:@classes, {})
12
- end
13
-
14
- module ClassMethods # :nodoc:
15
- # Access to the sub object.
16
- #
17
- # class Book
18
- # include Sumaki::Model
19
- # singular :company
20
- #
21
- # class Company
22
- # include Sumaki::Model
23
- # end
24
- # end
25
- #
26
- # data = {
27
- # title: 'The Ronaldo Chronicles',
28
- # company: {
29
- # name: 'Autumn Books',
30
- # }
31
- # }
32
- # book = Book.new(data)
33
- # book.company.class #=> Book::Company
34
- #
35
- # == Options
36
- #
37
- # [:class_name]
38
- # Specify the name of the class to wrap. Use this if the name of the class
39
- # to wrap is not inferred from the nested field names.
40
- def singular(name, class_name: nil)
41
- association_methods_module.define_method(name) do
42
- klass = self.class.class_for(name, class_name)
43
- klass.new(get(name), parent: self)
44
- end
45
- end
46
-
47
- # Access to the repeated sub objects
48
- #
49
- # class Company
50
- # include Sumaki::Model
51
- # repeated :member
52
- #
53
- # class Member
54
- # include Sumaki::Model
55
- # end
56
- # end
57
- #
58
- # data = {
59
- # name: 'The Ronaldo Vampire Hunter Agency',
60
- # member: [
61
- # { name: 'Ronaldo' },
62
- # { name: 'Draluc' },
63
- # { name: 'John' }
64
- # ]
65
- # }
66
- # company = Company.new(data)
67
- # company.member[2].class #=> Company::Member
68
- #
69
- # == Options
70
- #
71
- # [:class_name]
72
- # Specify the name of the class to wrap. Use this if the name of the class
73
- # to wrap is not inferred from the nested field names.
74
- def repeated(name, class_name: nil)
75
- association_methods_module.define_method(name) do
76
- klass = self.class.class_for(name, class_name)
77
- get(name).map { |object| klass.new(object, parent: self) }
78
- end
79
- end
80
-
81
- def class_for(name, class_name = nil)
82
- return @classes[name] if @classes.key?(name)
83
-
84
- basename = class_name || classify(name.to_s)
85
- klass = if const_defined?(basename)
86
- const_get(basename)
87
- else
88
- const_set(basename, Class.new { include Model })
89
- end
90
- klass.parent ||= self
91
- @classes[name] = klass
92
- end
93
-
94
- private
95
-
96
- def association_methods_module
97
- @association_methods_module ||= begin
98
- mod = Module.new
99
- include mod
100
- mod
101
- end
102
- end
103
-
104
- def classify(key)
105
- key.gsub(/([a-z\d]+)_?/) { |_| Regexp.last_match(1).capitalize }
106
- end
107
- end
108
-
109
- module InstanceMethods # :nodoc:
110
- def get(name)
111
- self.class.adapter.get(object, name)
112
- end
113
- end
114
- end
115
- end
116
- end