ruby-druid 0.1.8 → 0.1.9
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 +4 -4
- data/.rspec +2 -0
- data/.travis.yml +2 -3
- data/README.md +7 -1
- data/lib/druid/having.rb +30 -14
- data/lib/druid/post_aggregation.rb +46 -0
- data/lib/druid/query.rb +16 -1
- data/lib/druid/serializable.rb +19 -0
- data/ruby-druid.gemspec +1 -1
- data/spec/lib/client_spec.rb +0 -2
- data/spec/lib/query_spec.rb +118 -85
- data/spec/lib/zoo_handler_spec.rb +0 -2
- metadata +26 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d0e676ac15efc4d9777fa182d8c8855bc1ebf386
|
4
|
+
data.tar.gz: e54d2ec80e3bb5efdf02d3b5b127c565c5cfce86
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 884a621b0fb481490d114248bfff0207bc2fd54e889832cf86318c6d26fb53c73c535d97d4d832fc9872a4f6b76b0d426a7050190e41639abfad2d728a36dd7a
|
7
|
+
data.tar.gz: b46976c63af7169c6581d092522f5ac97098320efe903bd6d5a6e58c9c2792aab34d0e17c474de174fc44e74d91a0fc6594a89d39beaf3c8f7fabe3f432af21e
|
data/.rspec
ADDED
data/.travis.yml
CHANGED
@@ -3,8 +3,7 @@ rvm:
|
|
3
3
|
- jruby
|
4
4
|
- 2.0.0
|
5
5
|
- 1.9.3
|
6
|
+
script:
|
7
|
+
- bundle exec rspec --format documentation
|
6
8
|
notifications:
|
7
9
|
email: false
|
8
|
-
hipchat:
|
9
|
-
rooms:
|
10
|
-
secure: WhPGqnsNAVchiJz/rmmIPIFHXI7NVd+k/zClVhtgyoPEdebFYFgOCEIGbE/53IVymVeXFFBeJUVQg1Um4AVsZzVpS5sC6ABWw7rH5PZQZ7k177ZmuhbIAVYLLXcX0OmolgsATvGejifVj5i/Kld46kRx3JDlWL/mA465Kso7a/k=
|
data/README.md
CHANGED
@@ -91,12 +91,18 @@ A simple syntax for post aggregations with +,-,/,* can be used like:
|
|
91
91
|
|
92
92
|
```ruby
|
93
93
|
query = Druid::Query.new('service/source').long_sum([:aggregate1, :aggregate2])
|
94
|
-
query.postagg{(aggregate2 + aggregate2).as output_field_name}
|
94
|
+
query.postagg { (aggregate2 + aggregate2).as output_field_name }
|
95
95
|
```
|
96
96
|
|
97
97
|
Required fields for the postaggregation are fetched automatically by the
|
98
98
|
library.
|
99
99
|
|
100
|
+
Javascript post aggregations are also supported:
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
query.postagg { js('function(aggregate1, aggregate2) { return aggregate1 + aggregate2; }').as result }
|
104
|
+
```
|
105
|
+
|
100
106
|
### Query Interval
|
101
107
|
|
102
108
|
The interval for the query takes a string with date and time or objects that
|
data/lib/druid/having.rb
CHANGED
@@ -7,13 +7,19 @@ module Druid
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
-
class
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
10
|
+
class HavingFilter
|
11
|
+
include Serializable
|
12
|
+
|
13
|
+
def clause?
|
14
|
+
is_a?(HavingClause)
|
15
|
+
end
|
16
|
+
|
17
|
+
def operator?
|
18
|
+
is_a?(HavingOperator)
|
15
19
|
end
|
20
|
+
end
|
16
21
|
|
22
|
+
class HavingClause < HavingFilter
|
17
23
|
def initialize(metric)
|
18
24
|
@metric = metric
|
19
25
|
end
|
@@ -30,24 +36,34 @@ module Druid
|
|
30
36
|
self
|
31
37
|
end
|
32
38
|
|
33
|
-
def
|
34
|
-
|
39
|
+
def to_hash
|
40
|
+
{
|
41
|
+
:type => @type,
|
42
|
+
:aggregation => @metric,
|
43
|
+
:value => @value
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class HavingOperator < HavingFilter
|
49
|
+
def initialize(type)
|
50
|
+
@type = type
|
51
|
+
@elements = []
|
35
52
|
end
|
36
53
|
|
37
|
-
def
|
38
|
-
|
54
|
+
def and?
|
55
|
+
@type == 'and'
|
39
56
|
end
|
40
57
|
|
41
|
-
def
|
42
|
-
|
58
|
+
def add(element)
|
59
|
+
@elements << element
|
43
60
|
end
|
44
61
|
|
45
62
|
def to_hash
|
46
63
|
{
|
47
64
|
:type => @type,
|
48
|
-
:
|
49
|
-
:value => @value
|
65
|
+
:havingSpecs => @elements
|
50
66
|
}
|
51
67
|
end
|
52
68
|
end
|
53
|
-
end
|
69
|
+
end
|
@@ -5,6 +5,14 @@ module Druid
|
|
5
5
|
PostAggregationField.new(name)
|
6
6
|
end
|
7
7
|
end
|
8
|
+
|
9
|
+
def js(*args)
|
10
|
+
if args.empty?
|
11
|
+
PostAggregationField.new(:js)
|
12
|
+
else
|
13
|
+
PostAggregationJavascript.new(args.first)
|
14
|
+
end
|
15
|
+
end
|
8
16
|
end
|
9
17
|
|
10
18
|
module PostAggregationOperators
|
@@ -90,6 +98,8 @@ module Druid
|
|
90
98
|
end
|
91
99
|
|
92
100
|
class PostAggregationConstant
|
101
|
+
include PostAggregationOperators
|
102
|
+
|
93
103
|
attr_reader :value
|
94
104
|
|
95
105
|
def initialize(value)
|
@@ -108,4 +118,40 @@ module Druid
|
|
108
118
|
to_hash
|
109
119
|
end
|
110
120
|
end
|
121
|
+
|
122
|
+
class PostAggregationJavascript
|
123
|
+
include PostAggregationOperators
|
124
|
+
include Serializable
|
125
|
+
|
126
|
+
def initialize(function)
|
127
|
+
@field_names = extract_fields(function)
|
128
|
+
@function = function
|
129
|
+
end
|
130
|
+
|
131
|
+
def get_field_names
|
132
|
+
@field_names
|
133
|
+
end
|
134
|
+
|
135
|
+
def as(field)
|
136
|
+
@name = field.name.to_s
|
137
|
+
self
|
138
|
+
end
|
139
|
+
|
140
|
+
def to_hash
|
141
|
+
{
|
142
|
+
"type" => "javascript",
|
143
|
+
"name" => @name,
|
144
|
+
"fieldNames" => @field_names,
|
145
|
+
"function" => @function
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def extract_fields(function)
|
152
|
+
match = function.match(/function\((.+)\)/)
|
153
|
+
raise 'Invalid Javascript function' unless match && match.captures
|
154
|
+
match.captures.first.split(',').map {|field| field.strip }
|
155
|
+
end
|
156
|
+
end
|
111
157
|
end
|
data/lib/druid/query.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
require 'druid/serializable'
|
1
2
|
require 'druid/filter'
|
2
3
|
require 'druid/having'
|
3
4
|
require 'druid/post_aggregation'
|
5
|
+
|
4
6
|
require 'time'
|
5
7
|
require 'json'
|
6
8
|
|
@@ -137,7 +139,20 @@ module Druid
|
|
137
139
|
|
138
140
|
def having(&block)
|
139
141
|
having = Having.new.instance_exec(&block)
|
140
|
-
|
142
|
+
|
143
|
+
if old_having = @properties[:having]
|
144
|
+
if old_having.operator? && old_having.and?
|
145
|
+
new_having = old_having
|
146
|
+
else
|
147
|
+
new_having = HavingOperator.new('and')
|
148
|
+
new_having.add(old_having)
|
149
|
+
end
|
150
|
+
new_having.add(having)
|
151
|
+
else
|
152
|
+
new_having = having
|
153
|
+
end
|
154
|
+
|
155
|
+
@properties[:having] = new_having
|
141
156
|
self
|
142
157
|
end
|
143
158
|
|
data/ruby-druid.gemspec
CHANGED
data/spec/lib/client_spec.rb
CHANGED
data/spec/lib/query_spec.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
1
|
describe Druid::Query do
|
4
2
|
|
5
3
|
before :each do
|
@@ -20,7 +18,7 @@ describe Druid::Query do
|
|
20
18
|
@query.group_by()
|
21
19
|
JSON.parse(@query.to_json)['queryType'].should == 'groupBy'
|
22
20
|
end
|
23
|
-
|
21
|
+
|
24
22
|
it 'sets query type to timeseries' do
|
25
23
|
@query.time_series()
|
26
24
|
JSON.parse(@query.to_json)['queryType'].should == 'timeseries'
|
@@ -39,91 +37,113 @@ describe Druid::Query do
|
|
39
37
|
result['threshold'].should == 25
|
40
38
|
end
|
41
39
|
|
42
|
-
|
43
|
-
|
40
|
+
describe '#postagg' do
|
41
|
+
it 'build a post aggregation with a constant right' do
|
42
|
+
@query.postagg{(a + 1).as ctr }
|
44
43
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
44
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
45
|
+
"fn"=>"+",
|
46
|
+
"fields"=>
|
47
|
+
[{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
|
48
|
+
{"type"=>"constant", "value"=>1}],
|
49
|
+
"name"=>"ctr"}]
|
50
|
+
end
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
52
|
+
it 'build a + post aggregation' do
|
53
|
+
@query.postagg{(a + b).as ctr }
|
54
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
55
|
+
"fn"=>"+",
|
56
|
+
"fields"=>
|
57
|
+
[{"type"=>"fieldAccess","name"=>"a", "fieldName"=>"a"},
|
58
|
+
{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
|
59
|
+
"name"=>"ctr"}]
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'build a - post aggregation' do
|
63
|
+
@query.postagg{(a - b).as ctr }
|
64
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
65
|
+
"fn"=>"-",
|
66
|
+
"fields"=>
|
67
|
+
[{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
|
68
|
+
{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
|
69
|
+
"name"=>"ctr"}]
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'build a * post aggregation' do
|
73
|
+
@query.postagg{(a * b).as ctr }
|
74
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
75
|
+
"fn"=>"*",
|
76
|
+
"fields"=>
|
77
|
+
[{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
|
78
|
+
{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
|
79
|
+
"name"=>"ctr"}]
|
80
|
+
end
|
62
81
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
82
|
+
it 'build a / post aggregation' do
|
83
|
+
@query.postagg{(a / b).as ctr }
|
84
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
85
|
+
"fn"=>"/",
|
86
|
+
"fields"=>
|
87
|
+
[{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
|
88
|
+
{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
|
70
89
|
"name"=>"ctr"}]
|
71
|
-
|
90
|
+
end
|
72
91
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
92
|
+
it 'build a complex post aggregation' do
|
93
|
+
@query.postagg{((a / b) * 1000).as ctr }
|
94
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
95
|
+
"fn"=>"*",
|
96
|
+
"fields"=>
|
97
|
+
[{"type"=>"arithmetic", "fn"=>"/", "fields"=>
|
98
|
+
[{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
|
99
|
+
{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}]},
|
100
|
+
{"type"=>"constant", "value"=>1000}],
|
80
101
|
"name"=>"ctr"}]
|
81
|
-
|
102
|
+
end
|
82
103
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
"name"=>"ctr"}]
|
91
|
-
end
|
104
|
+
it 'adds fields required by the postagg operation to longsum' do
|
105
|
+
@query.postagg{ (a/b).as c }
|
106
|
+
JSON.parse(@query.to_json)['aggregations'].should == [
|
107
|
+
{"type"=>"longSum", "name"=>"a", "fieldName"=>"a"},
|
108
|
+
{"type"=>"longSum", "name"=>"b", "fieldName"=>"b"}
|
109
|
+
]
|
110
|
+
end
|
92
111
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
"
|
97
|
-
|
98
|
-
|
112
|
+
it 'chains aggregations' do
|
113
|
+
@query.postagg{(a / b).as ctr }.postagg{(b / a).as rtc }
|
114
|
+
|
115
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
|
116
|
+
"fn"=>"/",
|
117
|
+
"fields"=>
|
99
118
|
[{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
{
|
125
|
-
|
126
|
-
|
119
|
+
{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
|
120
|
+
"name"=>"ctr"},
|
121
|
+
{"type"=>"arithmetic",
|
122
|
+
"fn"=>"/",
|
123
|
+
"fields"=>
|
124
|
+
[{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"},
|
125
|
+
{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"}],
|
126
|
+
"name"=>"rtc"}
|
127
|
+
]
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'builds a javascript post aggregation' do
|
131
|
+
@query.postagg { js('function(agg1, agg2) { return agg1 + agg2; }').as result }
|
132
|
+
JSON.parse(@query.to_json)['postAggregations'].should == [
|
133
|
+
{
|
134
|
+
'type' => 'javascript',
|
135
|
+
'name' => 'result',
|
136
|
+
'fieldNames' => ['agg1', 'agg2'],
|
137
|
+
'function' => 'function(agg1, agg2) { return agg1 + agg2; }'
|
138
|
+
}
|
139
|
+
]
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'raises an error when an invalid javascript function is used' do
|
143
|
+
expect {
|
144
|
+
@query.postagg { js('{ return a_with_b - a; }').as b }
|
145
|
+
}.to raise_error
|
146
|
+
end
|
127
147
|
end
|
128
148
|
|
129
149
|
it 'builds aggregations on long_sum' do
|
@@ -135,7 +155,6 @@ describe Druid::Query do
|
|
135
155
|
]
|
136
156
|
end
|
137
157
|
|
138
|
-
|
139
158
|
it 'appends long_sum properties from aggregations on calling long_sum again' do
|
140
159
|
@query.long_sum(:a, :b, :c)
|
141
160
|
@query.double_sum(:x,:y)
|
@@ -368,11 +387,25 @@ end
|
|
368
387
|
]}
|
369
388
|
end
|
370
389
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
390
|
+
describe '#having' do
|
391
|
+
it 'creates a greater than having clause' do
|
392
|
+
@query.having{a > 100}
|
393
|
+
JSON.parse(@query.to_json)['having'].should == {
|
394
|
+
"type"=>"greaterThan", "aggregation"=>"a", "value"=>100
|
395
|
+
}
|
396
|
+
end
|
397
|
+
|
398
|
+
it 'chains having clauses with and' do
|
399
|
+
@query.having{a > 100}.having{b > 200}.having{c > 300}
|
400
|
+
JSON.parse(@query.to_json)['having'].should == {
|
401
|
+
"type" => "and",
|
402
|
+
"havingSpecs" => [
|
403
|
+
{ "type" => "greaterThan", "aggregation" => "a", "value" => 100 },
|
404
|
+
{ "type" => "greaterThan", "aggregation" => "b", "value" => 200 },
|
405
|
+
{ "type" => "greaterThan", "aggregation" => "c", "value" => 300 }
|
406
|
+
]
|
407
|
+
}
|
408
|
+
end
|
376
409
|
end
|
377
410
|
|
378
411
|
it 'does not accept in with empty array' do
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-druid
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LiquidM, Inc.
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: zk
|
15
|
-
version_requirements: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - '>='
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0'
|
20
15
|
requirement: !ruby/object:Gem::Requirement
|
21
16
|
requirements:
|
22
|
-
- -
|
17
|
+
- - ">="
|
23
18
|
- !ruby/object:Gem::Version
|
24
19
|
version: '0'
|
25
|
-
prerelease: false
|
26
20
|
type: :runtime
|
27
|
-
|
28
|
-
name: rest-client
|
21
|
+
prerelease: false
|
29
22
|
version_requirements: !ruby/object:Gem::Requirement
|
30
23
|
requirements:
|
31
|
-
- -
|
24
|
+
- - ">="
|
32
25
|
- !ruby/object:Gem::Version
|
33
26
|
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rest-client
|
34
29
|
requirement: !ruby/object:Gem::Requirement
|
35
30
|
requirements:
|
36
|
-
- -
|
31
|
+
- - ">="
|
37
32
|
- !ruby/object:Gem::Version
|
38
33
|
version: '0'
|
39
|
-
prerelease: false
|
40
34
|
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
41
|
description: Ruby client for metamx druid
|
42
42
|
email:
|
43
43
|
- opensource@liquidm.com
|
@@ -46,8 +46,9 @@ executables:
|
|
46
46
|
extensions: []
|
47
47
|
extra_rdoc_files: []
|
48
48
|
files:
|
49
|
-
- .gitignore
|
50
|
-
- .
|
49
|
+
- ".gitignore"
|
50
|
+
- ".rspec"
|
51
|
+
- ".travis.yml"
|
51
52
|
- Gemfile
|
52
53
|
- LICENSE
|
53
54
|
- README.md
|
@@ -62,6 +63,7 @@ files:
|
|
62
63
|
- lib/druid/post_aggregation.rb
|
63
64
|
- lib/druid/query.rb
|
64
65
|
- lib/druid/response_row.rb
|
66
|
+
- lib/druid/serializable.rb
|
65
67
|
- lib/druid/zoo_handler.rb
|
66
68
|
- ruby-druid.gemspec
|
67
69
|
- spec/lib/client_spec.rb
|
@@ -72,24 +74,24 @@ homepage: https://github.com/liquidm/ruby-druid
|
|
72
74
|
licenses:
|
73
75
|
- MIT
|
74
76
|
metadata: {}
|
75
|
-
post_install_message:
|
77
|
+
post_install_message:
|
76
78
|
rdoc_options: []
|
77
79
|
require_paths:
|
78
80
|
- lib
|
79
81
|
required_ruby_version: !ruby/object:Gem::Requirement
|
80
82
|
requirements:
|
81
|
-
- -
|
83
|
+
- - ">="
|
82
84
|
- !ruby/object:Gem::Version
|
83
85
|
version: '0'
|
84
86
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
85
87
|
requirements:
|
86
|
-
- -
|
88
|
+
- - ">="
|
87
89
|
- !ruby/object:Gem::Version
|
88
90
|
version: '0'
|
89
91
|
requirements: []
|
90
|
-
rubyforge_project:
|
91
|
-
rubygems_version: 2.
|
92
|
-
signing_key:
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.1.11
|
94
|
+
signing_key:
|
93
95
|
specification_version: 4
|
94
96
|
summary: Ruby client for metamx druid
|
95
97
|
test_files:
|
@@ -97,4 +99,4 @@ test_files:
|
|
97
99
|
- spec/lib/query_spec.rb
|
98
100
|
- spec/lib/zoo_handler_spec.rb
|
99
101
|
- spec/spec_helper.rb
|
100
|
-
has_rdoc:
|
102
|
+
has_rdoc:
|