levels 0.1.0
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.
- data/.gitignore +17 -0
- data/.rbenv-version +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +12 -0
- data/Guardfile +14 -0
- data/LICENSE +22 -0
- data/README.md +315 -0
- data/Rakefile +28 -0
- data/bin/levels +130 -0
- data/examples/01_base.rb +6 -0
- data/examples/01_merge_to_json.sh +27 -0
- data/examples/01_prod.json +8 -0
- data/examples/02_base.rb +4 -0
- data/examples/02_merge_with_file.sh +20 -0
- data/examples/02_value +1 -0
- data/levels.gemspec +20 -0
- data/lib/levels.rb +77 -0
- data/lib/levels/audit.rb +24 -0
- data/lib/levels/audit/group_observer.rb +26 -0
- data/lib/levels/audit/nested_group_observer.rb +37 -0
- data/lib/levels/audit/root_observer.rb +63 -0
- data/lib/levels/audit/value.rb +64 -0
- data/lib/levels/audit/value_observer.rb +46 -0
- data/lib/levels/audit/values.rb +66 -0
- data/lib/levels/configuration.rb +98 -0
- data/lib/levels/configured_group.rb +62 -0
- data/lib/levels/event_handler.rb +127 -0
- data/lib/levels/group.rb +61 -0
- data/lib/levels/input/json.rb +17 -0
- data/lib/levels/input/ruby.rb +120 -0
- data/lib/levels/input/system.rb +63 -0
- data/lib/levels/input/yaml.rb +17 -0
- data/lib/levels/key.rb +28 -0
- data/lib/levels/key_values.rb +54 -0
- data/lib/levels/lazy_evaluator.rb +54 -0
- data/lib/levels/level.rb +80 -0
- data/lib/levels/method_missing.rb +14 -0
- data/lib/levels/output/json.rb +33 -0
- data/lib/levels/output/system.rb +29 -0
- data/lib/levels/output/yaml.rb +19 -0
- data/lib/levels/runtime.rb +30 -0
- data/lib/levels/setup.rb +132 -0
- data/lib/levels/system/constants.rb +8 -0
- data/lib/levels/system/key_formatter.rb +15 -0
- data/lib/levels/system/key_generator.rb +50 -0
- data/lib/levels/system/key_parser.rb +67 -0
- data/lib/levels/version.rb +3 -0
- data/test/acceptance/audit_test.rb +105 -0
- data/test/acceptance/event_handler_test.rb +43 -0
- data/test/acceptance/read_json_test.rb +35 -0
- data/test/acceptance/read_ruby_test.rb +117 -0
- data/test/acceptance/read_system_test.rb +121 -0
- data/test/acceptance/read_yaml_test.rb +38 -0
- data/test/acceptance/setup_test.rb +115 -0
- data/test/acceptance/write_json_test.rb +39 -0
- data/test/acceptance/write_system_test.rb +68 -0
- data/test/acceptance/write_yaml_test.rb +33 -0
- data/test/bin/merge_test.rb +194 -0
- data/test/bin/options_test.rb +41 -0
- data/test/helper.rb +12 -0
- data/test/support/acceptance_spec.rb +58 -0
- data/test/support/base_spec.rb +14 -0
- data/test/support/bin_spec.rb +65 -0
- data/test/support/tempfile_helper.rb +35 -0
- data/test/unit/audit/group_observer_test.rb +24 -0
- data/test/unit/audit/nested_group_observer_test.rb +28 -0
- data/test/unit/audit/root_observer_test.rb +54 -0
- data/test/unit/audit/value_observer_test.rb +63 -0
- data/test/unit/audit/value_test.rb +41 -0
- data/test/unit/audit/values_test.rb +86 -0
- data/test/unit/configuration_test.rb +72 -0
- data/test/unit/configured_group_test.rb +75 -0
- data/test/unit/group_test.rb +105 -0
- data/test/unit/input/json_test.rb +32 -0
- data/test/unit/input/ruby_test.rb +140 -0
- data/test/unit/input/system_test.rb +59 -0
- data/test/unit/input/yaml_test.rb +33 -0
- data/test/unit/key_test.rb +45 -0
- data/test/unit/key_values_test.rb +106 -0
- data/test/unit/lazy_evaluator_test.rb +38 -0
- data/test/unit/level_test.rb +89 -0
- data/test/unit/levels_test.rb +23 -0
- data/test/unit/output/json_test.rb +55 -0
- data/test/unit/output/system_test.rb +32 -0
- data/test/unit/output/yaml_test.rb +38 -0
- data/test/unit/runtime_test.rb +40 -0
- data/test/unit/system/key_formatter_test.rb +43 -0
- data/test/unit/system/key_generator_test.rb +21 -0
- data/test/unit/system/key_parser_test.rb +207 -0
- metadata +215 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Levels::Audit::Values do
|
4
|
+
|
5
|
+
let(:group_key) { :group }
|
6
|
+
let(:value_key) { :value }
|
7
|
+
let(:values) { [] }
|
8
|
+
|
9
|
+
subject { Levels::Audit::Values.new(group_key, value_key, values) }
|
10
|
+
|
11
|
+
it "exposes the group_key" do
|
12
|
+
subject.group_key.must_equal group_key
|
13
|
+
end
|
14
|
+
|
15
|
+
it "exposes the value_key" do
|
16
|
+
subject.value_key.must_equal value_key
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "#final" do
|
20
|
+
v1 = Levels::Audit::Value.new("a", false)
|
21
|
+
v2 = Levels::Audit::Value.new("a", true)
|
22
|
+
|
23
|
+
values.concat [v1, v2]
|
24
|
+
|
25
|
+
subject.final.must_equal v2
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "#final_value" do
|
29
|
+
v1 = Levels::Audit::Value.new("a", false, "no")
|
30
|
+
v2 = Levels::Audit::Value.new("a", true, "hello")
|
31
|
+
|
32
|
+
values.concat [v1, v2]
|
33
|
+
|
34
|
+
subject.final_value.must_equal "hello"
|
35
|
+
end
|
36
|
+
|
37
|
+
specify "#each is Enumerable" do
|
38
|
+
subject.map.must_be_instance_of Enumerator
|
39
|
+
end
|
40
|
+
|
41
|
+
specify "#only_final?" do
|
42
|
+
v1 = MiniTest::Mock.new
|
43
|
+
v1.expect(:final?, false)
|
44
|
+
v2 = MiniTest::Mock.new
|
45
|
+
v2.expect(:final?, true)
|
46
|
+
|
47
|
+
subject.wont_be :only_final?
|
48
|
+
|
49
|
+
values << v2
|
50
|
+
subject.must_be :only_final?
|
51
|
+
|
52
|
+
values << v1
|
53
|
+
subject.wont_be :only_final?
|
54
|
+
end
|
55
|
+
|
56
|
+
specify "#recursive?" do
|
57
|
+
v1 = MiniTest::Mock.new
|
58
|
+
2.times { v1.expect(:recursive?, false) }
|
59
|
+
v2 = MiniTest::Mock.new
|
60
|
+
v2.expect(:recursive?, true)
|
61
|
+
|
62
|
+
subject.wont_be :recursive?
|
63
|
+
|
64
|
+
values << v1
|
65
|
+
subject.wont_be :recursive?
|
66
|
+
|
67
|
+
values << v2
|
68
|
+
subject.must_be :recursive?
|
69
|
+
end
|
70
|
+
|
71
|
+
specify "#size" do
|
72
|
+
values.size.must_equal 0
|
73
|
+
|
74
|
+
values << MiniTest::Mock.new
|
75
|
+
|
76
|
+
values.size.must_equal 1
|
77
|
+
end
|
78
|
+
|
79
|
+
specify "#empty?" do
|
80
|
+
values.must_be :empty?
|
81
|
+
|
82
|
+
values << MiniTest::Mock.new
|
83
|
+
|
84
|
+
values.wont_be :empty?
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Levels::Configuration do
|
4
|
+
|
5
|
+
let(:level1) { Levels::Level.new("l1") }
|
6
|
+
let(:level2) { Levels::Level.new("l2") }
|
7
|
+
|
8
|
+
subject { Levels::Configuration.new([level1, level2]) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
level1.set_group(:g1, a: 1)
|
12
|
+
|
13
|
+
level2.set_group(:g1, a: 9)
|
14
|
+
level2.set_group(:g2, b: 2)
|
15
|
+
end
|
16
|
+
|
17
|
+
specify "#to_s" do
|
18
|
+
subject.to_s.must_equal "<Levels::Configuration l1, l2>"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "allows groups to be retrieved" do
|
22
|
+
subject.g1.must_be_instance_of Levels::ConfiguredGroup
|
23
|
+
subject[:g1].must_be_instance_of Levels::ConfiguredGroup
|
24
|
+
end
|
25
|
+
|
26
|
+
it "initializes the configured group with the right levels" do
|
27
|
+
subject.g1.a.must_equal 9
|
28
|
+
subject.g1.b?.must_equal false
|
29
|
+
|
30
|
+
subject.g2.b.must_equal 2
|
31
|
+
subject.g2.a?.must_equal false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "raises an error if you access an unknown group" do
|
35
|
+
proc { subject.nothing }.must_raise Levels::UnknownGroup
|
36
|
+
proc { subject[:nothing] }.must_raise Levels::UnknownGroup
|
37
|
+
end
|
38
|
+
|
39
|
+
it "allows the existence of a group to be tested" do
|
40
|
+
subject.defined?(:g1).must_equal true
|
41
|
+
subject.defined?(:g2).must_equal true
|
42
|
+
subject.defined?(:foo).must_equal false
|
43
|
+
subject.g1?.must_equal true
|
44
|
+
subject.g2?.must_equal true
|
45
|
+
subject.foo?.must_equal false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "allows lazy evaluation" do
|
49
|
+
level2.set_group(:g3, sum: -> { g1.a + g2.b })
|
50
|
+
subject.g3.sum.must_equal 11
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#to_enum" do
|
54
|
+
|
55
|
+
it "returns an Enumerator" do
|
56
|
+
subject.to_enum.must_be_instance_of Enumerator
|
57
|
+
end
|
58
|
+
|
59
|
+
it "iterates over all groups" do
|
60
|
+
result = subject.to_enum.map do |k, v|
|
61
|
+
[k.to_sym, v.class]
|
62
|
+
end
|
63
|
+
expected = [
|
64
|
+
[:g1, Enumerator],
|
65
|
+
[:g2, Enumerator]
|
66
|
+
]
|
67
|
+
result.sort.must_equal expected.sort
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Levels::ConfiguredGroup do
|
4
|
+
|
5
|
+
let(:levels) { [] }
|
6
|
+
let(:group_observer) { MiniTest::Mock.new }
|
7
|
+
|
8
|
+
subject { Levels::ConfiguredGroup.new(levels, :test, group_observer) }
|
9
|
+
|
10
|
+
def define_value(key, value)
|
11
|
+
level = Levels::Level.new("one")
|
12
|
+
level.set_group(:test, key => value)
|
13
|
+
levels << level
|
14
|
+
end
|
15
|
+
|
16
|
+
def observe_value(key, value)
|
17
|
+
observed_values = MiniTest::Mock.new
|
18
|
+
observed_values.expect(:final_value, value)
|
19
|
+
group_observer.expect(:observe_values, observed_values, [levels, :test, key])
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "checking value existence" do
|
23
|
+
|
24
|
+
before do
|
25
|
+
define_value :a, nil
|
26
|
+
end
|
27
|
+
|
28
|
+
it "allows defined?" do
|
29
|
+
subject.defined?(:a).must_equal true
|
30
|
+
end
|
31
|
+
|
32
|
+
it "allows method access" do
|
33
|
+
subject.a?.must_equal true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "reading values" do
|
38
|
+
|
39
|
+
before do
|
40
|
+
define_value :a, nil
|
41
|
+
observe_value :a, 1
|
42
|
+
end
|
43
|
+
|
44
|
+
it "allows hash access to any key" do
|
45
|
+
subject[:a].must_equal 1
|
46
|
+
end
|
47
|
+
|
48
|
+
it "allows method access" do
|
49
|
+
subject.a.must_equal 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "#to_enum" do
|
54
|
+
|
55
|
+
before do
|
56
|
+
define_value :a, nil
|
57
|
+
observe_value :a, 1
|
58
|
+
end
|
59
|
+
|
60
|
+
it "returns an Enumerator" do
|
61
|
+
subject.to_enum.must_be_instance_of Enumerator
|
62
|
+
end
|
63
|
+
|
64
|
+
it "iterates over all keys and values" do
|
65
|
+
result = subject.to_enum.map do |k, v|
|
66
|
+
[k.to_sym, v]
|
67
|
+
end
|
68
|
+
expected = [
|
69
|
+
[:a, 1],
|
70
|
+
]
|
71
|
+
result.sort.must_equal expected.sort
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Levels::Group do
|
4
|
+
|
5
|
+
let(:hash) { {} }
|
6
|
+
let(:value_transformer) { nil }
|
7
|
+
|
8
|
+
subject { Levels::Group.new(hash, value_transformer) }
|
9
|
+
|
10
|
+
before do
|
11
|
+
hash[:name] = "ok"
|
12
|
+
hash[:value] = 123
|
13
|
+
hash[:other] = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
specify "#to_s" do
|
17
|
+
subject.to_s.must_equal "<Levels::Group>"
|
18
|
+
end
|
19
|
+
|
20
|
+
it "allows hash access" do
|
21
|
+
subject[:name].must_equal "ok"
|
22
|
+
subject[:value].must_equal 123
|
23
|
+
subject[:other].must_equal nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it "allows dot access" do
|
27
|
+
subject.name.must_equal "ok"
|
28
|
+
subject.value.must_equal 123
|
29
|
+
subject.other.must_equal nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises an error if you access a nonexistent key" do
|
33
|
+
proc { subject[:foo] }.must_raise Levels::UnknownKey
|
34
|
+
proc { subject.foo }.must_raise Levels::UnknownKey
|
35
|
+
end
|
36
|
+
|
37
|
+
it "allows the existence of a key to be tested" do
|
38
|
+
subject.defined?(:name).must_equal true
|
39
|
+
subject.defined?(:foo).must_equal false
|
40
|
+
subject.name?.must_equal true
|
41
|
+
subject.foo?.must_equal false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "makes sure you don't call it wrong" do
|
45
|
+
proc { subject.name("ok") }.must_raise ArgumentError
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "initialized with string keys" do
|
49
|
+
|
50
|
+
it "converts everything to symbols" do
|
51
|
+
hash["string"] = 123
|
52
|
+
subject.string.must_equal 123
|
53
|
+
subject[:string].must_equal 123
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "initialized with a value transformer" do
|
58
|
+
|
59
|
+
let(:value_transformer) {
|
60
|
+
-> key, value { [key, value] }
|
61
|
+
}
|
62
|
+
|
63
|
+
it "returns the value via the transformer" do
|
64
|
+
subject.name.must_equal [:name, "ok"]
|
65
|
+
subject[:name].must_equal [:name, "ok"]
|
66
|
+
subject["name"].must_equal [:name, "ok"]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#to_enum" do
|
71
|
+
|
72
|
+
it "returns an Enumerator" do
|
73
|
+
subject.to_enum.must_be_instance_of Enumerator
|
74
|
+
end
|
75
|
+
|
76
|
+
it "iterates over all keys and values" do
|
77
|
+
result = subject.to_enum.map do |k, v|
|
78
|
+
[k, v]
|
79
|
+
end
|
80
|
+
expected = [
|
81
|
+
[:name, "ok"],
|
82
|
+
[:value, 123],
|
83
|
+
[:other, nil]
|
84
|
+
]
|
85
|
+
result.sort.must_equal expected.sort
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "#eql_hash?" do
|
90
|
+
|
91
|
+
let(:expected) {
|
92
|
+
{
|
93
|
+
name: "ok",
|
94
|
+
value: 123,
|
95
|
+
"other" => nil
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
it "is true if the hash matches the data" do
|
100
|
+
subject.eql_hash?(expected).must_equal true
|
101
|
+
subject.eql_hash?(a: 1).must_equal false
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Levels::Input::JSON do
|
4
|
+
|
5
|
+
let(:json_string) {
|
6
|
+
<<-STR
|
7
|
+
{
|
8
|
+
"group1": {
|
9
|
+
"key1": "string",
|
10
|
+
"key2": 123
|
11
|
+
},
|
12
|
+
"group2": {
|
13
|
+
"key": [1, 2, 3]
|
14
|
+
}
|
15
|
+
}
|
16
|
+
STR
|
17
|
+
}
|
18
|
+
|
19
|
+
subject { Levels::Input::JSON.new(json_string) }
|
20
|
+
|
21
|
+
it "reads data from the JSON structure" do
|
22
|
+
assert_input_equals_hash(
|
23
|
+
group1: {
|
24
|
+
key1: "string",
|
25
|
+
key2: 123
|
26
|
+
},
|
27
|
+
group2: {
|
28
|
+
key: [1, 2, 3]
|
29
|
+
}
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Levels::Input::Ruby do
|
4
|
+
|
5
|
+
let(:level) { Levels::Level.new("Ruby") }
|
6
|
+
|
7
|
+
subject { Levels::Input::Ruby.new(ruby_string, "file.rb", 1) }
|
8
|
+
|
9
|
+
describe "successfully" do
|
10
|
+
|
11
|
+
let(:ruby_string) {
|
12
|
+
<<-RUBY
|
13
|
+
group :foo
|
14
|
+
set value: "ok"
|
15
|
+
RUBY
|
16
|
+
}
|
17
|
+
|
18
|
+
it "populates the level" do
|
19
|
+
subject.read(level)
|
20
|
+
level.eql_hash?(
|
21
|
+
foo: {
|
22
|
+
value: "ok"
|
23
|
+
}
|
24
|
+
).must_equal true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "with an error" do
|
29
|
+
|
30
|
+
let(:ruby_string) {
|
31
|
+
<<-RUBY
|
32
|
+
xgroup :foo
|
33
|
+
set value: "ok"
|
34
|
+
RUBY
|
35
|
+
}
|
36
|
+
|
37
|
+
it "provides useful information" do
|
38
|
+
begin
|
39
|
+
subject.read(level)
|
40
|
+
rescue => e
|
41
|
+
e.class.must_equal NoMethodError
|
42
|
+
e.message.must_equal %(undefined method `xgroup' for <Levels>:Levels::Input::Ruby::DSL)
|
43
|
+
e.backtrace.first.must_equal "file.rb:1:in `read'"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe Levels::Input::Ruby::DSL do
|
50
|
+
|
51
|
+
let(:level) { Levels::Level.new("Ruby") }
|
52
|
+
|
53
|
+
subject { Levels::Input::Ruby::DSL.new(level) }
|
54
|
+
|
55
|
+
def define(&block)
|
56
|
+
subject.instance_eval(&block)
|
57
|
+
subject.close_current_group
|
58
|
+
end
|
59
|
+
|
60
|
+
it "defines an empty group" do
|
61
|
+
define do
|
62
|
+
group :one
|
63
|
+
end
|
64
|
+
level.defined?(:one).must_equal true
|
65
|
+
end
|
66
|
+
|
67
|
+
it "defines a group with keys" do
|
68
|
+
define do
|
69
|
+
group :one
|
70
|
+
set key: "value"
|
71
|
+
end
|
72
|
+
level.one.key.must_equal "value"
|
73
|
+
end
|
74
|
+
|
75
|
+
it "is an error to set a key outside of a group" do
|
76
|
+
-> {
|
77
|
+
define do
|
78
|
+
set key: "value"
|
79
|
+
end
|
80
|
+
}.must_raise(SyntaxError)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "is an error to set a key more than once" do
|
84
|
+
-> {
|
85
|
+
define do
|
86
|
+
group :one
|
87
|
+
set key: "value"
|
88
|
+
set "key" => "value"
|
89
|
+
end
|
90
|
+
}.must_raise(RuntimeError)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "is an error to define a group more than once" do
|
94
|
+
-> {
|
95
|
+
subject.group :one
|
96
|
+
subject.close_current_group
|
97
|
+
subject.group "one"
|
98
|
+
}.must_raise(RuntimeError)
|
99
|
+
end
|
100
|
+
|
101
|
+
describe "the ways to set a value" do
|
102
|
+
|
103
|
+
before do
|
104
|
+
subject.group :one
|
105
|
+
end
|
106
|
+
|
107
|
+
def assert_key
|
108
|
+
subject.close_current_group
|
109
|
+
level.one.key.must_equal "value"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "can be set as a ruby 1.9 hash" do
|
113
|
+
subject.set key: "value"
|
114
|
+
assert_key
|
115
|
+
end
|
116
|
+
|
117
|
+
it "can be set as a ruby 1.8 hash" do
|
118
|
+
subject.set :key => "value"
|
119
|
+
assert_key
|
120
|
+
end
|
121
|
+
|
122
|
+
it "is possible to set multiple keys via a hash" do
|
123
|
+
subject.set :key => "value", :bar => "foo"
|
124
|
+
assert_key
|
125
|
+
level.one.bar.must_equal "foo"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "can be set with two arguments" do
|
129
|
+
subject.set :key, "value"
|
130
|
+
assert_key
|
131
|
+
end
|
132
|
+
|
133
|
+
it "is an error to pass other types of args" do
|
134
|
+
-> { subject.set :a }.must_raise(ArgumentError)
|
135
|
+
-> { subject.set :a, 1, :b }.must_raise(ArgumentError)
|
136
|
+
-> { subject.set :a, 1, :b, 2 }.must_raise(ArgumentError)
|
137
|
+
-> { subject.set :b, :a => 1 }.must_raise(ArgumentError)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|