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 +4 -4
- data/.rubocop.yml +2 -2
- data/Gemfile.lock +58 -37
- data/README.md +49 -8
- data/lib/sumaki/adapter/hash.rb +16 -0
- data/lib/sumaki/model/associations/association.rb +53 -0
- data/lib/sumaki/model/associations/collection.rb +53 -0
- data/lib/sumaki/model/associations/reflection.rb +64 -0
- data/lib/sumaki/model/associations.rb +159 -0
- data/lib/sumaki/model/attribute.rb +43 -5
- data/lib/sumaki/model/enum.rb +23 -33
- data/lib/sumaki/model.rb +66 -2
- data/lib/sumaki/version.rb +1 -1
- metadata +23 -7
- data/lib/sumaki/model/association.rb +0 -118
- data/sumaki.gemspec +0 -45
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4cc5d96c3a979b46ace35c6bcae70136875b415af2dbe971f45731ff69aa61ff
|
4
|
+
data.tar.gz: dbae61d4d18b0c6e3034620ea3dbe77467b4133da5df1eb45b9d89c927d98504
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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/
|
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/
|
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.
|
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.
|
11
|
-
irb (
|
12
|
-
reline (>= 0.3.
|
13
|
-
diff-lcs (1.5.
|
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.
|
16
|
-
irb (1.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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.
|
24
|
-
|
25
|
-
|
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.
|
28
|
-
|
29
|
-
|
30
|
-
rspec-
|
31
|
-
rspec-
|
32
|
-
|
33
|
-
|
34
|
-
|
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.
|
37
|
-
rspec-mocks (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.
|
40
|
-
rspec-support (3.
|
41
|
-
rubocop (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.
|
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.
|
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.
|
52
|
-
parser (>= 3.
|
53
|
-
rubocop-capybara (2.
|
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-
|
56
|
-
rubocop (~> 1.
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
128
|
-
|
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
|
-
|
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/
|
288
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/nowlinuxing/sumaki.
|
248
289
|
|
249
290
|
## License
|
250
291
|
|
data/lib/sumaki/adapter/hash.rb
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
data/lib/sumaki/model/enum.rb
CHANGED
@@ -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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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/
|
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
|
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
|
data/lib/sumaki/version.rb
CHANGED
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.
|
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:
|
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/
|
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:
|
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.
|
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
|