anodator 1.0.0.pre1 → 1.0.0.pre2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +3 -0
- data/.gitignore +3 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -2
- data/README.md +9 -4
- data/Rakefile +3 -3
- data/anodator.gemspec +17 -14
- data/bin/console +3 -3
- data/bin/console-on-docker +4 -0
- data/bin/docker-console +2 -0
- data/bin/docker-prompt +2 -0
- data/bin/prompt-on-docker +4 -0
- data/bin/setup +0 -0
- data/example/example_01.rb +88 -61
- data/lib/anodator.rb +3 -2
- data/lib/anodator/anodator_error.rb +1 -1
- data/lib/anodator/check_result.rb +7 -7
- data/lib/anodator/checker.rb +33 -26
- data/lib/anodator/common.rb +15 -0
- data/lib/anodator/data_source.rb +87 -0
- data/lib/anodator/data_source_set.rb +36 -0
- data/lib/anodator/input_spec.rb +105 -111
- data/lib/anodator/input_spec_item.rb +8 -8
- data/lib/anodator/message.rb +14 -14
- data/lib/anodator/output_spec.rb +40 -43
- data/lib/anodator/rule.rb +26 -32
- data/lib/anodator/rule_set.rb +6 -12
- data/lib/anodator/utils.rb +36 -42
- data/lib/anodator/validator.rb +9 -9
- data/lib/anodator/validator/base.rb +44 -27
- data/lib/anodator/validator/blank_validator.rb +2 -2
- data/lib/anodator/validator/complex_validator.rb +12 -12
- data/lib/anodator/validator/configuration_error.rb +1 -2
- data/lib/anodator/validator/date_validator.rb +93 -103
- data/lib/anodator/validator/format_validator.rb +8 -11
- data/lib/anodator/validator/inclusion_validator.rb +3 -3
- data/lib/anodator/validator/length_validator.rb +6 -6
- data/lib/anodator/validator/numeric_validator.rb +13 -13
- data/lib/anodator/validator/presence_validator.rb +2 -2
- data/lib/anodator/validator/value_proxy.rb +31 -9
- data/lib/anodator/version.rb +1 -1
- metadata +41 -6
- data/VERSION +0 -1
data/lib/anodator/checker.rb
CHANGED
@@ -1,48 +1,55 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
require 'anodator/input_spec'
|
2
|
+
require 'anodator/rule_set'
|
3
|
+
require 'anodator/output_spec'
|
4
|
+
require 'anodator/data_source_set'
|
5
|
+
require 'anodator/anodator_error'
|
5
6
|
|
6
7
|
module Anodator
|
8
|
+
# Checker class.
|
7
9
|
class Checker
|
8
|
-
|
9
|
-
@input_spec = input_spec
|
10
|
-
@rule_set = rule_set
|
11
|
-
@output_specs = [default_output_spec]
|
10
|
+
include Common
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
16
|
-
unless @rule_set.is_a? RuleSet
|
17
|
-
raise ArgumentError.new("rule_set must be RuleSet object")
|
18
|
-
end
|
19
|
-
unless @output_specs.first.is_a? OutputSpec
|
20
|
-
raise ArgumentError.new("default_output_spec must be OutputSpec object")
|
21
|
-
end
|
12
|
+
def initialize(input_spec, rule_set, output_spec, config_check = false)
|
13
|
+
validate_initialize_params(input_spec, rule_set, output_spec)
|
22
14
|
|
15
|
+
@input_spec = input_spec
|
16
|
+
@rule_set = rule_set
|
17
|
+
@output_specs = [output_spec]
|
18
|
+
@data_source_set = DataSourceSet.new
|
23
19
|
Validator::Base.values = @input_spec
|
20
|
+
Validator::Base.data_source_set = @data_source_set
|
24
21
|
|
25
|
-
validate_configuration if
|
22
|
+
validate_configuration if config_check
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate_initialize_params(input_spec, rule_set, output_spec)
|
26
|
+
params =
|
27
|
+
[
|
28
|
+
[input_spec, InputSpec],
|
29
|
+
[rule_set, RuleSet],
|
30
|
+
[output_spec, OutputSpec]
|
31
|
+
]
|
32
|
+
|
33
|
+
check_instances(params, ArgumentError)
|
26
34
|
end
|
27
35
|
|
28
36
|
def validate_configuration
|
29
|
-
# RuleSet
|
30
37
|
@rule_set.validate_configuration
|
31
|
-
|
32
|
-
@output_specs.each do |spec|
|
33
|
-
spec.validate_configuration
|
34
|
-
end
|
38
|
+
@output_specs.each(&:validate_configuration)
|
35
39
|
end
|
36
40
|
|
37
41
|
def add_output_spec(output_spec, configuration_check = false)
|
38
|
-
|
39
|
-
|
40
|
-
end
|
42
|
+
check_instances([[output_spec, OutputSpec]], ArgumentError)
|
43
|
+
|
41
44
|
@output_specs << output_spec
|
42
45
|
|
43
46
|
validate_configuration if configuration_check
|
44
47
|
end
|
45
48
|
|
49
|
+
def add_data_source(data_source)
|
50
|
+
@data_source_set << data_source
|
51
|
+
end
|
52
|
+
|
46
53
|
def run(values)
|
47
54
|
@input_spec.source = values
|
48
55
|
@rule_set.check_all
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Anodator
|
2
|
+
# Anodator common class.
|
3
|
+
#
|
4
|
+
# Common class is useful common functions.
|
5
|
+
module Common
|
6
|
+
def check_instances(value_and_classes, exception_class)
|
7
|
+
value_and_classes.each do |value_and_class|
|
8
|
+
v = value_and_class[0]
|
9
|
+
c = value_and_class[1]
|
10
|
+
|
11
|
+
raise exception_class, "#{v} must be #{c}" unless v.is_a? c
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
module Anodator
|
2
|
+
# Unknown key on DataSource error.
|
3
|
+
class UnknownKeyOnDataSourceError < StandardError; end
|
4
|
+
# DataSource is external data source for check input datas.
|
5
|
+
#
|
6
|
+
# datas = {
|
7
|
+
# 'key1' => {
|
8
|
+
# name: 'taro',
|
9
|
+
# age: 20
|
10
|
+
# },
|
11
|
+
# 'key2' => {
|
12
|
+
# name: 'jiro',
|
13
|
+
# age: 35
|
14
|
+
# }
|
15
|
+
# }
|
16
|
+
# # datas object must be respond to :[].
|
17
|
+
# ds = DataSource.new('name', datas)
|
18
|
+
class DataSource
|
19
|
+
VALID_OPTIONS = {
|
20
|
+
on_nil: %i[raise_error nil],
|
21
|
+
on_exception: %i[raise_error nil]
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
DEFAULT_OPTIONS = {
|
25
|
+
on_nil: :nil,
|
26
|
+
on_exception: :raise_error
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
attr_reader :identifier
|
30
|
+
|
31
|
+
def initialize(identifier, datas, options = {})
|
32
|
+
msg = 'datas must be respond to :[]'
|
33
|
+
raise ArgumentError, msg unless datas.respond_to?(:[])
|
34
|
+
options = merge_default_options(options)
|
35
|
+
validate_options(options)
|
36
|
+
|
37
|
+
@identifier = identifier.to_s
|
38
|
+
@datas = datas
|
39
|
+
@options = options
|
40
|
+
end
|
41
|
+
|
42
|
+
def [](key)
|
43
|
+
@datas[key].nil? ? on_nil_value : @datas[key]
|
44
|
+
rescue UnknownKeyOnDataSourceError
|
45
|
+
raise
|
46
|
+
rescue StandardError => e
|
47
|
+
on_exception(e)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def merge_default_options(options)
|
53
|
+
DEFAULT_OPTIONS.each_with_object(options.dup) do |(k, v), merged|
|
54
|
+
merged[k] ||= v
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_options(options)
|
59
|
+
validate_option_keys(options)
|
60
|
+
validate_option_values(options)
|
61
|
+
end
|
62
|
+
|
63
|
+
def validate_option_keys(options)
|
64
|
+
options.each_key do |key|
|
65
|
+
msg = "Invalid option '#{key.inspect}'"
|
66
|
+
raise ArgumentError, msg unless VALID_OPTIONS.key?(key)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def validate_option_values(options)
|
71
|
+
options.each do |key, value|
|
72
|
+
msg = "Invalid #{key.inspect} option value '#{value.inspect}'"
|
73
|
+
raise ArgumentError, msg unless VALID_OPTIONS[key].include?(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def on_nil_value
|
78
|
+
return nil if @options[:on_nil] == :nil
|
79
|
+
raise UnknownKeyOnDataSourceError, 'DataSource nil'
|
80
|
+
end
|
81
|
+
|
82
|
+
def on_exception(e)
|
83
|
+
return nil if @options[:on_exception] == :nil
|
84
|
+
raise UnknownKeyOnDataSourceError, e
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'anodator/data_source'
|
2
|
+
|
3
|
+
module Anodator
|
4
|
+
# duplicated DataSource Identifier error.
|
5
|
+
class DuplicatedDataSourceIdentifierError < StandardError; end
|
6
|
+
# DataSourceSet is a set of DataSource
|
7
|
+
class DataSourceSet
|
8
|
+
def initialize
|
9
|
+
@data_sources = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def <<(data_source)
|
13
|
+
msg = 'data_source must be DataSource.'
|
14
|
+
raise ArgumentError, msg unless data_source.is_a? DataSource
|
15
|
+
|
16
|
+
check_duplicate_key(data_source)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fetch(identifier, key, column)
|
20
|
+
identifier = identifier.to_s.to_sym
|
21
|
+
key = key.to_s.to_sym
|
22
|
+
column = column.to_s.to_sym
|
23
|
+
|
24
|
+
@data_sources[identifier][key][column]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def check_duplicate_key(data_source)
|
30
|
+
key = data_source.identifier.to_sym
|
31
|
+
raise DuplicatedDataSourceIdentifierError if @data_sources.key?(key)
|
32
|
+
|
33
|
+
@data_sources[key] = data_source
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/anodator/input_spec.rb
CHANGED
@@ -1,60 +1,68 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'anodator/input_spec_item'
|
2
|
+
require 'anodator/anodator_error'
|
3
3
|
|
4
4
|
module Anodator
|
5
5
|
class DuplicatedInputSpecItemError < AnodatorError; end
|
6
6
|
class UnknownTargetExpressionError < AnodatorError; end
|
7
7
|
class SourceDataNotProvidedError < AnodatorError; end
|
8
8
|
|
9
|
+
# InputSpec
|
9
10
|
class InputSpec
|
10
11
|
CALCULATION_HOLDER_REGEXP = /\[\[([^\]]+)\]\]/
|
11
12
|
|
13
|
+
include Common
|
14
|
+
|
12
15
|
def initialize(spec_items = [])
|
13
16
|
@spec_items = []
|
14
17
|
@source = nil
|
15
|
-
@number_dict = {
|
16
|
-
@name_dict = {
|
18
|
+
@number_dict = {}
|
19
|
+
@name_dict = {}
|
20
|
+
|
21
|
+
check_instances([[spec_items, Array]], ArgumentError)
|
22
|
+
setup_input_spec_items(spec_items)
|
23
|
+
end
|
17
24
|
|
18
|
-
|
19
|
-
|
25
|
+
def setup_input_spec_items(spec_items)
|
26
|
+
spec_items.each do |spec|
|
27
|
+
next unless %i[number name].all? { |k| spec.key?(k) }
|
28
|
+
push_spec_items(build_input_spec_item(spec))
|
20
29
|
end
|
30
|
+
end
|
31
|
+
private :setup_input_spec_items
|
21
32
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
push_spec_items(InputSpecItem.new(spec_item_values[:number],
|
28
|
-
spec_item_values[:name],
|
29
|
-
spec_item_values[:type]))
|
30
|
-
else
|
31
|
-
push_spec_items(InputSpecItem.new(spec_item_values[:number],
|
32
|
-
spec_item_values[:name]))
|
33
|
-
end
|
34
|
-
end
|
33
|
+
def build_input_spec_item(spec)
|
34
|
+
if spec[:type].nil?
|
35
|
+
InputSpecItem.new(spec[:number], spec[:name])
|
36
|
+
else
|
37
|
+
InputSpecItem.new(spec[:number], spec[:name], spec[:type])
|
35
38
|
end
|
36
39
|
end
|
40
|
+
private :build_input_spec_item
|
37
41
|
|
38
42
|
def push_spec_items(spec)
|
39
|
-
|
40
|
-
|
41
|
-
end
|
42
|
-
if @name_dict.keys.include?(spec.name)
|
43
|
-
raise DuplicatedInputSpecItemError.new("duplicated name spec item '#{spec.name}'")
|
44
|
-
end
|
43
|
+
check_duplicate_key(spec.number, spec.name)
|
44
|
+
|
45
45
|
@spec_items << spec
|
46
46
|
index = @spec_items.size - 1
|
47
|
-
@number_dict[spec.number] = { :
|
48
|
-
@name_dict[spec.name] = { :
|
47
|
+
@number_dict[spec.number] = { item: spec, index: index }
|
48
|
+
@name_dict[spec.name] = { item: spec, index: index }
|
49
49
|
end
|
50
50
|
private :push_spec_items
|
51
51
|
|
52
|
+
def check_duplicate_key(num, name)
|
53
|
+
msg = nil
|
54
|
+
msg = "duplicated number spec item '#{num}'" if @number_dict.key?(num)
|
55
|
+
msg = "duplicated name spec item '#{name}'" if @name_dict.key?(name)
|
56
|
+
|
57
|
+
raise DuplicatedInputSpecItemError, msg unless msg.nil?
|
58
|
+
end
|
59
|
+
private :check_duplicate_key
|
60
|
+
|
52
61
|
def source=(source)
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
62
|
+
msg = 'source should respond to :[] method'
|
63
|
+
raise ArgumentError, msg unless source.respond_to? :[]
|
64
|
+
|
65
|
+
@source = source
|
58
66
|
end
|
59
67
|
|
60
68
|
def clear_source
|
@@ -62,104 +70,91 @@ module Anodator
|
|
62
70
|
end
|
63
71
|
|
64
72
|
def value_at(index)
|
65
|
-
raise SourceDataNotProvidedError
|
73
|
+
raise SourceDataNotProvidedError if @source.nil?
|
74
|
+
msg = "accessed by index '#{index}'"
|
75
|
+
raise UnknownTargetExpressionError, msg if @spec_items[index].nil?
|
66
76
|
|
67
|
-
|
68
|
-
raise UnknownTargetExpressionError.new("accessed by index '#{index}'")
|
69
|
-
else
|
70
|
-
return @source[index].to_s
|
71
|
-
end
|
77
|
+
@source[index].to_s
|
72
78
|
end
|
73
79
|
|
74
80
|
def value_at_by_number(number)
|
75
|
-
raise SourceDataNotProvidedError
|
81
|
+
raise SourceDataNotProvidedError if @source.nil?
|
82
|
+
msg = "accessed by number '#{number}'"
|
83
|
+
raise UnknownTargetExpressionError, msg unless @number_dict.key?(number)
|
76
84
|
|
77
|
-
|
78
|
-
return value_at(@number_dict[number][:index])
|
79
|
-
else
|
80
|
-
raise UnknownTargetExpressionError.new("accessed by number '#{number}'")
|
81
|
-
end
|
85
|
+
value_at(@number_dict[number][:index])
|
82
86
|
end
|
83
87
|
|
84
88
|
def value_at_by_name(name)
|
85
|
-
raise SourceDataNotProvidedError
|
89
|
+
raise SourceDataNotProvidedError if @source.nil?
|
90
|
+
msg = "accessed by name '#{name}'"
|
91
|
+
raise UnknownTargetExpressionError, msg unless @name_dict.key?(name)
|
86
92
|
|
87
|
-
|
88
|
-
return value_at(@name_dict[name][:index])
|
89
|
-
else
|
90
|
-
raise UnknownTargetExpressionError.new("accessed by name '#{name}'")
|
91
|
-
end
|
93
|
+
value_at(@name_dict[name][:index])
|
92
94
|
end
|
93
95
|
|
94
96
|
def value_by_calculation(calculation)
|
95
97
|
holders = check_calculation_expression(calculation)
|
96
|
-
|
97
|
-
|
98
|
-
value = self[expression]
|
99
|
-
spec = spec_item_by_expression(expression)
|
100
|
-
|
101
|
-
case spec.type
|
102
|
-
when InputSpecItem::TYPE_NUMERIC
|
103
|
-
hash["[[#{expression}]]"] = %Q|BigDecimal("#{value}")|
|
104
|
-
else # String, other
|
105
|
-
hash["[[#{expression}]]"] = %Q|"#{value}"|
|
106
|
-
end
|
107
|
-
next hash
|
108
|
-
end
|
109
|
-
|
110
|
-
calculation.gsub!(CALCULATION_HOLDER_REGEXP) do |match|
|
111
|
-
values[match]
|
112
|
-
end
|
98
|
+
values = convert_values_from_holders(holders)
|
99
|
+
calculation.gsub!(CALCULATION_HOLDER_REGEXP) { |m| values[m] }
|
113
100
|
|
114
101
|
value = eval(calculation)
|
115
|
-
value = value.to_s(
|
102
|
+
value = value.to_s('F') if value.is_a? BigDecimal
|
116
103
|
|
117
104
|
return value
|
118
|
-
rescue
|
119
|
-
|
120
|
-
|
121
|
-
raise UnknownTargetExpressionError.new("accessed by calculation '#{calculation}'")
|
105
|
+
rescue StandardError
|
106
|
+
msg = "accessed by calculation '#{calculation}'"
|
107
|
+
raise UnknownTargetExpressionError, msg
|
122
108
|
end
|
123
109
|
private :value_by_calculation
|
124
110
|
|
111
|
+
def convert_values_from_holders(holders)
|
112
|
+
holders.each_with_object({}) do |expression, hash|
|
113
|
+
value = self[expression]
|
114
|
+
spec = spec_item_by_expression(expression)
|
115
|
+
|
116
|
+
hash["[[#{expression}]]"] =
|
117
|
+
if spec.type == InputSpecItem::TYPE_NUMERIC
|
118
|
+
%|BigDecimal("#{value}")|
|
119
|
+
else
|
120
|
+
%("#{value}")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
private :convert_values_from_holders
|
125
|
+
|
125
126
|
def [](target_expression)
|
126
|
-
raise SourceDataNotProvidedError
|
127
|
+
raise SourceDataNotProvidedError if @source.nil?
|
128
|
+
return value_at(target_expression) if target_expression.is_a? Integer
|
127
129
|
|
128
|
-
if target_expression
|
129
|
-
return
|
130
|
-
elsif /^CALC::(.+)$/.match target_expression
|
131
|
-
return value_by_calculation($1)
|
132
|
-
else
|
133
|
-
begin
|
134
|
-
return value_at_by_number(target_expression)
|
135
|
-
rescue UnknownTargetExpressionError
|
136
|
-
return value_at_by_name(target_expression)
|
137
|
-
end
|
130
|
+
if target_expression =~ /^CALC::(.+)$/
|
131
|
+
return value_by_calculation(Regexp.last_match(1))
|
138
132
|
end
|
133
|
+
|
134
|
+
value_at_by_number(target_expression)
|
135
|
+
rescue UnknownTargetExpressionError
|
136
|
+
value_at_by_name(target_expression)
|
139
137
|
end
|
140
138
|
|
141
139
|
def spec_item_at(index)
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
end
|
140
|
+
msg = "accessed by index '#{index}'"
|
141
|
+
raise UnknownTargetExpressionError, msg if @spec_items[index].nil?
|
142
|
+
|
143
|
+
@spec_items[index].dup
|
147
144
|
end
|
148
145
|
|
149
146
|
def spec_item_at_by_number(number)
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
end
|
147
|
+
msg = "accessed by number '#{number}'"
|
148
|
+
raise UnknownTargetExpressionError, msg unless @number_dict.key?(number)
|
149
|
+
|
150
|
+
@number_dict[number][:item].dup
|
155
151
|
end
|
156
152
|
|
157
153
|
def spec_item_at_by_name(name)
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
end
|
154
|
+
msg = "accessed by name '#{name}'"
|
155
|
+
raise UnknownTargetExpressionError, msg unless @name_dict.key?(name)
|
156
|
+
|
157
|
+
@name_dict[name][:item].dup
|
163
158
|
end
|
164
159
|
|
165
160
|
def spec_items_by_calculation(calculation)
|
@@ -171,26 +166,25 @@ module Anodator
|
|
171
166
|
private :spec_items_by_calculation
|
172
167
|
|
173
168
|
def spec_item_by_expression(target_expression)
|
174
|
-
if target_expression.is_a? Integer
|
175
|
-
|
176
|
-
|
177
|
-
return spec_items_by_calculation(
|
178
|
-
else
|
179
|
-
begin
|
180
|
-
return spec_item_at_by_number(target_expression)
|
181
|
-
rescue UnknownTargetExpressionError
|
182
|
-
return spec_item_at_by_name(target_expression)
|
183
|
-
end
|
169
|
+
return spec_item_at(target_expression) if target_expression.is_a? Integer
|
170
|
+
|
171
|
+
if target_expression =~ /^CALC::(.+)$/
|
172
|
+
return spec_items_by_calculation(Regexp.last_match(1))
|
184
173
|
end
|
174
|
+
|
175
|
+
return spec_item_at_by_number(target_expression)
|
176
|
+
rescue UnknownTargetExpressionError
|
177
|
+
return spec_item_at_by_name(target_expression)
|
185
178
|
end
|
186
179
|
|
187
180
|
# return all holders specs
|
188
181
|
def check_calculation_expression(calculation)
|
189
|
-
if /(@|require|load|;)
|
190
|
-
|
182
|
+
if calculation =~ /(@|require|load|;)/
|
183
|
+
raise ArgumentError, "Invalid calcuation expression '#{calcuation}'"
|
191
184
|
end
|
192
185
|
|
193
|
-
calculation.scan(CALCULATION_HOLDER_REGEXP)
|
186
|
+
calculation.scan(CALCULATION_HOLDER_REGEXP)
|
187
|
+
.flatten.map do |target_expression|
|
194
188
|
spec_item_by_expression(target_expression)
|
195
189
|
next target_expression
|
196
190
|
end
|