sumaki 0.1.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: 743c9cc65c7fb7b06a254d6f96ccacd405101fbd87b12b9ba9a0283341f0203b
4
- data.tar.gz: 922c9652b6a0bee5fcca21e261b653a8fe9362ad0ee174050f6a0e8fae9986d1
3
+ metadata.gz: 4cc5d96c3a979b46ace35c6bcae70136875b415af2dbe971f45731ff69aa61ff
4
+ data.tar.gz: dbae61d4d18b0c6e3034620ea3dbe77467b4133da5df1eb45b9d89c927d98504
5
5
  SHA512:
6
- metadata.gz: 3d368a98fe977ba95aad3b0d01233a46eb00391e0c2825faa6b4fe9991b6394808a90dbfe37af1648ddf3bed13b904fb3dee72f5ee83237c4c0d876a628df660
7
- data.tar.gz: 122196f48283f4e23f3a9ba52d84aff764487066016da59bb42e5fe5e405780bde902f41bf45bc25ead3b6fcc5347935b786d4723e17328f137052b0ff0836ed
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,70 +1,91 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sumaki (0.1.0)
4
+ sumaki (0.3.0)
5
+ minenum
5
6
 
6
7
  GEM
7
8
  remote: https://rubygems.org/
8
9
  specs:
9
10
  ast (2.4.2)
10
- debug (1.7.1)
11
- irb (>= 1.5.0)
12
- reline (>= 0.3.1)
13
- diff-lcs (1.5.0)
11
+ debug (1.9.2)
12
+ irb (~> 1.10)
13
+ reline (>= 0.3.8)
14
+ diff-lcs (1.5.1)
14
15
  docile (1.4.0)
15
- io-console (0.6.0)
16
- irb (1.6.2)
17
- reline (>= 0.3.0)
18
- json (2.6.3)
19
- parallel (1.22.1)
20
- parser (3.2.0.0)
16
+ io-console (0.7.2)
17
+ irb (1.13.1)
18
+ rdoc (>= 4.0.0)
19
+ reline (>= 0.4.2)
20
+ json (2.7.2)
21
+ language_server-protocol (3.17.0.3)
22
+ minenum (0.1.0)
23
+ parallel (1.24.0)
24
+ parser (3.3.1.0)
21
25
  ast (~> 2.4.1)
26
+ racc
27
+ psych (5.1.2)
28
+ stringio
29
+ racc (1.7.3)
22
30
  rainbow (3.1.1)
23
- rake (13.0.6)
24
- regexp_parser (2.6.2)
25
- reline (0.3.2)
31
+ rake (13.2.1)
32
+ rdoc (6.6.3.1)
33
+ psych (>= 4.0.0)
34
+ regexp_parser (2.9.2)
35
+ reline (0.5.7)
26
36
  io-console (~> 0.5)
27
- rexml (3.2.5)
28
- rspec (3.12.0)
29
- rspec-core (~> 3.12.0)
30
- rspec-expectations (~> 3.12.0)
31
- rspec-mocks (~> 3.12.0)
32
- rspec-core (3.12.0)
33
- rspec-support (~> 3.12.0)
34
- rspec-expectations (3.12.2)
37
+ rexml (3.2.8)
38
+ strscan (>= 3.0.9)
39
+ rspec (3.13.0)
40
+ rspec-core (~> 3.13.0)
41
+ rspec-expectations (~> 3.13.0)
42
+ rspec-mocks (~> 3.13.0)
43
+ rspec-core (3.13.0)
44
+ rspec-support (~> 3.13.0)
45
+ rspec-expectations (3.13.0)
35
46
  diff-lcs (>= 1.2.0, < 2.0)
36
- rspec-support (~> 3.12.0)
37
- rspec-mocks (3.12.3)
47
+ rspec-support (~> 3.13.0)
48
+ rspec-mocks (3.13.1)
38
49
  diff-lcs (>= 1.2.0, < 2.0)
39
- rspec-support (~> 3.12.0)
40
- rspec-support (3.12.0)
41
- rubocop (1.44.1)
50
+ rspec-support (~> 3.13.0)
51
+ rspec-support (3.13.1)
52
+ rubocop (1.63.5)
42
53
  json (~> 2.3)
54
+ language_server-protocol (>= 3.17.0)
43
55
  parallel (~> 1.10)
44
- parser (>= 3.2.0.0)
56
+ parser (>= 3.3.0.2)
45
57
  rainbow (>= 2.2.2, < 4.0)
46
58
  regexp_parser (>= 1.8, < 3.0)
47
59
  rexml (>= 3.2.5, < 4.0)
48
- rubocop-ast (>= 1.24.1, < 2.0)
60
+ rubocop-ast (>= 1.31.1, < 2.0)
49
61
  ruby-progressbar (~> 1.7)
50
62
  unicode-display_width (>= 2.4.0, < 3.0)
51
- rubocop-ast (1.24.1)
52
- parser (>= 3.1.1.0)
53
- rubocop-capybara (2.17.0)
63
+ rubocop-ast (1.31.3)
64
+ parser (>= 3.3.1.0)
65
+ rubocop-capybara (2.20.0)
54
66
  rubocop (~> 1.41)
55
- rubocop-rspec (2.18.1)
56
- rubocop (~> 1.33)
67
+ rubocop-factory_bot (2.25.1)
68
+ rubocop (~> 1.41)
69
+ rubocop-rspec (2.29.2)
70
+ rubocop (~> 1.40)
57
71
  rubocop-capybara (~> 2.17)
58
- ruby-progressbar (1.11.0)
72
+ rubocop-factory_bot (~> 2.22)
73
+ rubocop-rspec_rails (~> 2.28)
74
+ rubocop-rspec_rails (2.28.3)
75
+ rubocop (~> 1.40)
76
+ ruby-progressbar (1.13.0)
59
77
  simplecov (0.22.0)
60
78
  docile (~> 1.1)
61
79
  simplecov-html (~> 0.11)
62
80
  simplecov_json_formatter (~> 0.1)
63
81
  simplecov-html (0.12.3)
64
82
  simplecov_json_formatter (0.1.4)
65
- unicode-display_width (2.4.2)
83
+ stringio (3.1.0)
84
+ strscan (3.1.0)
85
+ unicode-display_width (2.5.0)
66
86
 
67
87
  PLATFORMS
88
+ ruby
68
89
  x86_64-linux
69
90
 
70
91
  DEPENDENCIES
@@ -77,4 +98,4 @@ DEPENDENCIES
77
98
  sumaki!
78
99
 
79
100
  BUNDLED WITH
80
- 2.4.5
101
+ 2.5.9
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ---
4
4
 
5
+ [![Gem Version](https://badge.fury.io/rb/sumaki.svg)](https://badge.fury.io/rb/sumaki)
6
+ ![example workflow](https://github.com/nowlinuxing/sumaki/actions/workflows/main.yml/badge.svg?branch=main)
5
7
  [![Maintainability](https://api.codeclimate.com/v1/badges/dd92d4092d6858cbcfb2/maintainability)](https://codeclimate.com/github/nowlinuxing/sumaki/maintainability)
6
8
  [![Test Coverage](https://api.codeclimate.com/v1/badges/dd92d4092d6858cbcfb2/test_coverage)](https://codeclimate.com/github/nowlinuxing/sumaki/test_coverage)
7
9
 
@@ -91,12 +93,22 @@ class Anime
91
93
  field :title
92
94
  field :url
93
95
  end
96
+ ```
94
97
 
98
+ ```ruby
99
+ # Read the field values
95
100
  anime = Anime.new({ title: 'The Vampire Dies in No Time', url: 'https://sugushinu-anime.jp/' })
96
101
  anime.title #=> 'The Vampire Dies in No Time'
97
102
  anime.url #=> 'https://sugushinu-anime.jp/'
98
103
  ```
99
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
+
100
112
  If the data contains attributes not declared in the field, it raises no error and is simply ignored.
101
113
 
102
114
  ### Access to the sub object
@@ -112,23 +124,31 @@ class Book
112
124
  class Company
113
125
  include Sumaki::Model
114
126
  field :name
115
- field :prefecture
116
127
  end
117
128
  end
129
+ ```
118
130
 
131
+ ```ruby
119
132
  data = {
120
133
  title: 'The Ronaldo Chronicles',
121
134
  company: {
122
135
  name: 'Autumn Books',
123
- prefecture: 'Tokyo'
124
136
  }
125
137
  }
126
138
 
127
- comic = Book.new(data)
128
- 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">
129
149
  ```
130
150
 
131
- 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.
132
152
 
133
153
  This can be changed by specifying the class to wrap.
134
154
 
@@ -170,7 +190,9 @@ class Company
170
190
  field :name
171
191
  end
172
192
  end
193
+ ```
173
194
 
195
+ ```ruby
174
196
  data = {
175
197
  name: 'The Ronaldo Vampire Hunter Agency',
176
198
  member: [
@@ -180,11 +202,19 @@ data = {
180
202
  ]
181
203
  }
182
204
 
205
+ # Read from the sub object
183
206
  company = Company.new(data)
184
207
  company.member.size #=> 3
185
208
  company.member[2].name #=> 'John'
186
209
  ```
187
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
+
188
218
  The `class_name` option can also be used to specify the class to wrap.
189
219
 
190
220
  ### Access to the parent object
@@ -223,16 +253,27 @@ By declaring `enum`, You can map a field to the specified value.
223
253
  class Character
224
254
  include Sumaki::Model
225
255
  field :name
226
- enum :type, vampire: 1, vampire_hunter: 2, familier: 3, editor: 4
256
+ enum :type, { vampire: 1, vampire_hunter: 2, familier: 3, editor: 4 }
227
257
  end
258
+ ```
228
259
 
260
+ ```ruby
229
261
  data = {
230
262
  name: 'John',
231
263
  type: 3
232
264
  }
233
265
 
266
+ # Read the enum
234
267
  character = Character.new(data)
235
- 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
236
277
  ```
237
278
 
238
279
 
@@ -244,7 +285,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
244
285
 
245
286
  ## Contributing
246
287
 
247
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/sumaki.
288
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nowlinuxing/sumaki.
248
289
 
249
290
  ## License
250
291
 
@@ -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
@@ -1,33 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'minenum'
4
+
3
5
  module Sumaki
4
6
  module Model
5
7
  # = Sumaki::Model::Enum
6
8
  module Enum
7
9
  def self.included(base)
10
+ base.include Minenum::Model
8
11
  base.extend ClassMethods
9
12
  end
10
13
 
11
- class EnumValue < Hash # :nodoc:
12
- code = <<-RUBY
13
- case value
14
- when Symbol then super(value) || super(value.to_s)
15
- when String then super(value) || super(value.to_sym)
16
- else super(value)
17
- end
18
- RUBY
19
-
20
- class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
21
- def key?(value); #{code}; end
22
- RUBY
23
-
24
- class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
25
- def key(value); #{code}; end
26
- RUBY
14
+ module EnumAttrAccessor # :nodoc:
15
+ def get(model, name)
16
+ model.get(name)
17
+ end
27
18
 
28
- class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
29
- def value?(value); #{code}; end
30
- RUBY
19
+ def set(model, name, value)
20
+ model.set(name, value)
21
+ end
22
+ module_function :get, :set
31
23
  end
32
24
 
33
25
  module ClassMethods # :nodoc:
@@ -36,7 +28,7 @@ module Sumaki
36
28
  # class Character
37
29
  # include Sumaki::Model
38
30
  # field :name
39
- # enum :type, vampire: 1, vampire_hunter: 2, familier: 3, editor: 4
31
+ # enum :type, { vampire: 1, vampire_hunter: 2, familier: 3, editor: 4 }
40
32
  # end
41
33
  #
42
34
  # data = {
@@ -45,19 +37,17 @@ module Sumaki
45
37
  # }
46
38
  #
47
39
  # character = Character.new(data)
48
- # character.type #=> :familier
49
- def enum(name, **values)
50
- values = EnumValue[values]
51
-
52
- enum_methods_module.define_method(name) do
53
- value = get(name)
54
-
55
- if values.key?(value)
56
- value
57
- elsif values.value?(value)
58
- values.key(value)
59
- end
60
- end
40
+ # character.type.name #=> :familier
41
+ # character.type.familier? #=> true
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
49
+ def enum(name, values)
50
+ super(name, values, adapter: EnumAttrAccessor)
61
51
  end
62
52
 
63
53
  private
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.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sumaki
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.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: 2023-02-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minenum
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: |
14
28
  Sumaki is a wrapper for structured data like JSON.
15
29
  Since Sumaki wraps the target data as it is, rather than parsing it using a schema, the original data can be referenced at any time.
@@ -34,12 +48,14 @@ files:
34
48
  - lib/sumaki/adapter/hash.rb
35
49
  - lib/sumaki/config.rb
36
50
  - lib/sumaki/model.rb
37
- - 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
38
55
  - lib/sumaki/model/attribute.rb
39
56
  - lib/sumaki/model/enum.rb
40
57
  - lib/sumaki/version.rb
41
58
  - sig/sumaki.rbs
42
- - sumaki.gemspec
43
59
  homepage: https://github.com/nowlinuxing/sumaki/
44
60
  licenses:
45
61
  - MIT
@@ -57,14 +73,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
73
  requirements:
58
74
  - - ">="
59
75
  - !ruby/object:Gem::Version
60
- version: 2.6.0
76
+ version: 3.0.0
61
77
  required_rubygems_version: !ruby/object:Gem::Requirement
62
78
  requirements:
63
79
  - - ">="
64
80
  - !ruby/object:Gem::Version
65
81
  version: '0'
66
82
  requirements: []
67
- rubygems_version: 3.4.1
83
+ rubygems_version: 3.5.9
68
84
  signing_key:
69
85
  specification_version: 4
70
86
  summary: Sumaki is a wrapper for structured data like JSON.
@@ -1,118 +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
- private
111
-
112
- def get(name)
113
- self.class.adapter.get(object, name)
114
- end
115
- end
116
- end
117
- end
118
- end
data/sumaki.gemspec DELETED
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'lib/sumaki/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'sumaki'
7
- spec.version = Sumaki::VERSION
8
- spec.authors = ['Loose Coupling']
9
- spec.email = ['loosecpl@gmail.com']
10
-
11
- spec.summary = 'Sumaki is a wrapper for structured data like JSON.'
12
- spec.description = <<~DESC
13
- Sumaki is a wrapper for structured data like JSON.
14
- Since Sumaki wraps the target data as it is, rather than parsing it using a schema, the original data can be referenced at any time.
15
- This makes it easy to add or modify definitions as needed while checking the target data.
16
- This feature may be useful when there is no document defining the structure of the data, or when the specification is complex and difficult to grasp, and the definition is written little by little starting from the obvious places.
17
- DESC
18
- spec.homepage = 'https://github.com/nowlinuxing/sumaki/'
19
- spec.license = 'MIT'
20
- spec.required_ruby_version = '>= 2.6.0'
21
-
22
- spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
23
-
24
- spec.metadata['homepage_uri'] = spec.homepage
25
- spec.metadata['source_code_uri'] = spec.homepage
26
- spec.metadata['changelog_uri'] = spec.homepage
27
- spec.metadata['rubygems_mfa_required'] = 'true'
28
-
29
- # Specify which files should be added to the gem when it is released.
30
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
- spec.files = Dir.chdir(__dir__) do
32
- `git ls-files -z`.split("\x0").reject do |f|
33
- (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
34
- end
35
- end
36
- spec.bindir = 'exe'
37
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
38
- spec.require_paths = ['lib']
39
-
40
- # Uncomment to register a new dependency of your gem
41
- # spec.add_dependency "example-gem", "~> 1.0"
42
-
43
- # For more information and examples about making a new gem, check out our
44
- # guide at: https://bundler.io/guides/creating_gem.html
45
- end