otoroshi 0.0.6 → 0.1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e31c6e00090297fb9709600d1580a24a4729ee1a9af094015ea7c197fde884d3
4
- data.tar.gz: 033526ab6473e809a19c812104cf05cadba29485d617161c1799f6664777f9fc
3
+ metadata.gz: 8928c55b726e2db1fb3bd19948c23be9c8fb0175650ae1f65cd0489bf7751c38
4
+ data.tar.gz: db9cc8034319672be477f1352508e6258d9ce105d6eedca6501630776e196407
5
5
  SHA512:
6
- metadata.gz: a5e9595b7c246dde6897fc4f314cdf0c9ef00576dd042cc9c4e34c60de249d4d346ebc2f661f1190708a26211cacbdd1a9f457a9e5802e57049b5f5d831dc9d1
7
- data.tar.gz: 42aace4283c39b3d956022e643635d73a9b3a5feb74070f0dd441d8ef165b5d9c76475a7bd274b94e4da8f85aa0ecb08868116842c6af98b1cdf4112a93bbe77
6
+ metadata.gz: 44856f1f0d75272e70a49d07db5d7ce5218849ca9a0a04164c875d9277f18ce2afddc2cb254e8d9079232a23f384ec58c9ea0602ea6e170504ad1bd59e280895
7
+ data.tar.gz: 7978129f51d5ef3b224efa547e0ede889ffd4010ca8f571dde79830190f21b7a69defd70f8f44059ef1daf8647310fd9583944e7bbbcca05753417b1fec9d72a
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Otoroshi entry-point
4
+ module Otoroshi
5
+ require_relative 'otoroshi/sanctuary'
6
+ require_relative 'otoroshi/exceptions'
7
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './initializer'
4
+
5
+ module Otoroshi
6
+ class Error < StandardError; end
7
+
8
+ # Manages errors raised when value is not an instance of the expected class
9
+ class TypeError < Error
10
+ # @param property [Symbol] name of the property
11
+ # @param type [Class] class to match
12
+ # @example
13
+ # ":number is not an instance of Integer"
14
+ def initialize(property, type)
15
+ super ":#{property} is not an instance of #{type}"
16
+ end
17
+ end
18
+
19
+ # Manages errors raised when value is not accepted (not in the "one_of")
20
+ class OneOfError < Error
21
+ # @param property [Symbol] name of the property
22
+ # @param values [Array] accepted values
23
+ # @example
24
+ # ":fruit is not in [:apple, :pear]"
25
+ def initialize(property, values)
26
+ # reintegrate the colon for symbols which is lost during interpolation
27
+ to_s = ->(v) { v.is_a?(Symbol) ? ":#{v}" : v }
28
+ list = values.map { |v| to_s.call(v) }.join(', ')
29
+ super ":#{property} is not in [#{list}]"
30
+ end
31
+ end
32
+
33
+ # Manages errors raised when value does not pass the assertion
34
+ class AssertError < Error
35
+ # @param property [Symbol] name of the property
36
+ # @example
37
+ # ":number does not respect the assertion"
38
+ def initialize(property)
39
+ super ":#{property} does not respect the assertion"
40
+ end
41
+ end
42
+
43
+ module Collection
44
+ # Manages errors raised when value should be an collection
45
+ class ArrayError < Error
46
+ # @param property [Symbol] name of the property
47
+ # @example
48
+ # ":numbers is not an array"
49
+ def initialize(property)
50
+ super ":#{property} is not an array"
51
+ end
52
+ end
53
+
54
+ # Manages errors raised when at least one element of the collection is not an instance of the expected class
55
+ class TypeError < Error
56
+ # @param property [Symbol] name of the property
57
+ # @param type [Class] class to match
58
+ # @example
59
+ # ":numbers contains elements that are not instances of Integer"
60
+ def initialize(property, type)
61
+ super ":#{property} contains elements that are not instances of #{type}"
62
+ end
63
+ end
64
+
65
+ # Manages errors raised when at least one element of the collection is not accepted (not in the "one_of")
66
+ class OneOfError < Error
67
+ # @param property [Symbol] name of the property
68
+ # @param values [Array] accepted values
69
+ # @example
70
+ # ":fruits contains elements that are not in [:apple, :pear]"
71
+ def initialize(property, values)
72
+ # reintegrate the colon for symbols which is lost during interpolation
73
+ to_s = ->(v) { v.is_a?(Symbol) ? ":#{v}" : v }
74
+ list = values.map { |v| to_s.call(v) }.join(', ')
75
+ super ":#{property} contains elements that are not in [#{list}]"
76
+ end
77
+ end
78
+
79
+ # Manages errors raised when value does not pass the assertion
80
+ class AssertError < Error
81
+ # @param property [Symbol] name of the property
82
+ # @example
83
+ # ":numbers contains elements that do not respect the assertion"
84
+ def initialize(property)
85
+ super ":#{property} contains elements that do not respect the assertion"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Otoroshi
4
+ # Drawing of #initialize method
5
+ #
6
+ class Initializer
7
+ class << self
8
+ # Draw a stringified initialize method
9
+ #
10
+ # @param properties [Hash] a description of the class properties
11
+ #
12
+ # @return [String]
13
+ #
14
+ # @example
15
+ # <<-RUBY
16
+ # def initialize(number: 0, message:, fruits: [])
17
+ # self.number = number
18
+ # self.message = message
19
+ # self.fruits = fruits
20
+ # end
21
+ # RUBY
22
+ def draw(properties)
23
+ new(properties).draw
24
+ end
25
+ end
26
+
27
+ # Initialize an instance
28
+ #
29
+ # @param properties [Hash] a description of the class properties
30
+ def initialize(properties = {})
31
+ @properties = properties
32
+ end
33
+
34
+ # Draws a stringified initialize method
35
+ #
36
+ # @return [String]
37
+ #
38
+ # @example
39
+ # <<-RUBY
40
+ # def initialize(foo:, bar: 0)
41
+ # self.foo = foo
42
+ # self.bar = bar
43
+ # end
44
+ # RUBY
45
+ def draw
46
+ <<~RUBY
47
+ def initialize(#{initialize_parameters})
48
+ #{initialize_body}
49
+ end
50
+ RUBY
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :properties
56
+
57
+ # Generates initialize method parameters
58
+ #
59
+ # @return [String]
60
+ #
61
+ # @example
62
+ # "foo:, bar: 0"
63
+ def initialize_parameters
64
+ parameters =
65
+ properties.map do |key, options|
66
+ "#{key}:#{default_parameter_for(options)}"
67
+ end
68
+ parameters.join(', ')
69
+ end
70
+
71
+ # Generates the default value of a parameter depending on options
72
+ #
73
+ # @return [String]
74
+ #
75
+ # @example when nil is allowed and default is set
76
+ # " \"default\""
77
+ # @example when nil is allowed and default is not set
78
+ # " nil"
79
+ # @example when nil is not allowed
80
+ # ""
81
+ def default_parameter_for(options)
82
+ default, allow_nil = options.values_at(:default, :allow_nil)
83
+ if default.nil?
84
+ allow_nil ? ' nil' : ''
85
+ else
86
+ " #{prefix(default)}#{default}#{suffix(default)}"
87
+ end
88
+ end
89
+
90
+ # Generates the characters to put before the value
91
+ # @note it avoids symbol without colon or string without quotes
92
+ # which would be interpreted as methods
93
+ def prefix(default)
94
+ case default
95
+ when Symbol then ':'
96
+ when String then '"'
97
+ end
98
+ end
99
+
100
+ # Generates the characters to put after the value
101
+ # @note it avoids string without quotes which would be interpreted as method
102
+ def suffix(default)
103
+ case default
104
+ when String then '"'
105
+ end
106
+ end
107
+
108
+ # Generates initialize method assignments
109
+ #
110
+ # @return [String]
111
+ #
112
+ # @example Given properties { foo: { allow_nil: false, default: nil }, { allow_nil: true, default: 0 } }
113
+ # <<-RUBY
114
+ # self.foo = foo
115
+ # self.bar = bar
116
+ # RUBY
117
+ def initialize_body
118
+ assignments =
119
+ properties.keys.map do |name|
120
+ "self.#{name} = #{name}"
121
+ end
122
+ assignments.join("\n ")
123
+ end
124
+ end
125
+ end
@@ -1,17 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'initializer'
4
+
3
5
  module Otoroshi
4
- # This module is designed to be included in a class. This will provide
6
+ # This module is designed to be in a class. This will provide
5
7
  # the "property" ({Sanctuary::ClassMethods.property}) method for defining class properties.
6
8
  #
7
9
  # @example
8
10
  # class Importer
9
11
  # include Otoroshi::Sanctuary
10
12
  #
11
- # property :file_path, String, validate: ->(v) { v.match? /.+\.csv/ }
12
- # property :headers, [TrueClass, FalseClass], default: false
13
- # property :col_sep, String, validate: ->(v) { v.in? [',', ';', '\s', '\t', '|'] }, default: ','
14
- # property :converters, Symbol, validate: ->(v) { v.in? %i[integer float date] }, allow_nil: true
13
+ # property :whatever
14
+ # property :number, Integer
15
+ # property :numbers, [Integer]
16
+ # property :positive_number, Integer, verify: ->(v) { v >= 0 }
17
+ # property :number_or_nil, Integer, allow_nil: true
18
+ # property :number_with_default, Integer, default: 42
19
+ # property :number_in_collection, Integer, one_of: [1, 2, 3, 5, 8, 13, 21, 34]
15
20
  # end
16
21
  module Sanctuary
17
22
  class << self
@@ -26,218 +31,196 @@ module Otoroshi
26
31
  # Adds a new "property" to the class
27
32
  #
28
33
  # @param name [Symbol] the property name
29
- # @param type [Class] the expected value type
30
- # @param validate [Proc] a lambda processing the value and returning true or false
34
+ # @param type [Class, Array<Class>] the expected value or values type
35
+ # @param one_of [Array] the accepted values
36
+ # @param assert [Proc] a lambda processing the value and returning true or false
31
37
  # @param allow_nil [true, false] allow nil as a value
32
38
  # @param default [Object] default value if not set on initialization
33
39
  #
34
40
  # @return [void]
35
41
  #
36
42
  # @example
37
- # property name, type: String, validate: ->(v) { v.length > 3 }, allow_nil: true
43
+ # property name, type: String, assert: ->(v) { v.length > 3 }, allow_nil: true
38
44
  # @example
39
- # property score, type: Integer, validate: ->(v) { v >= 0 }, default: 0
40
- def property(name, type = Object, validate: ->(_) { true }, allow_nil: false, default: nil)
41
- add_to_properties(name, allow_nil, default)
42
- define_validate_type!(name, type, allow_nil)
43
- define_validate_lambda!(name, validate, allow_nil)
45
+ # property scores, type: [Integer], assert: ->(v) { v >= 0 }, default: []
46
+ def property(name, type = Object, one_of: nil, assert: ->(_) { true }, allow_nil: false, default: nil)
47
+ add_to_properties(name, type, allow_nil, default)
48
+ expected_type = type.is_a?(Array) ? type.first || Object : type
49
+ collection = expected_type == Array || type.is_a?(Array)
50
+ define_validate_type(name, expected_type, collection, allow_nil)
51
+ define_validate_one_of(name, collection, one_of, allow_nil)
52
+ define_validate_assertion(name, collection, assert, allow_nil)
53
+ define_validate(name)
44
54
  define_getter(name)
45
55
  define_setter(name)
46
- redefine_initialize
56
+ class_eval Initializer.draw(properties), __FILE__, __LINE__ + 1
47
57
  end
48
58
 
49
- # Returns the class properties
59
+ # Class properties
50
60
  #
51
61
  # @return [Hash]
52
62
  #
53
- # @note this method will be updated by {add_to_properties}
63
+ # @example
64
+ # {
65
+ # number: { type: Integer, allow_nil: false, default: 0 },
66
+ # message: { type: Integer, allow_nil: true, default: nil }
67
+ # }
54
68
  def properties
55
69
  {}
56
70
  end
57
71
 
58
72
  private
59
73
 
60
- # Updates {properties} to add new property to the returned ones
61
- #
62
- # @return [void]
63
- def add_to_properties(name, allow_nil, default)
74
+ # Adds a properties to the {properties}
75
+ def add_to_properties(name, type, allow_nil, default)
64
76
  current_state = properties
65
- current_state[name] = { allow_nil: allow_nil, default: default }
77
+ current_state[name] = {
78
+ type: type,
79
+ allow_nil: allow_nil,
80
+ default: default
81
+ }
66
82
  define_singleton_method(:properties) { current_state }
67
83
  end
68
84
 
69
- # Defines a private method that raises an error if type is not respected
85
+ # Defines a private method that validates type condition
70
86
  #
71
- # @param name [Symbol] the property name
72
- # @param type [Class] the type to test against
73
- # @param allow_nil [true, false] allow nil as a value
74
- #
75
- # @return [void]
87
+ # Given name = :score, type = Integer, allow_nil = false
76
88
  #
77
- # @example
78
- # define_validate_type!(score, Integer, false) => def validate_score_type!(value) ...
79
- # @example Generated method
80
- # def validate_score_type!(value)
89
+ # def validate_score_type(value)
81
90
  # return if Integer.nil? || false && value.nil?
82
91
  # return if value.is_a? Integer
83
92
  #
84
- # raise ArgumentError, ":score does not match required type"
93
+ # raise Otoroshi::TypeError, :score, Integer
85
94
  # end
86
- def define_validate_type!(name, type, allow_nil)
87
- lambda = type_validation(type)
88
- define_method :"validate_#{name}_type!" do |value|
89
- return if type.nil? || allow_nil && value.nil?
90
- return if lambda.call(value)
95
+ def define_validate_type(name, type, collection, allow_nil)
96
+ validator = validate_type?(name, type, collection)
97
+ define_method :"validate_#{name}_type" do |value|
98
+ allow_nil && value.nil? || validator.call(value)
99
+ end
100
+ private :"validate_#{name}_type"
101
+ end
91
102
 
92
- raise ArgumentError, ":#{name} does not match required type"
103
+ # Lambda that validates (value) respects the type condition
104
+ # @return [Proc]
105
+ def validate_type?(name, type, collection)
106
+ if collection
107
+ # validate each element of (v) is an instance of the type
108
+ lambda do |v|
109
+ v.is_a?(Array) || raise(Otoroshi::Collection::ArrayError, name)
110
+ v.all? { |elt| elt.is_a? type } || raise(Otoroshi::Collection::TypeError.new(name, type))
111
+ end
112
+ else
113
+ # validate (v) is an instance of the type
114
+ ->(v) { v.is_a?(type) || raise(Otoroshi::TypeError.new(name, type)) }
93
115
  end
94
- private :"validate_#{name}_type!"
95
116
  end
96
117
 
97
- # Defines a lambda to be call to validate that value matches the type
118
+ # Defines a private method that validates one_of condition
98
119
  #
99
- # @param type [Class] the type to test against
120
+ # Given name = :side, collection = false, one_of = [:left, :right], allow_nil = false
100
121
  #
101
- # @return [Proc] the lambda to use in order to test the value matches the type
122
+ # def validate_side_type(value)
123
+ # return if false && value.nil?
124
+ # return if [:left, :right].include? value
102
125
  #
103
- # @example
104
- # type_validation(Integer) #=> ->(v) { v.is_a? Integer }
105
- # @example
106
- # type_validation([String, Symbol]) #=> ->(v) { [String, Symbol].any? { |t| v.is_a? t } }
107
- def type_validation(type)
108
- if type.is_a? Array
109
- ->(v) { type.any? { |t| v.is_a? t } }
126
+ # raise Otoroshi::OneOfError, :side, [:left, :right]
127
+ # end
128
+ def define_validate_one_of(name, collection, one_of, allow_nil)
129
+ validator = validate_one_of?(name, one_of, collection)
130
+ define_method(:"validate_#{name}_one_of") do |value|
131
+ allow_nil && value.nil? || validator.call(value)
132
+ end
133
+ private :"validate_#{name}_one_of"
134
+ end
135
+
136
+ # Lambda that validates (value) respects the one_of condition
137
+ # @return [Proc]
138
+ def validate_one_of?(name, one_of, collection)
139
+ return ->(_) {} unless one_of
140
+
141
+ if collection
142
+ lambda do |v|
143
+ v.all? { |e| one_of.include? e } || raise(Otoroshi::Collection::OneOfError.new(name, one_of))
144
+ end
110
145
  else
111
- ->(v) { v.is_a? type }
146
+ lambda do |v|
147
+ one_of.include?(v) || raise(Otoroshi::OneOfError.new(name, one_of))
148
+ end
112
149
  end
113
150
  end
114
151
 
115
- # Defines a private method that raises an error if validate block returns false
152
+ # Defines a private method that validates assert condition
116
153
  #
117
- # @param name [Symbol] the property name
118
- # @param validate [Proc] a lambda processing the value and returning true or false
119
- # @param allow_nil [true, false] allow nil as a value
154
+ # Given name = :score, assert = ->(v) { v >= 0 }, allow_nil = false
120
155
  #
121
- # @return [void]
122
- #
123
- # @example
124
- # define_validate_lambda!("score", ->(v) { v >= 0 }, false) #=> def validate_score_lambda!(value) ...
125
- # @example Generated instance method
126
- # def validate_score_lambda!(value)
156
+ # def validate_score_assertion(value)
127
157
  # return if false && value.nil?
128
158
  # return if value >= 0
129
159
  #
130
- # raise ArgumentError, ":score does not match validation"
160
+ # raise Otoroshi::AssertError, :score
131
161
  # end
132
- def define_validate_lambda!(name, validate, allow_nil)
133
- define_method :"validate_#{name}_lambda!" do |value|
134
- return if allow_nil && value.nil?
135
- return if instance_exec(value, &validate)
162
+ def define_validate_assertion(name, collection, assert, allow_nil)
163
+ validator = validate_assert?(name, assert, collection)
164
+ define_method :"validate_#{name}_assertion" do |value|
165
+ allow_nil && value.nil? || validator.call(value)
166
+ end
167
+ private :"validate_#{name}_assertion"
168
+ end
136
169
 
137
- raise ArgumentError, ":#{name} does not match validation"
170
+ # Lambda that validates (value) respects the assert condition
171
+ # @return [Proc]
172
+ def validate_assert?(name, assert, collection)
173
+ if collection
174
+ ->(v) { v.all? { |e| instance_exec(e, &assert) } || raise(Otoroshi::Collection::AssertError, name) }
175
+ else
176
+ ->(v) { instance_exec(v, &assert) || raise(Otoroshi::AssertError, name) }
138
177
  end
139
- private :"validate_#{name}_lambda!"
140
178
  end
141
179
 
142
- # Defines a getter method for the property
180
+ # Defines a private method that calls all validations
143
181
  #
144
- # @param name [Symbol] the property name
182
+ # Given name = :score
145
183
  #
146
- # @return [void]
184
+ # def validate_score!(value)
185
+ # validate_score_type(value)
186
+ # validate_score_one_of(value)
187
+ # validate_score_assert(value)
188
+ # end
189
+ def define_validate(name)
190
+ define_method :"validate_#{name}!" do |value|
191
+ __send__(:"validate_#{name}_type", value)
192
+ __send__(:"validate_#{name}_one_of", value)
193
+ __send__(:"validate_#{name}_assertion", value)
194
+ end
195
+ private :"validate_#{name}!"
196
+ end
197
+
198
+ # Defines a getter
147
199
  #
148
- # @example
149
- # define_getter(:score) #=> def score ...
150
- # @example Generated instance method
151
- # def score
152
- # instance_variable_get(@score)
153
- # end
200
+ # Given name = :score
201
+ #
202
+ # def score
203
+ # instance_variable_get(@score)
204
+ # end
154
205
  def define_getter(name)
155
- define_method(name) { instance_variable_get("@#{name}") }
206
+ define_method(name) { instance_variable_get("@#{name}").clone.freeze }
156
207
  end
157
208
 
158
- # Defines a setter method for the property
159
- #
160
- # @param name [Symbol] the property name
209
+ # Defines a setter
161
210
  #
162
- # @return [void]
211
+ # Given name = :score
163
212
  #
164
- # @example
165
- # define_getter(:score) #=> def score=(value) ...
166
- # @example Generated instance method
167
213
  # def score=(value)
168
- # validate_score_type!(value)
214
+ # validate_score_type(value)
169
215
  # validate_score!(value)
170
216
  # instance_variable_set(@score, value)
171
217
  # end
172
218
  def define_setter(name)
173
219
  define_method :"#{name}=" do |value|
174
- __send__(:"validate_#{name}_type!", value)
175
- __send__(:"validate_#{name}_lambda!", value)
220
+ __send__(:"validate_#{name}!", value)
176
221
  instance_variable_set("@#{name}", value)
177
222
  end
178
223
  end
179
-
180
- # Redefines initialize method
181
- #
182
- # @return [void]
183
- #
184
- # @note method is defined with `class_eval`
185
- #
186
- # @example Generated method
187
- # def initialize(foo:, bar: 0)
188
- # self.foo = foo
189
- # self.bar = bar
190
- # end
191
- def redefine_initialize
192
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
193
- def initialize(#{initialize_parameters})
194
- #{initialize_body}
195
- end
196
- RUBY
197
- end
198
-
199
- # Defines initialize method parameters
200
- #
201
- # @return [String]
202
- #
203
- # @example Given properties { foo: { allow_nil: false, default: nil }, { allow_nil: true, default: 0 } }
204
- # redefine_initialize #=> "foo:, bar: 0"
205
- def initialize_parameters
206
- parameters =
207
- properties.map do |key, options|
208
- allow_nil, default = options.values
209
- "#{key}:#{default_parameter_for(allow_nil, default)}"
210
- end
211
- parameters.join(', ')
212
- end
213
-
214
- # Defines the default value of a parameter depending on options
215
- #
216
- # @param options [Hash]
217
- #
218
- # @return [String]
219
- #
220
- # @example when nil is allowed and default is set
221
- # default_parameter_for(true, 0) #=> " 0"
222
- # @example when nil is allowed and default is not set
223
- # default_parameter_for(true, nil) #=> " nil"
224
- # @example when nil is not allowed
225
- # default_parameter_for(false, nil) #=> ""
226
- def default_parameter_for(allow_nil, default)
227
- return " #{default}" if default
228
-
229
- allow_nil ? ' nil' : ''
230
- end
231
-
232
- # Defines initialize method body
233
- #
234
- # @return [String]
235
- #
236
- # @example Given properties { foo: { allow_nil: false, default: nil }, { allow_nil: true, default: 0 } }
237
- # initialize_body #=> "self.foo = foo\nself.bar = bar"
238
- def initialize_body
239
- properties.keys.map { |key| "self.#{key} = #{key}" }.join("\n")
240
- end
241
224
  end
242
225
  end
243
226
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otoroshi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edouard Piron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-10 00:00:00.000000000 Z
11
+ date: 2020-10-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Help defining class properties
14
14
  email: ed.piron@gmail.com
@@ -16,6 +16,9 @@ executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
+ - lib/otoroshi.rb
20
+ - lib/otoroshi/exceptions.rb
21
+ - lib/otoroshi/initializer.rb
19
22
  - lib/otoroshi/sanctuary.rb
20
23
  homepage: https://rubygems.org/gems/otoroshi
21
24
  licenses: