lenjador 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +8 -0
- data/README.md +121 -0
- data/Rakefile +7 -0
- data/benchmark/whitelisting.rb +53 -0
- data/lenjador.gemspec +36 -0
- data/lib/lenjador/adapters/stdout_adapter.rb +29 -0
- data/lib/lenjador/adapters/stdout_json_adapter.rb +60 -0
- data/lib/lenjador/adapters.rb +22 -0
- data/lib/lenjador/null_logger.rb +18 -0
- data/lib/lenjador/preprocessors/blacklist.rb +61 -0
- data/lib/lenjador/preprocessors/json_pointer_trie.rb +40 -0
- data/lib/lenjador/preprocessors/strategies/mask.rb +43 -0
- data/lib/lenjador/preprocessors/strategies/prune.rb +44 -0
- data/lib/lenjador/preprocessors/whitelist.rb +56 -0
- data/lib/lenjador/preprocessors.rb +18 -0
- data/lib/lenjador/utils.rb +91 -0
- data/lib/lenjador.rb +74 -0
- data/spec/adapters/stdout_adapter_spec.rb +48 -0
- data/spec/adapters/stdout_json_adapter_spec.rb +62 -0
- data/spec/lenjador_spec.rb +142 -0
- data/spec/preprocessors/blacklist_spec.rb +131 -0
- data/spec/preprocessors/json_pointer_trie.rb +36 -0
- data/spec/preprocessors/whitelist_spec.rb +335 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/support/implement_interface.rb +11 -0
- data/spec/utils_spec.rb +84 -0
- metadata +168 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class Lenjador
|
4
|
+
module Utils
|
5
|
+
DECIMAL_FRACTION_OF_SECOND = 3
|
6
|
+
|
7
|
+
# Build logstash json compatible event
|
8
|
+
#
|
9
|
+
# @param [Hash] metadata
|
10
|
+
# @param [#to_s] level
|
11
|
+
# @param [String] service_name
|
12
|
+
#
|
13
|
+
# @return [Hash]
|
14
|
+
def self.build_event(metadata, level, application_name)
|
15
|
+
overwritable_params
|
16
|
+
.merge(metadata)
|
17
|
+
.merge(
|
18
|
+
application: application_name,
|
19
|
+
level: level
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return application name
|
24
|
+
#
|
25
|
+
# Returns lower snake case application name. This allows the
|
26
|
+
# application value to be used in the elasticsearch index name.
|
27
|
+
#
|
28
|
+
# @param [String] service_name
|
29
|
+
#
|
30
|
+
# @return [String]
|
31
|
+
def self.application_name(service_name)
|
32
|
+
underscore(service_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.overwritable_params
|
36
|
+
{
|
37
|
+
:@timestamp => Time.now.utc.iso8601(DECIMAL_FRACTION_OF_SECOND)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.serialize_time_objects!(object)
|
42
|
+
if object.is_a?(Hash)
|
43
|
+
object.each do |key, value|
|
44
|
+
object[key] = serialize_time_objects!(value)
|
45
|
+
end
|
46
|
+
elsif object.is_a?(Array)
|
47
|
+
object.each_index do |index|
|
48
|
+
object[index] = serialize_time_objects!(object[index])
|
49
|
+
end
|
50
|
+
elsif object.is_a?(Time) || object.is_a?(Date)
|
51
|
+
object.iso8601
|
52
|
+
else
|
53
|
+
object
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
if RUBY_PLATFORM =~ /java/
|
58
|
+
require 'jrjackson'
|
59
|
+
|
60
|
+
DUMP_OPTIONS = {
|
61
|
+
timezone: 'utc',
|
62
|
+
date_format: "YYYY-MM-dd'T'HH:mm:ss.SSSX"
|
63
|
+
}.freeze
|
64
|
+
|
65
|
+
def self.generate_json(obj)
|
66
|
+
JrJackson::Json.dump(obj, DUMP_OPTIONS)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
require 'oj'
|
70
|
+
DUMP_OPTIONS = { mode: :compat, time_format: :ruby }.freeze
|
71
|
+
|
72
|
+
def self.generate_json(obj)
|
73
|
+
serialize_time_objects!(obj)
|
74
|
+
|
75
|
+
Oj.dump(obj, DUMP_OPTIONS)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.underscore(input)
|
80
|
+
word = input.to_s.dup
|
81
|
+
word.gsub!(/::/, '/')
|
82
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
83
|
+
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
84
|
+
word.tr!("-", "_")
|
85
|
+
word.downcase!
|
86
|
+
word
|
87
|
+
end
|
88
|
+
|
89
|
+
private_class_method :overwritable_params
|
90
|
+
end
|
91
|
+
end
|
data/lib/lenjador.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require_relative 'lenjador/adapters'
|
3
|
+
require_relative 'lenjador/utils'
|
4
|
+
require_relative 'lenjador/null_logger'
|
5
|
+
require_relative 'lenjador/preprocessors'
|
6
|
+
|
7
|
+
LOG_LEVEL_QUERY_METHODS = [:debug?, :info?, :warn?, :error?, :fatal?]
|
8
|
+
|
9
|
+
class Lenjador
|
10
|
+
def self.build(service_name, loggers_config, preprocessors_config = {})
|
11
|
+
loggers_config ||= {stdout: nil}
|
12
|
+
preprocessors = preprocessors_config.map do |type, arguments|
|
13
|
+
Preprocessors.get(type.to_s, arguments || {})
|
14
|
+
end
|
15
|
+
adapters = loggers_config.map do |type, arguments|
|
16
|
+
Adapters.get(type.to_s, service_name, arguments || {})
|
17
|
+
end
|
18
|
+
new(adapters, preprocessors)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(adapters, preprocessors)
|
22
|
+
@adapters = adapters
|
23
|
+
@preprocessors = preprocessors
|
24
|
+
end
|
25
|
+
|
26
|
+
def debug(*args, &block)
|
27
|
+
log :debug, *args, &block
|
28
|
+
end
|
29
|
+
|
30
|
+
def info(*args, &block)
|
31
|
+
log :info, *args, &block
|
32
|
+
end
|
33
|
+
|
34
|
+
def warn(*args, &block)
|
35
|
+
log :warn, *args, &block
|
36
|
+
end
|
37
|
+
|
38
|
+
def error(*args, &block)
|
39
|
+
log :error, *args, &block
|
40
|
+
end
|
41
|
+
|
42
|
+
def fatal(*args, &block)
|
43
|
+
log :fatal, *args, &block
|
44
|
+
end
|
45
|
+
|
46
|
+
LOG_LEVEL_QUERY_METHODS.each do |method|
|
47
|
+
define_method(method) do
|
48
|
+
@adapters.any? {|adapter| adapter.public_send(method) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def log(level, *args, &block)
|
55
|
+
data = parse_log_data(*args, &block)
|
56
|
+
processed_data = preprocess(data)
|
57
|
+
|
58
|
+
@adapters.each do |adapter|
|
59
|
+
adapter.log(level, processed_data)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def preprocess(data)
|
64
|
+
@preprocessors.inject(data) do |data_to_process, preprocessor|
|
65
|
+
preprocessor.process(data_to_process)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def parse_log_data(message = nil, metadata = {}, &block)
|
70
|
+
return message if message.is_a?(Hash)
|
71
|
+
|
72
|
+
(metadata || {}).merge(message: block ? block.call : message)
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../lib/lenjador/adapters/stdout_adapter'
|
3
|
+
|
4
|
+
describe Lenjador::Adapters::StdoutAdapter do
|
5
|
+
it 'creates a stdout logger' do
|
6
|
+
io_logger = described_class.new(0)
|
7
|
+
|
8
|
+
logger = io_logger.instance_variable_get(:@logger)
|
9
|
+
expect(logger).to be_a Logger
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#log' do
|
13
|
+
let(:adapter) { described_class.new(0) }
|
14
|
+
let(:logger) { adapter.logger }
|
15
|
+
|
16
|
+
context 'with only a message' do
|
17
|
+
it 'stringifies it correctly' do
|
18
|
+
expect(logger).to receive(:info).with('test')
|
19
|
+
|
20
|
+
adapter.log :info, message: 'test'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with an empty message' do
|
25
|
+
it 'stringifies it correctly' do
|
26
|
+
expect(logger).to receive(:info).with(' {"a":"b"}')
|
27
|
+
|
28
|
+
adapter.log :info, message: '', a: 'b'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'with no message' do
|
33
|
+
it 'stringifies it correctly' do
|
34
|
+
expect(logger).to receive(:info).with('{"a":"b"}')
|
35
|
+
|
36
|
+
adapter.log :info, a: 'b'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with a message and metadata' do
|
41
|
+
it 'stringifies it correctly' do
|
42
|
+
expect(logger).to receive(:info).with('test {"a":"b"}')
|
43
|
+
|
44
|
+
adapter.log :info, message: 'test', a: 'b'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
require_relative '../../lib/lenjador/adapters/stdout_json_adapter'
|
4
|
+
|
5
|
+
describe Lenjador::Adapters::StdoutJsonAdapter do
|
6
|
+
let(:debug_level_code) { 0 }
|
7
|
+
let(:debug_level) { Lenjador::Adapters::LOG_LEVELS[debug_level_code] }
|
8
|
+
let(:info_level_code) { 1 }
|
9
|
+
let(:info_level) { Lenjador::Adapters::LOG_LEVELS[info_level_code] }
|
10
|
+
|
11
|
+
let(:stdout) { StringIO.new }
|
12
|
+
|
13
|
+
around do |example|
|
14
|
+
old_stdout = $stdout
|
15
|
+
$stdout = stdout
|
16
|
+
|
17
|
+
begin
|
18
|
+
example.call
|
19
|
+
ensure
|
20
|
+
$stdout = old_stdout
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#log' do
|
25
|
+
context 'when below threshold' do
|
26
|
+
let(:adapter) { described_class.new(debug_level_code, service_name) }
|
27
|
+
let(:metadata) { {x: 'y'} }
|
28
|
+
let(:event) { {a: 'b', x: 'y'} }
|
29
|
+
let(:serialized_event) { JSON.dump(event) }
|
30
|
+
let(:service_name) { 'my-service' }
|
31
|
+
let(:application_name) { 'my_service' }
|
32
|
+
|
33
|
+
before do
|
34
|
+
allow(Lenjador::Utils).to receive(:build_event)
|
35
|
+
.with(metadata, info_level, application_name)
|
36
|
+
.and_return(event)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'sends serialized event to $stdout' do
|
40
|
+
adapter.log(info_level, metadata)
|
41
|
+
expect(output).to eq serialized_event + "\n"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when above threshold' do
|
46
|
+
let(:adapter) { described_class.new(info_level_code, service_name) }
|
47
|
+
let(:metadata) { {x: 'y'} }
|
48
|
+
let(:service_name) { 'my-service' }
|
49
|
+
|
50
|
+
it 'does not log the event' do
|
51
|
+
adapter.log(debug_level, metadata)
|
52
|
+
expect(output).to be_empty
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def output
|
60
|
+
stdout.string
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Lenjador do
|
4
|
+
describe '.build' do
|
5
|
+
it 'creates stdout logger' do
|
6
|
+
expect(described_class).to receive(:new) do |adapters|
|
7
|
+
expect(adapters.count).to be(1)
|
8
|
+
expect(adapters.first).to be_a(described_class::Adapters::StdoutAdapter)
|
9
|
+
end
|
10
|
+
|
11
|
+
described_class.build('test_service', stdout: nil)
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'creates stdout json logger' do
|
15
|
+
expect(described_class).to receive(:new) do |adapters|
|
16
|
+
expect(adapters.count).to be(1)
|
17
|
+
expect(adapters.first).to be_a(described_class::Adapters::StdoutJsonAdapter)
|
18
|
+
end
|
19
|
+
|
20
|
+
described_class.build('test_service', stdout: {json: true})
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'creates stdout logger when no loggers are specified' do
|
24
|
+
expect(described_class).to receive(:new) do |adapters|
|
25
|
+
expect(adapters.count).to be(1)
|
26
|
+
expect(adapters.first).to be_a(described_class::Adapters::StdoutAdapter)
|
27
|
+
end
|
28
|
+
|
29
|
+
described_class.build('test_service', nil)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'creates preprocessor when preprocessor defined' do
|
33
|
+
expect(described_class).to receive(:new) do |adapters, preprocessors|
|
34
|
+
expect(preprocessors.count).to be(1)
|
35
|
+
expect(preprocessors.first).to be_a(described_class::Preprocessors::Blacklist)
|
36
|
+
end
|
37
|
+
|
38
|
+
preprocessors = {blacklist: {fields: []}}
|
39
|
+
described_class.build('test_service', nil, preprocessors)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when preprocessor defined' do
|
44
|
+
let(:lenjador) { described_class.new([adapter], [preprocessor]) }
|
45
|
+
let(:adapter) { double }
|
46
|
+
let(:preprocessor) { double }
|
47
|
+
let(:data) { {data: 'data'} }
|
48
|
+
|
49
|
+
it 'preprocesses data before logging' do
|
50
|
+
expect(preprocessor).to receive(:process).with(data).and_return(data.merge(processed: true)).ordered
|
51
|
+
expect(adapter).to receive(:log).with(:info, data.merge(processed: true)).ordered
|
52
|
+
|
53
|
+
lenjador.info(data)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'when parsing log data' do
|
58
|
+
let(:lenjador) { described_class.new([adapter], preprocessors) }
|
59
|
+
let(:adapter) { double }
|
60
|
+
let(:preprocessors) { [] }
|
61
|
+
|
62
|
+
it 'parses empty string with nil metadata' do
|
63
|
+
expect(adapter).to receive(:log).with(:info, message: '')
|
64
|
+
|
65
|
+
lenjador.info('', nil)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'parses nil as metadata' do
|
69
|
+
expect(adapter).to receive(:log).with(:info, message: nil)
|
70
|
+
|
71
|
+
lenjador.info(nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'parses only message' do
|
75
|
+
expect(adapter).to receive(:log).with(:info, message: 'test message')
|
76
|
+
|
77
|
+
lenjador.info 'test message'
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'parses only metadata' do
|
81
|
+
expect(adapter).to receive(:log).with(:info, test: 'data')
|
82
|
+
|
83
|
+
lenjador.info test: 'data'
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'parses message and metadata' do
|
87
|
+
expect(adapter).to receive(:log).with(:info, message: 'test message', test: 'data')
|
88
|
+
|
89
|
+
lenjador.info 'test message', test: 'data'
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'parses block as a message' do
|
93
|
+
message = 'test message'
|
94
|
+
expect(adapter).to receive(:log).with(:info, message: message)
|
95
|
+
|
96
|
+
lenjador.info { message }
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'ignores progname on block syntax' do
|
100
|
+
message = 'test message'
|
101
|
+
expect(adapter).to receive(:log).with(:info, message: message)
|
102
|
+
|
103
|
+
lenjador.info('progname') { message }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'log level queries' do
|
108
|
+
context 'when adapter has debug level' do
|
109
|
+
let(:logger) do
|
110
|
+
described_class.build('test_service', stdout: {level: 'debug'})
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'responds true to debug? and higher levels' do
|
114
|
+
expect(logger.debug?).to be(true)
|
115
|
+
expect(logger.info?).to be(true)
|
116
|
+
expect(logger.warn?).to be(true)
|
117
|
+
expect(logger.error?).to be(true)
|
118
|
+
expect(logger.fatal?).to be(true)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'when adapter has info level' do
|
123
|
+
let(:logger) do
|
124
|
+
described_class.build('test_service', stdout: {level: 'info'})
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'responds true to info? and higher levels' do
|
128
|
+
expect(logger.debug?).to be(false)
|
129
|
+
expect(logger.info?).to be(true)
|
130
|
+
expect(logger.warn?).to be(true)
|
131
|
+
expect(logger.error?).to be(true)
|
132
|
+
expect(logger.fatal?).to be(true)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'has the same interface as Ruby logger' do
|
138
|
+
skip "https://salemove.atlassian.net/browse/INF-464"
|
139
|
+
logger = described_class.build('test_service', stdout: {level: 'debug'})
|
140
|
+
expect(logger).to implement_interface(Logger)
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../lib/lenjador/preprocessors/blacklist'
|
3
|
+
|
4
|
+
describe Lenjador::Preprocessors::Blacklist do
|
5
|
+
subject(:processed_data) { described_class.new(config).process(data) }
|
6
|
+
|
7
|
+
let(:config) {{
|
8
|
+
fields: [{ key: 'field', action: action }]
|
9
|
+
}}
|
10
|
+
|
11
|
+
let(:action) {}
|
12
|
+
let(:data) {{
|
13
|
+
field: value,
|
14
|
+
data: {
|
15
|
+
field: 'secret'
|
16
|
+
},
|
17
|
+
array: [{field: 'secret'}]
|
18
|
+
}}
|
19
|
+
|
20
|
+
let(:value) { 'secret' }
|
21
|
+
|
22
|
+
context 'when action is unsupported' do
|
23
|
+
let(:action) { 'reverse' }
|
24
|
+
|
25
|
+
it 'throws exception' do
|
26
|
+
expect { processed_data }.to raise_exception(Lenjador::Preprocessors::Blacklist::UnsupportedActionException)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'when action is "exclude"' do
|
31
|
+
let(:action) { 'exclude' }
|
32
|
+
|
33
|
+
it 'removes the field' do
|
34
|
+
expect(processed_data).not_to include(:field)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'removes nested field' do
|
38
|
+
expect(processed_data).not_to include_at_depth({field: 'secret'}, 1)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'removes nested in array field' do
|
42
|
+
expect(processed_data[:array]).not_to include({field: 'secret'})
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when field is deeply nested' do
|
46
|
+
let(:depth) { 10 }
|
47
|
+
let(:data) { data_with_nested_field({field: 'secret'}, depth) }
|
48
|
+
|
49
|
+
it 'removes deeply nested field' do
|
50
|
+
expect(processed_data).not_to include_at_depth({field: 'secret'}, depth)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when action is "mask"' do
|
56
|
+
let(:action) { 'mask' }
|
57
|
+
|
58
|
+
it 'masks nested field' do
|
59
|
+
expect(processed_data).to include_at_depth({field: '*****'}, 1)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'masks nested in array field' do
|
63
|
+
expect(processed_data[:array]).to include({field: '*****'})
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when field is string' do
|
67
|
+
let(:value) { 'secret' }
|
68
|
+
|
69
|
+
it 'masks value with asterisks' do
|
70
|
+
expect(processed_data).to include(field: '*****')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when field is number' do
|
75
|
+
let(:value) { 42 }
|
76
|
+
|
77
|
+
it 'masks number value' do
|
78
|
+
expect(processed_data).to include(field: '*****')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when field is boolean' do
|
83
|
+
let(:value) { true }
|
84
|
+
|
85
|
+
it 'masks value with asterisks' do
|
86
|
+
expect(processed_data).to include(field: '*****')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when field is array' do
|
91
|
+
let(:value) { [1,2,3,4] }
|
92
|
+
|
93
|
+
it 'masks value with asterisks' do
|
94
|
+
expect(processed_data).to include(field: '*****')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'when field is hash' do
|
99
|
+
let(:value) { {data: {}} }
|
100
|
+
|
101
|
+
it 'masks value with asterisks' do
|
102
|
+
expect(processed_data).to include(field: '*****')
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when field is deeply nested' do
|
107
|
+
let(:depth) { 10 }
|
108
|
+
let(:data) { data_with_nested_field({field: 'secret'}, depth) }
|
109
|
+
|
110
|
+
it 'masks deeply nested field' do
|
111
|
+
expect(processed_data).to include_at_depth({field: '*****'}, depth)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def data_with_nested_field(field, depth)
|
117
|
+
depth.times.inject(field) do |mem|
|
118
|
+
{}.merge(data: mem)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
RSpec::Matchers.define :include_at_depth do |expected_hash, depth|
|
123
|
+
match do |actual|
|
124
|
+
nested_data = depth.times.inject(actual) do |mem|
|
125
|
+
mem[:data]
|
126
|
+
end
|
127
|
+
|
128
|
+
expect(nested_data).to include(expected_hash)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lenjador/preprocessors/json_pointer_trie'
|
3
|
+
|
4
|
+
RSpec.describe Lenjador::Preprocessors::JSONPointerTrie do
|
5
|
+
let(:trie) { described_class.new }
|
6
|
+
|
7
|
+
describe '#includes?' do
|
8
|
+
it 'returns true for empty prefix' do
|
9
|
+
expect(trie).to include('')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'returns true if trie contains requested prefix or value itself' do
|
13
|
+
trie.insert('/data/nested/key')
|
14
|
+
|
15
|
+
expect(trie).to include('/data')
|
16
|
+
expect(trie).to include('/data/nested')
|
17
|
+
expect(trie).to include('/data/nested/key')
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns false if trie does not contain requested prefix or value' do
|
21
|
+
trie.insert('/data/nested/key')
|
22
|
+
|
23
|
+
expect(trie).to_not include('/bad_data')
|
24
|
+
expect(trie).to_not include('/data/bad_nested')
|
25
|
+
expect(trie).to_not include('/data/nested/bad_key')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'returns true if trie contains requested prefix under wildcard' do
|
29
|
+
trie.insert('/data/~/key')
|
30
|
+
|
31
|
+
expect(trie).to include('/data/arbitrary_key/key')
|
32
|
+
expect(trie).to include('/data/another_key/key')
|
33
|
+
expect(trie).to_not include('/data/arbitrary_key/bad_nested_key')
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|