anodator 1.0.0.pre1 → 1.0.0.pre2
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 +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
|