sumaki 0.2.0 → 0.4.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: 23a3806fcf2a27686f82307a043ef96e36efc90fe73fa2f0da6bba0b1c4506dc
4
+ data.tar.gz: 3602002eaab7328a7b331add996774487687aa310aa06bdea99e84746544fca1
5
5
  SHA512:
6
- metadata.gz: a913803fdde6db3a69bce4ad967048673c6d3b089d05265029d21e09074b817d45cb7b3c2f1570cc50e3ec034bdb85420eb7f38a0b6625803d2054bbb4fad733
7
- data.tar.gz: 355746b7f56600a79023a056aaaee86a599f9b5951b371b8b6a600086428cf392528f8447bc03f71f3173a91cd939b661d1134cb80cae4d99c993053e9973987
6
+ metadata.gz: 9c5104b11ef1d3029c8f3e0e5795279a9ae82061c61d2e6b15f4f9fb62629c4a99fb362ec4818959827da4a10034b4024442092c0f0aa0fb056cdae1c1333197
7
+ data.tar.gz: f3e582ba5ccafc8a44f1ef66a48df31c2ea1b5e95ef434b958b42647218f5a593dfb13c9aa0cb899e63356313e7a42118ced4445a2a426d70438db3f5da0b70a
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.4.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,14 +93,48 @@ 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
 
114
+ #### Type casting
115
+
116
+ When a type is specified, it will be typecast.
117
+
118
+ ```ruby
119
+ class Character
120
+ include Sumaki::Model
121
+
122
+ field :age, :int
123
+ end
124
+
125
+ character = Character.new({ age: '208' })
126
+ character.age #=> 208
127
+ ```
128
+
129
+ Types are:
130
+
131
+ * `:int`
132
+ * `:float`
133
+ * `:string`
134
+ * `:bool`
135
+ * `:date`
136
+ * `:datetime`
137
+
104
138
  ### Access to the sub object
105
139
 
106
140
  By declaring `singular`, you can access the sub object.
@@ -114,23 +148,31 @@ class Book
114
148
  class Company
115
149
  include Sumaki::Model
116
150
  field :name
117
- field :prefecture
118
151
  end
119
152
  end
153
+ ```
120
154
 
155
+ ```ruby
121
156
  data = {
122
157
  title: 'The Ronaldo Chronicles',
123
158
  company: {
124
159
  name: 'Autumn Books',
125
- prefecture: 'Tokyo'
126
160
  }
127
161
  }
128
162
 
129
- comic = Book.new(data)
130
- comic.company.name #=> 'Autumn Books'
163
+ # Read from the sub object
164
+ book = Book.new(data)
165
+ book.company.name #=> 'Autumn Books'
131
166
  ```
132
167
 
133
- sub object is wrapped with the class inferred from the field name under the original class.
168
+ ```ruby
169
+ # Build a sub object
170
+ book = Book.new({})
171
+ book.build_company(name: 'Autumn Books')
172
+ book.company #=> #<Book::Company:0x000073a618e31e80 name: "Autumn Books">
173
+ ```
174
+
175
+ Sub object is wrapped with the class inferred from the field name under the original class.
134
176
 
135
177
  This can be changed by specifying the class to wrap.
136
178
 
@@ -172,7 +214,9 @@ class Company
172
214
  field :name
173
215
  end
174
216
  end
217
+ ```
175
218
 
219
+ ```ruby
176
220
  data = {
177
221
  name: 'The Ronaldo Vampire Hunter Agency',
178
222
  member: [
@@ -182,11 +226,19 @@ data = {
182
226
  ]
183
227
  }
184
228
 
229
+ # Read from the sub object
185
230
  company = Company.new(data)
186
231
  company.member.size #=> 3
187
232
  company.member[2].name #=> 'John'
188
233
  ```
189
234
 
235
+ ```ruby
236
+ # Build a sub object
237
+ company = Company.new({})
238
+ company.member.build(name: 'John')
239
+ company.member[0].name #=> 'John'
240
+ ```
241
+
190
242
  The `class_name` option can also be used to specify the class to wrap.
191
243
 
192
244
  ### Access to the parent object
@@ -227,14 +279,25 @@ class Character
227
279
  field :name
228
280
  enum :type, { vampire: 1, vampire_hunter: 2, familier: 3, editor: 4 }
229
281
  end
282
+ ```
230
283
 
284
+ ```ruby
231
285
  data = {
232
286
  name: 'John',
233
287
  type: 3
234
288
  }
235
289
 
290
+ # Read the enum
236
291
  character = Character.new(data)
237
- character.type #=> :familier
292
+ character.type.name #=> :familier
293
+ character.type.familier? #=> true
294
+ ```
295
+
296
+ ```ruby
297
+ # Write the enum value
298
+ character = Character.new({})
299
+ character.type = 1
300
+ character.type.name #=> :vampire
238
301
  ```
239
302
 
240
303
 
@@ -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
@@ -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
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type'
4
+
5
+ module Sumaki
6
+ module Model
7
+ module Fields
8
+ class Reflection # :nodoc:
9
+ def initialize(name, type = nil)
10
+ @name = name
11
+ @type = type
12
+ end
13
+
14
+ def name
15
+ @name.to_sym
16
+ end
17
+
18
+ def type_class
19
+ @type_class ||= @type.nil? ? Type::Value : Type.lookup(@type)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'value'
4
+
5
+ module Sumaki
6
+ module Model
7
+ module Fields
8
+ module Type
9
+ class Boolean < Value # :nodoc:
10
+ def self.serialize(value)
11
+ return if value.nil?
12
+
13
+ case value
14
+ when true then true
15
+ when false then false
16
+ else
17
+ raise ArgumentError
18
+ end
19
+ end
20
+
21
+ def self.deserialize(value)
22
+ case value
23
+ when true then true
24
+ when false then false
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end