sumaki 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'value'
4
+ require 'date'
5
+
6
+ module Sumaki
7
+ module Model
8
+ module Fields
9
+ module Type
10
+ class DateError < Error; end
11
+
12
+ class Date < Value # :nodoc:
13
+ def self.serialize(value)
14
+ value.nil? ? nil : cast(value)
15
+ rescue ::Date::Error
16
+ raise DateError
17
+ end
18
+
19
+ def self.deserialize(value)
20
+ value.nil? ? nil : cast(value)
21
+ rescue ::Date::Error
22
+ nil
23
+ end
24
+
25
+ def self.cast(value)
26
+ return value.to_date if value.respond_to?(:to_date)
27
+
28
+ ::Date.parse(value.to_s)
29
+ end
30
+ private_class_method :cast
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'value'
4
+ require 'date'
5
+
6
+ module Sumaki
7
+ module Model
8
+ module Fields
9
+ module Type
10
+ class DateError < Error; end
11
+
12
+ class DateTime < Value # :nodoc:
13
+ def self.serialize(value)
14
+ value.nil? ? nil : cast(value)
15
+ rescue ::Date::Error
16
+ raise DateError
17
+ end
18
+
19
+ def self.deserialize(value)
20
+ value.nil? ? nil : cast(value)
21
+ rescue ::Date::Error
22
+ nil
23
+ end
24
+
25
+ def self.cast(value)
26
+ return value.to_datetime if value.respond_to?(:to_datetime)
27
+
28
+ ::DateTime.parse(value.to_s)
29
+ end
30
+ private_class_method :cast
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
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 Float < Value # :nodoc:
10
+ def self.serialize(value)
11
+ try_casting do
12
+ value.nil? ? nil : Float(value)
13
+ end
14
+ end
15
+
16
+ def self.deserialize(value)
17
+ value.nil? ? nil : Float(value, exception: false)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
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 Integer < Value # :nodoc:
10
+ def self.serialize(value)
11
+ try_casting do
12
+ value.nil? ? nil : Integer(value)
13
+ end
14
+ end
15
+
16
+ def self.deserialize(value)
17
+ value.nil? ? nil : Integer(value, exception: false)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
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 String < Value # :nodoc:
10
+ def self.serialize(value)
11
+ try_casting do
12
+ value.nil? ? nil : String(value)
13
+ end
14
+ end
15
+
16
+ def self.deserialize(value)
17
+ value.nil? ? nil : String(value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sumaki
4
+ module Model
5
+ module Fields
6
+ module Type
7
+ class Error < StandardError; end
8
+ class ArgumentError < Error; end
9
+ class TypeError < Error; end
10
+
11
+ class Value # :nodoc:
12
+ def self.serialize(value)
13
+ value
14
+ end
15
+
16
+ def self.deserialize(value)
17
+ value
18
+ end
19
+
20
+ def self.try_casting
21
+ yield
22
+ rescue ::ArgumentError
23
+ raise Type::ArgumentError
24
+ rescue ::TypeError
25
+ raise Type::TypeError
26
+ end
27
+ private_class_method :try_casting
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type/value'
4
+ require_relative 'type/integer'
5
+ require_relative 'type/float'
6
+ require_relative 'type/string'
7
+ require_relative 'type/boolean'
8
+ require_relative 'type/date'
9
+ require_relative 'type/date_time'
10
+
11
+ module Sumaki
12
+ module Model
13
+ module Fields
14
+ module Type # :nodoc:
15
+ class Types # :nodoc:
16
+ def initialize
17
+ @types = {}
18
+ end
19
+
20
+ def register(name, type_class)
21
+ @types[name] = type_class
22
+ end
23
+
24
+ def lookup(type_name)
25
+ @types.fetch(type_name)
26
+ end
27
+ end
28
+
29
+ @types = Types.new
30
+
31
+ def register(...)
32
+ @types.register(...)
33
+ end
34
+
35
+ def lookup(...)
36
+ @types.lookup(...)
37
+ end
38
+
39
+ module_function :register, :lookup
40
+
41
+ register(:int, Integer)
42
+ register(:float, Float)
43
+ register(:string, String)
44
+ register(:bool, Boolean)
45
+ register(:date, Date)
46
+ register(:datetime, DateTime)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fields/reflection'
4
+
5
+ module Sumaki
6
+ module Model
7
+ # = Sumaki::Model::Fields
8
+ module Fields
9
+ def self.included(base)
10
+ base.extend ClassMethods
11
+ base.include InstanceMethods
12
+ end
13
+
14
+ class FieldAccessor # :nodoc:
15
+ def initialize(model)
16
+ @model = model
17
+ end
18
+
19
+ def get(field_name)
20
+ reflection = @model.class.field_reflections[field_name]
21
+
22
+ value = @model.get(reflection.name)
23
+ reflection.type_class.deserialize(value)
24
+ end
25
+
26
+ def set(field_name, value)
27
+ reflection = @model.class.field_reflections[field_name]
28
+
29
+ serialized = reflection.type_class.serialize(value)
30
+ @model.set(reflection.name, serialized)
31
+ end
32
+ end
33
+
34
+ module AccessorAdder # :nodoc:
35
+ def add(model_class, methods_module, reflection)
36
+ add_getter(methods_module, reflection.name)
37
+ add_setter(methods_module, reflection.name)
38
+
39
+ model_class.field_reflections[reflection.name] = reflection
40
+ end
41
+
42
+ def add_getter(methods_module, field_name)
43
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
44
+ def #{field_name} # def title
45
+ field_accessor.get(:'#{field_name}') # field_accessor.get(:'title')
46
+ end # end
47
+ RUBY
48
+ end
49
+
50
+ def add_setter(methods_module, field_name)
51
+ methods_module.module_eval <<~RUBY, __FILE__, __LINE__ + 1
52
+ def #{field_name}=(value) # def title=(value)
53
+ field_accessor.set(:'#{field_name}', value) # field_accessor.set(:'title', value)
54
+ end # end
55
+ RUBY
56
+ end
57
+ module_function :add, :add_getter, :add_setter
58
+ end
59
+
60
+ module ClassMethods # :nodoc:
61
+ # Access to the field.
62
+ #
63
+ # class Anime
64
+ # include Sumaki::Model
65
+ # field :title
66
+ # field :url
67
+ # end
68
+ #
69
+ # anime = Anime.new({ title: 'The Vampire Dies in No Time', url: 'https://sugushinu-anime.jp/' })
70
+ # anime.title #=> 'The Vampire Dies in No Time'
71
+ # anime.url #=> 'https://sugushinu-anime.jp/'
72
+ #
73
+ # The Field value cam be set.
74
+ #
75
+ # anime = Anime.new({})
76
+ # anime.title = 'The Vampire Dies in No Time'
77
+ # anime.title #=> 'The Vampire Dies in No Time'
78
+ #
79
+ # == Type casting
80
+ #
81
+ # When a type is specified, it will be typecast.
82
+ #
83
+ # class Character
84
+ # include Sumaki::Model
85
+ #
86
+ # field :age, :int
87
+ # end
88
+ #
89
+ # character = Character.new({ age: '208' })
90
+ # character.age #=> 208
91
+ #
92
+ # Types are:
93
+ #
94
+ # * <tt>:int</tt>
95
+ # * <tt>:float</tt>
96
+ # * <tt>:string</tt>
97
+ # * <tt>:bool</tt>
98
+ # * <tt>:date</tt>
99
+ # * <tt>:datetime</tt>
100
+ def field(name, type = nil)
101
+ reflection = Reflection.new(name, type)
102
+ AccessorAdder.add(self, attribute_methods_module, reflection)
103
+ end
104
+
105
+ def field_names
106
+ field_reflections.keys
107
+ end
108
+
109
+ def field_reflections
110
+ @field_reflections ||= {}
111
+ end
112
+
113
+ private
114
+
115
+ def attribute_methods_module
116
+ @attribute_methods_module ||= begin
117
+ mod = Module.new
118
+ include mod
119
+ mod
120
+ end
121
+ end
122
+ end
123
+
124
+ module InstanceMethods # :nodoc:
125
+ def fields
126
+ self.class.field_names.map.with_object({}) { |e, r| r[e] = public_send(e) }
127
+ end
128
+
129
+ private
130
+
131
+ def field_accessor
132
+ @field_accessor ||= FieldAccessor.new(self)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
data/lib/sumaki/model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'model/attribute'
4
- require_relative 'model/association'
3
+ require_relative 'model/fields'
4
+ require_relative 'model/associations'
5
5
  require_relative 'model/enum'
6
6
 
7
7
  module Sumaki
@@ -163,8 +163,8 @@ module Sumaki
163
163
  base.extend ClassMethods
164
164
  base.include InstanceMethods
165
165
 
166
- base.include Attribute
167
- base.include Association
166
+ base.include Fields
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,45 @@ 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) # rubocop:disable Metrics/MethodLength
241
+ pp.object_address_group(self) do
242
+ pp.seplist(fields, -> { pp.text ',' }) do |field, value|
243
+ pp.breakable
244
+ pp.group(1) do
245
+ pp.text field.to_s
246
+ pp.text ':'
247
+ pp.breakable
248
+ pp.pp value
249
+ end
250
+ end
251
+ end
252
+ end
187
253
  end
188
254
  end
189
255
  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.4.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.4.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-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minenum
@@ -48,9 +48,21 @@ 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
52
- - lib/sumaki/model/attribute.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
53
55
  - lib/sumaki/model/enum.rb
56
+ - lib/sumaki/model/fields.rb
57
+ - lib/sumaki/model/fields/reflection.rb
58
+ - lib/sumaki/model/fields/type.rb
59
+ - lib/sumaki/model/fields/type/boolean.rb
60
+ - lib/sumaki/model/fields/type/date.rb
61
+ - lib/sumaki/model/fields/type/date_time.rb
62
+ - lib/sumaki/model/fields/type/float.rb
63
+ - lib/sumaki/model/fields/type/integer.rb
64
+ - lib/sumaki/model/fields/type/string.rb
65
+ - lib/sumaki/model/fields/type/value.rb
54
66
  - lib/sumaki/version.rb
55
67
  - sig/sumaki.rbs
56
68
  homepage: https://github.com/nowlinuxing/sumaki/
@@ -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
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Sumaki
4
- module Model
5
- # = Sumaki::Model::Attribute
6
- module Attribute
7
- def self.included(base)
8
- base.extend ClassMethods
9
- end
10
-
11
- module ClassMethods # :nodoc:
12
- # Access to the field.
13
- #
14
- # class Anime
15
- # include Sumaki::Model
16
- # field :title
17
- # field :url
18
- # end
19
- #
20
- # anime = Anime.new({ title: 'The Vampire Dies in No Time', url: 'https://sugushinu-anime.jp/' })
21
- # anime.title #=> 'The Vampire Dies in No Time'
22
- # anime.url #=> 'https://sugushinu-anime.jp/'
23
- 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
29
- end
30
-
31
- private
32
-
33
- def attribute_methods_module
34
- @attribute_methods_module ||= begin
35
- mod = Module.new
36
- include mod
37
- mod
38
- end
39
- end
40
- end
41
- end
42
- end
43
- end