moguro 0.0.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 +7 -0
- data/.gitignore +10 -0
- data/.rspec +3 -0
- data/.rubocop.yml +48 -0
- data/.travis.yml +9 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +91 -0
- data/Rakefile +12 -0
- data/example.rb +64 -0
- data/lib/moguro.rb +33 -0
- data/lib/moguro/caluse.rb +90 -0
- data/lib/moguro/contract.rb +67 -0
- data/lib/moguro/decorator.rb +69 -0
- data/lib/moguro/errors.rb +97 -0
- data/lib/moguro/extractor.rb +9 -0
- data/lib/moguro/extractors/arguments_extractor.rb +36 -0
- data/lib/moguro/extractors/return_value_extractor.rb +19 -0
- data/lib/moguro/handler.rb +9 -0
- data/lib/moguro/handlers/class_handler.rb +71 -0
- data/lib/moguro/handlers/method_handler.rb +74 -0
- data/lib/moguro/method_reference.rb +45 -0
- data/lib/moguro/processors/arguments_processor.rb +42 -0
- data/lib/moguro/processors/contract_processor.rb +66 -0
- data/lib/moguro/processors/enumerable_processor.rb +43 -0
- data/lib/moguro/processros.rb +10 -0
- data/lib/moguro/sandbox.rb +15 -0
- data/lib/moguro/types.rb +37 -0
- data/lib/moguro/types/any.rb +23 -0
- data/lib/moguro/types/boolean.rb +29 -0
- data/lib/moguro/types/enumerable.rb +47 -0
- data/lib/moguro/types/nil.rb +18 -0
- data/lib/moguro/types/skin.rb +29 -0
- data/lib/moguro/values.rb +72 -0
- data/lib/moguro/version.rb +5 -0
- data/moguro.gemspec +37 -0
- metadata +263 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
###
|
5
|
+
# Contract of class state
|
6
|
+
# @since 0.0.1
|
7
|
+
# @abstract
|
8
|
+
###
|
9
|
+
class Contract
|
10
|
+
attr_reader :method
|
11
|
+
|
12
|
+
def initialize(klass, callback, method)
|
13
|
+
@klass = klass
|
14
|
+
@type_clauses = Moguro::Processors::ContractProcessor.generate_type_validator(callback, klass)
|
15
|
+
@cb = callback
|
16
|
+
@method = method
|
17
|
+
end
|
18
|
+
|
19
|
+
def verify!(instance, args)
|
20
|
+
values = extractor.extract(args)
|
21
|
+
@type_clauses.verify!(values)
|
22
|
+
instance.instance_exec(values.to_h, &@cb)
|
23
|
+
end
|
24
|
+
|
25
|
+
def extractor
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class PreconditionContract < Contract
|
31
|
+
def initialize(klass, callback, method)
|
32
|
+
super(klass, callback, method)
|
33
|
+
end
|
34
|
+
|
35
|
+
def verify!(instance, args)
|
36
|
+
super
|
37
|
+
rescue Moguro::Errors::TypeMismatchError => e
|
38
|
+
raise Moguro::Errors::ArgumentsTypeMismatchError.new(e, @klass, method)
|
39
|
+
rescue => e
|
40
|
+
raise e
|
41
|
+
end
|
42
|
+
|
43
|
+
def extractor
|
44
|
+
@extractor ||= Moguro::Extractor::ArgumentsExtractor.new(method.parameters)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class PostconditionContract < Contract
|
49
|
+
def initialize(klass, callback, method)
|
50
|
+
super(klass, callback, method)
|
51
|
+
end
|
52
|
+
|
53
|
+
def verify!(instance, args)
|
54
|
+
super
|
55
|
+
rescue Moguro::Errors::TypeMismatchError => e
|
56
|
+
raise Moguro::Errors::ReturnValueTypeMismatchError.new(e, @klass, method)
|
57
|
+
rescue => e
|
58
|
+
raise e
|
59
|
+
end
|
60
|
+
|
61
|
+
def extractor
|
62
|
+
@extractor ||= Moguro::Extractor::ReturnValueExtractor.new(
|
63
|
+
@cb.parameters
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
###
|
5
|
+
# Provide contract programming feature to the class that included this module.
|
6
|
+
# Validates the arguments to the method, and validates the return value of the method.
|
7
|
+
# And you arbitrary test possible by Assertion library.
|
8
|
+
# @since 0.0.1
|
9
|
+
###
|
10
|
+
module Decorator
|
11
|
+
CONTRACT_CONTAINER = Struct.new(:pre, :post)
|
12
|
+
|
13
|
+
def self.included(klass)
|
14
|
+
klass.extend(ClassMethods)
|
15
|
+
klass.include(Moguro::Types)
|
16
|
+
# @todo
|
17
|
+
return unless Moguro.enabled?
|
18
|
+
require 'test/unit/assertions'
|
19
|
+
klass.include(Test::Unit::Assertions)
|
20
|
+
klass.extend(Test::Unit::Assertions)
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def singleton_method_added(name)
|
25
|
+
return super unless Moguro.enabled? && contract_handler.validatable?
|
26
|
+
super
|
27
|
+
contract_handler.handle_singleton_method(name)
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_added(name)
|
31
|
+
return super unless Moguro.enabled? && contract_handler.validatable?
|
32
|
+
super
|
33
|
+
contract_handler.handle_method(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
###
|
37
|
+
# Validates the arguments to the method
|
38
|
+
# @since 0.0.1
|
39
|
+
# @param [Lambda|Proc] callback
|
40
|
+
# @return [Void]
|
41
|
+
#
|
42
|
+
def pre_c(callback)
|
43
|
+
return unless Moguro.enabled?
|
44
|
+
contract_handler.contract_arguments(callback)
|
45
|
+
end
|
46
|
+
|
47
|
+
###
|
48
|
+
# Validates the arguments to the method
|
49
|
+
# @since 0.0.1
|
50
|
+
# @param [Lambda|Proc] callback
|
51
|
+
# @return [Void]
|
52
|
+
###
|
53
|
+
def post_c(callback)
|
54
|
+
return unless Moguro.enabled?
|
55
|
+
contract_handler.contract_return_value(callback)
|
56
|
+
end
|
57
|
+
|
58
|
+
###
|
59
|
+
# Validates the arguments to the method
|
60
|
+
# @since 0.0.1
|
61
|
+
# @param [Lambda|Proc] callback
|
62
|
+
# @return [Void]
|
63
|
+
##
|
64
|
+
def contract_handler
|
65
|
+
@contract_handler ||= Moguro::Handlers::ClassHandler.new(self)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
module Errors
|
5
|
+
###
|
6
|
+
# @private
|
7
|
+
# Base class for Errors
|
8
|
+
# If default failure callback is used it stores failure data
|
9
|
+
###
|
10
|
+
class BaseError < ::ArgumentError
|
11
|
+
end
|
12
|
+
|
13
|
+
###
|
14
|
+
# Type miss match error class for Arguments and return value
|
15
|
+
# @attribute :actual, :expected
|
16
|
+
class TypeMismatchError < BaseError
|
17
|
+
attr_accessor :actual, :expected
|
18
|
+
attr_reader :failure_clause, :failure_value
|
19
|
+
|
20
|
+
def initialize(failure_clause, failure_value)
|
21
|
+
@failure_clause = failure_clause
|
22
|
+
@failure_value = failure_value
|
23
|
+
@actual = nil
|
24
|
+
@expected = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def failure_detail
|
28
|
+
<<~"DETAIL".chomp
|
29
|
+
Expected: #{@expected}
|
30
|
+
Actual: #{@actual}
|
31
|
+
DETAIL
|
32
|
+
end
|
33
|
+
|
34
|
+
def failure_summary
|
35
|
+
"Type MissMatch: #{@failure_clause.key} is expected #{@failure_clause.type_inspect} actual #{@failure_value.type_inspect}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ArgumentsTypeMismatchError < BaseError
|
40
|
+
def initialize(type_mismatch_error, klass, method)
|
41
|
+
@e = type_mismatch_error
|
42
|
+
@klass = klass
|
43
|
+
@method = method
|
44
|
+
super(failure_message)
|
45
|
+
end
|
46
|
+
|
47
|
+
def failure_title
|
48
|
+
'ArgumentsTypeError'
|
49
|
+
end
|
50
|
+
|
51
|
+
def failure_message
|
52
|
+
<<~"MESSAGE".chomp
|
53
|
+
#{failure_title} => #{@e.failure_summary}
|
54
|
+
#{@e.failure_detail}
|
55
|
+
#{failure_location}
|
56
|
+
MESSAGE
|
57
|
+
end
|
58
|
+
|
59
|
+
def failure_location
|
60
|
+
<<~"LOCATION".chomp
|
61
|
+
Value guarded in: #{@klass}::#{@method.name}
|
62
|
+
At: #{@method.position}
|
63
|
+
LOCATION
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class ReturnValueTypeMismatchError < BaseError
|
68
|
+
def initialize(type_mismatch_error, klass, method)
|
69
|
+
@e = type_mismatch_error
|
70
|
+
@klass = klass
|
71
|
+
@method = method
|
72
|
+
super(failure_message)
|
73
|
+
end
|
74
|
+
|
75
|
+
def failure_title
|
76
|
+
'ReturnValueTypeError'
|
77
|
+
end
|
78
|
+
|
79
|
+
def failure_message
|
80
|
+
<<~"MESSAGE".chomp
|
81
|
+
#{failure_title} => #{@e.failure_summary}
|
82
|
+
#{@e.failure_detail}
|
83
|
+
#{failure_location}
|
84
|
+
MESSAGE
|
85
|
+
end
|
86
|
+
|
87
|
+
def failure_location
|
88
|
+
<<~"LOCATION".chomp
|
89
|
+
Value guarded in: #{@klass}::#{@method.name}
|
90
|
+
At: #{@method.position}
|
91
|
+
LOCATION
|
92
|
+
end
|
93
|
+
end
|
94
|
+
class ArgumentError < BaseError
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
module Extractor
|
5
|
+
class ArgumentsExtractor
|
6
|
+
def initialize(parameters)
|
7
|
+
@parameters = parameters
|
8
|
+
end
|
9
|
+
|
10
|
+
def extract(args)
|
11
|
+
opt = args.last.is_a?(Hash) ? args.last : nil
|
12
|
+
@parameters.each_with_object(Values.new).with_index do |(params, values), i|
|
13
|
+
arg_type = params[0]
|
14
|
+
key = params[1]
|
15
|
+
case arg_type
|
16
|
+
when :req
|
17
|
+
val = args[i]
|
18
|
+
values.add_value(key, val)
|
19
|
+
when :keyreq
|
20
|
+
if opt&.key?(key)
|
21
|
+
val = opt[key]
|
22
|
+
values.add_value(key, val)
|
23
|
+
else
|
24
|
+
values.add_value(key, val, missing: true)
|
25
|
+
end
|
26
|
+
when :key
|
27
|
+
val = opt[key]
|
28
|
+
values.add_value(key, val)
|
29
|
+
when :opt
|
30
|
+
values.add_value(key, args[i])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
module Extractor
|
5
|
+
class ReturnValueExtractor
|
6
|
+
def initialize(parameters)
|
7
|
+
@parameters = parameters
|
8
|
+
end
|
9
|
+
|
10
|
+
def extract(return_value)
|
11
|
+
@parameters.each_with_object(Values.new).with_index do |(params, values), i|
|
12
|
+
key = params[1]
|
13
|
+
val = return_value[i]
|
14
|
+
values.add_value(key, val, missing: val.nil?)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
module Handlers
|
5
|
+
# Handler for method added event
|
6
|
+
# @private
|
7
|
+
# @since 0.0.1
|
8
|
+
# @attr_reader [Class] binding class
|
9
|
+
class ClassHandler
|
10
|
+
attr_reader :klass
|
11
|
+
|
12
|
+
def initialize(klass)
|
13
|
+
@klass = klass
|
14
|
+
@instance_handlers = {}
|
15
|
+
@class_handlers = {}
|
16
|
+
@arguments_contract = nil
|
17
|
+
@return_value_contract = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_singleton_method(name)
|
21
|
+
@class_handlers[name] ||= SingletonMethodHandler.new(
|
22
|
+
self,
|
23
|
+
name,
|
24
|
+
arguments_contract: @arguments_contract,
|
25
|
+
return_value_contract: @return_value_contract
|
26
|
+
)
|
27
|
+
clear_validator
|
28
|
+
m = @class_handlers[name].module
|
29
|
+
@klass.singleton_class.class_eval { prepend(m) }
|
30
|
+
end
|
31
|
+
|
32
|
+
def handle_method(name)
|
33
|
+
@instance_handlers[name] = MethodHandler.new(
|
34
|
+
self,
|
35
|
+
name,
|
36
|
+
arguments_contract: @arguments_contract,
|
37
|
+
return_value_contract: @return_value_contract
|
38
|
+
)
|
39
|
+
clear_validator
|
40
|
+
@klass.prepend(@instance_handlers[name].module)
|
41
|
+
end
|
42
|
+
|
43
|
+
def contract_arguments(callback)
|
44
|
+
@arguments_contract = callback
|
45
|
+
end
|
46
|
+
|
47
|
+
def contract_return_value(callback)
|
48
|
+
@return_value_contract = callback
|
49
|
+
end
|
50
|
+
|
51
|
+
def validatable?
|
52
|
+
!@arguments_contract.nil? || !@return_value_contract.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def _get_class_method_handler(name)
|
56
|
+
@class_handlers[name]
|
57
|
+
end
|
58
|
+
|
59
|
+
def _get_instance_method_handler(name)
|
60
|
+
@instance_handlers[name]
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def clear_validator
|
66
|
+
@arguments_contract = nil
|
67
|
+
@return_value_contract = nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Moguro
|
4
|
+
module Handlers
|
5
|
+
class MethodHandler
|
6
|
+
def initialize(class_handler, name, arguments_contract: nil, return_value_contract: nil)
|
7
|
+
@class_handler = class_handler
|
8
|
+
@name = name
|
9
|
+
@arguments_contract =
|
10
|
+
if arguments_contract.nil?
|
11
|
+
arguments_contract
|
12
|
+
else
|
13
|
+
PreconditionContract.new(klass, arguments_contract, reference)
|
14
|
+
end
|
15
|
+
@return_value_contract =
|
16
|
+
if return_value_contract.nil?
|
17
|
+
return_value_contract
|
18
|
+
else
|
19
|
+
PostconditionContract.new(klass, return_value_contract, reference)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def module
|
24
|
+
m = Module.new
|
25
|
+
|
26
|
+
arguments_contract = @arguments_contract
|
27
|
+
return_value_contract = @return_value_contract
|
28
|
+
|
29
|
+
arguments_contract_cb = lambda { |klass, args|
|
30
|
+
arguments_contract&.verify!(klass, args)
|
31
|
+
}
|
32
|
+
|
33
|
+
return_value_contract_cb = lambda { |klass, results|
|
34
|
+
return_value_contract&.verify!(klass, results)
|
35
|
+
}
|
36
|
+
name = @name
|
37
|
+
m.module_eval do
|
38
|
+
define_method(name) do |*args, &block|
|
39
|
+
arguments_contract_cb.call(self, args)
|
40
|
+
*results = super(*args, &block)
|
41
|
+
return_value_contract_cb.call(self, results)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
m
|
45
|
+
end
|
46
|
+
|
47
|
+
def reference
|
48
|
+
@reference ||= MethodReference.new(
|
49
|
+
method
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def parameters
|
56
|
+
@parameters ||= method.parameters
|
57
|
+
end
|
58
|
+
|
59
|
+
def method
|
60
|
+
@method ||= klass.instance_method(@name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def klass
|
64
|
+
@klass ||= @class_handler.klass
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class SingletonMethodHandler < MethodHandler
|
69
|
+
def method
|
70
|
+
@method ||= klass.method(@name)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|