sumaki 0.3.0 → 0.5.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: 4cc5d96c3a979b46ace35c6bcae70136875b415af2dbe971f45731ff69aa61ff
4
- data.tar.gz: dbae61d4d18b0c6e3034620ea3dbe77467b4133da5df1eb45b9d89c927d98504
3
+ metadata.gz: dc2e15ea308cacd7c524ba33698734f08020c2c9d6fb9b39accb4c79bc108a67
4
+ data.tar.gz: 1090e186dd028117e5f6cb7df816004dcc9508313757e6af8b802d6eeef74a2d
5
5
  SHA512:
6
- metadata.gz: 9a7ecf9d761bf8b6f8ff2b5236090f83105795dcb78e45d27c109a7377ead5ed323a38e51a6b3be5996bedd95917e7676e13894dd16517bac180de6ea589be6f
7
- data.tar.gz: 1c3624fdf113f3d260e58e00f68c96f51abcdac1fe1f55e7f30ec93a3b62159b3a051618a3bfe4b9c791f66499d6160429083c73eb90d2661d97e027c74c0f16
6
+ metadata.gz: 720e0a53bf096093e5e25c5b2dbd2d562666e696d274147d1b3634e49a350309be22b1ada5e069c43f23025e4fdeafbde25c7a9f0b8a4f2d05ffc47fcc7a25d6
7
+ data.tar.gz: e00e0ef3b7f0af18c438e680e4bfdc40aa83b3409853c5cc587c529b43c9c2f73cd9ad72d0e383d738fbe8879247a8f25beb829001daa3d397e836f1486850a6
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sumaki (0.3.0)
5
- minenum
4
+ sumaki (0.5.0)
5
+ minenum (>= 0.2.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
@@ -19,7 +19,7 @@ GEM
19
19
  reline (>= 0.4.2)
20
20
  json (2.7.2)
21
21
  language_server-protocol (3.17.0.3)
22
- minenum (0.1.0)
22
+ minenum (0.2.0)
23
23
  parallel (1.24.0)
24
24
  parser (3.3.1.0)
25
25
  ast (~> 2.4.1)
data/README.md CHANGED
@@ -111,6 +111,30 @@ anime.title #=> 'The Vampire Dies in No Time'
111
111
 
112
112
  If the data contains attributes not declared in the field, it raises no error and is simply ignored.
113
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
+
114
138
  ### Access to the sub object
115
139
 
116
140
  By declaring `singular`, you can access the sub object.
@@ -274,6 +298,12 @@ character.type.familier? #=> true
274
298
  character = Character.new({})
275
299
  character.type = 1
276
300
  character.type.name #=> :vampire
301
+
302
+ character.type = :vampire_hunter
303
+ character.type.name #=> :vampire_hunter
304
+
305
+ character.type.familier!
306
+ character.type.name #=> :familier
277
307
  ```
278
308
 
279
309
 
@@ -18,8 +18,8 @@ module Sumaki
18
18
  def model_class
19
19
  @model_class ||= begin
20
20
  basename = @class_name&.to_s || classify(@name.to_s)
21
- klass = if @owner_class.const_defined?(basename)
22
- @owner_class.const_get(basename)
21
+ klass = if @owner_class.const_defined?(basename, false)
22
+ @owner_class.const_get(basename, false)
23
23
  else
24
24
  @owner_class.const_set(basename, Class.new { include Model })
25
25
  end
@@ -11,15 +11,14 @@ module Sumaki
11
11
  base.extend ClassMethods
12
12
  end
13
13
 
14
- module EnumAttrAccessor # :nodoc:
15
- def get(model, name)
16
- model.get(name)
14
+ class EnumAttrAccessor < Minenum::Enum::Adapter::Base # :nodoc:
15
+ def get
16
+ @enum_object.get(@name)
17
17
  end
18
18
 
19
- def set(model, name, value)
20
- model.set(name, value)
19
+ def set(value)
20
+ @enum_object.set(@name, value)
21
21
  end
22
- module_function :get, :set
23
22
  end
24
23
 
25
24
  module ClassMethods # :nodoc:
@@ -47,7 +46,7 @@ module Sumaki
47
46
  # character.type = 1
48
47
  # character.type.name #=> :vampire
49
48
  def enum(name, values)
50
- super(name, values, adapter: EnumAttrAccessor)
49
+ super(name, values, adapter_builder: EnumAttrAccessor)
51
50
  end
52
51
 
53
52
  private
@@ -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
@@ -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,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'model/attribute'
3
+ require_relative 'model/fields'
4
4
  require_relative 'model/associations'
5
5
  require_relative 'model/enum'
6
6
 
@@ -163,7 +163,7 @@ module Sumaki
163
163
  base.extend ClassMethods
164
164
  base.include InstanceMethods
165
165
 
166
- base.include Attribute
166
+ base.include Fields
167
167
  base.include Associations
168
168
  base.include Enum
169
169
  end
@@ -237,12 +237,14 @@ module Sumaki
237
237
  "#<#{self.class.name} #{inspection}>"
238
238
  end
239
239
 
240
- def pretty_print(pp)
240
+ def pretty_print(pp) # rubocop:disable Metrics/MethodLength
241
241
  pp.object_address_group(self) do
242
- pp.seplist(fields) do |field, value|
242
+ pp.seplist(fields, -> { pp.text ',' }) do |field, value|
243
243
  pp.breakable
244
244
  pp.group(1) do
245
- pp.text "#{field}: "
245
+ pp.text field.to_s
246
+ pp.text ':'
247
+ pp.breakable
246
248
  pp.pp value
247
249
  end
248
250
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sumaki
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.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.3.0
4
+ version: 0.5.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-05-17 00:00:00.000000000 Z
11
+ date: 2024-06-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minenum
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.2.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.2.0
27
27
  description: |
28
28
  Sumaki is a wrapper for structured data like JSON.
29
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.
@@ -52,8 +52,17 @@ files:
52
52
  - lib/sumaki/model/associations/association.rb
53
53
  - lib/sumaki/model/associations/collection.rb
54
54
  - lib/sumaki/model/associations/reflection.rb
55
- - lib/sumaki/model/attribute.rb
56
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
57
66
  - lib/sumaki/version.rb
58
67
  - sig/sumaki.rbs
59
68
  homepage: https://github.com/nowlinuxing/sumaki/
@@ -1,81 +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
- 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
34
- end
35
-
36
- module ClassMethods # :nodoc:
37
- # Access to the field.
38
- #
39
- # class Anime
40
- # include Sumaki::Model
41
- # field :title
42
- # field :url
43
- # end
44
- #
45
- # anime = Anime.new({ title: 'The Vampire Dies in No Time', url: 'https://sugushinu-anime.jp/' })
46
- # anime.title #=> 'The Vampire Dies in No Time'
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'
54
- def field(name)
55
- field_names << name.to_sym
56
- AccessorAdder.add(attribute_methods_module, name)
57
- end
58
-
59
- def field_names
60
- @field_names ||= []
61
- end
62
-
63
- private
64
-
65
- def attribute_methods_module
66
- @attribute_methods_module ||= begin
67
- mod = Module.new
68
- include mod
69
- mod
70
- end
71
- end
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
79
- end
80
- end
81
- end