sumaki 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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