keikokuc 0.7 → 0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -11,12 +11,16 @@ class Keikokuc::Client
11
11
  InvalidNotification = Class.new
12
12
  Unauthorized = Class.new
13
13
 
14
- attr_accessor :producer_api_key, :user, :password
14
+ attr_accessor :username, :api_key
15
15
 
16
+ # Internal: Initialize a Client
17
+ #
18
+ # opts = a hash containing two possible attributes:
19
+ # api_key - the user's or producer's API key (required)
20
+ # username - the producer's username (only required for publishers)
16
21
  def initialize(opts = {})
17
- @producer_api_key = opts[:producer_api_key]
18
- @user = opts[:user]
19
- @password = opts[:password]
22
+ @api_key = opts.fetch(:api_key)
23
+ @username = opts[:username]
20
24
  end
21
25
 
22
26
  # Internal: posts a new notification to keikoku
@@ -25,7 +29,8 @@ class Keikokuc::Client
25
29
  #
26
30
  # Examples
27
31
  #
28
- # client = Keikokuc::Client.new(producer_api_key: 'abcd')
32
+ # client = Keikokuc::Client.new(username: 'heroku-postgres',
33
+ # api_key: 'abcd')
29
34
  # response, error = client.post_notification(message: 'hello')
30
35
  #
31
36
  # Returns
@@ -42,7 +47,7 @@ class Keikokuc::Client
42
47
  # * `Client::Unauthorized` if API key auth fails
43
48
  def post_notification(attributes)
44
49
  begin
45
- response = notifications_api.post(encode_json(attributes), {'X-KEIKOKU-AUTH' => producer_api_key})
50
+ response = notifications_api.post(encode_json(attributes))
46
51
  [parse_json(response), nil]
47
52
  rescue RestClient::UnprocessableEntity => e
48
53
  [parse_json(e.response), InvalidNotification]
@@ -56,7 +61,7 @@ class Keikokuc::Client
56
61
  #
57
62
  # Examples
58
63
  #
59
- # client = Keikokuc::Client.new(user: 'user@example.com', password: 'pass')
64
+ # client = Keikokuc::Client.new(api_key: 'api-key')
60
65
  # response, error = client.get_notifications
61
66
  #
62
67
  # Returns
@@ -83,6 +88,8 @@ class Keikokuc::Client
83
88
  #
84
89
  # remote_id - the keikoku id for the notification to mark as read
85
90
  #
91
+ # Returns
92
+ #
86
93
  # two objects:
87
94
  # The response as a hash
88
95
  # The error if any (nil if no error)
@@ -95,7 +102,6 @@ class Keikokuc::Client
95
102
  begin
96
103
  response = notifications_api["/#{remote_id}/read"].post ''
97
104
  parsed_response = parse_json(response)
98
- parsed_response[:read_at] = DateTime.parse(parsed_response[:read_at])
99
105
  [parsed_response, nil]
100
106
  rescue RestClient::Unauthorized
101
107
  [{}, Unauthorized]
@@ -103,12 +109,13 @@ class Keikokuc::Client
103
109
  end
104
110
  handle_timeout :read_notification
105
111
 
106
-
107
112
  private
108
113
  def notifications_api # :nodoc:
109
- @notifications_api ||= RestClient::Resource.new(api_url,
110
- :user => user,
111
- :password => password)
114
+ @notifications_api ||= RestClient::Resource.new(
115
+ api_url,
116
+ :user => username || '',
117
+ :password => api_key
118
+ )
112
119
  end
113
120
 
114
121
  def api_url # :nodoc:
@@ -7,7 +7,8 @@
7
7
  # notification = Keikokuc::Notification.new(message: 'hello',
8
8
  # severity: 'info',
9
9
  # target_name: 'sunny-skies-42'
10
- # producer_api_key: 'abcd')
10
+ # producer_password: 'abcd',
11
+ # username: 'heroku-postgres')
11
12
  # if notification.publish
12
13
  # # persist notification
13
14
  # else
@@ -17,8 +18,8 @@
17
18
  class Keikokuc::Notification
18
19
  attr_accessor :message, :url, :severity,
19
20
  :target_name, :account_email,
20
- :producer_api_key, :remote_id,
21
- :errors, :read_at, :account_sequence
21
+ :producer_password, :username,
22
+ :remote_id, :errors, :account_sequence
22
23
 
23
24
  # Public: Initialize a notification
24
25
  #
@@ -30,17 +31,17 @@ class Keikokuc::Notification
30
31
  #
31
32
  # All keys on the attr_accessor list will be set
32
33
  def initialize(opts = {})
33
- @message = opts[:message]
34
- @url = opts[:url]
35
- @severity = opts[:severity]
36
- @target_name = opts[:target_name]
37
- @account_email = opts[:account_email]
38
- @producer_api_key = opts[:producer_api_key]
39
- @remote_id = opts[:remote_id]
40
- @errors = opts[:errors]
41
- @read_at = opts[:read_at]
42
- @account_sequence = opts[:account_sequence]
43
- @client = opts[:client]
34
+ @message = opts[:message]
35
+ @url = opts[:url]
36
+ @severity = opts[:severity]
37
+ @target_name = opts[:target_name]
38
+ @account_email = opts[:account_email]
39
+ @producer_password = opts[:producer_password]
40
+ @username = opts[:username]
41
+ @remote_id = opts[:remote_id]
42
+ @errors = opts[:errors]
43
+ @account_sequence = opts[:account_sequence]
44
+ @client = opts[:client]
44
45
  end
45
46
 
46
47
  # Public: publishes this notification to keikoku
@@ -70,39 +71,31 @@ class Keikokuc::Notification
70
71
  # Returns a boolean set to true if marking as read succeeded
71
72
  def read
72
73
  response, error = client.read_notification(remote_id)
73
- if error.nil?
74
- self.read_at = response[:read_at]
75
- end
76
74
  error.nil?
77
75
  end
78
76
 
79
- # Public: whether this notification is marked as read by this user
80
- #
81
- # Returns true if the user has marked this notification as read
82
- def read?
83
- !!@read_at
84
- end
85
-
86
77
  # Internal: coerces this notification to a hash
87
78
  #
88
79
  # Returns this notification's attributes as a hash
89
80
  def to_hash
90
81
  {
91
- :message => @message,
92
- :url => @url,
93
- :severity => @severity,
94
- :target_name => @target_name,
95
- :account_email => @account_email,
96
- :producer_api_key => @producer_api_key,
97
- :remote_id => @remote_id,
98
- :errors => @errors,
99
- :read_at => @read_at,
100
- :account_sequence => @account_sequence,
101
- :client => @client
82
+ :message => @message,
83
+ :url => @url,
84
+ :severity => @severity,
85
+ :target_name => @target_name,
86
+ :account_email => @account_email,
87
+ :producer_password => @producer_password,
88
+ :remote_id => @remote_id,
89
+ :errors => @errors,
90
+ :account_sequence => @account_sequence,
91
+ :client => @client
102
92
  }
103
93
  end
104
94
 
105
95
  def client # :nodoc:
106
- @client ||= Keikokuc::Client.new(:producer_api_key => producer_api_key)
96
+ @client ||= Keikokuc::Client.new(
97
+ :username => username,
98
+ :api_key => producer_password
99
+ )
107
100
  end
108
101
  end
@@ -8,8 +8,7 @@
8
8
  #
9
9
  # Examples
10
10
  #
11
- # notifications = Keikokuc::NotificationList.new(user: 'user@example.com',
12
- # api_key: 'abcd')
11
+ # notifications = Keikokuc::NotificationList.new(api_key: 'abcd')
13
12
  # if notifications.fetch
14
13
  # notifications.each do |notification|
15
14
  # puts notification.inspect
@@ -20,18 +19,16 @@
20
19
  class Keikokuc::NotificationList
21
20
  include Enumerable
22
21
 
23
- attr_accessor :user, :password
22
+ attr_accessor :api_key
24
23
 
25
24
  # Public: Initializes a NotificationList
26
25
  #
27
26
  # opts - options hash containing attribute values for the object
28
27
  # being constructed accepting the following three keys:
29
- # user - the heroku account's email (required)
30
- # password - the heroku account's password (required)
28
+ # api_key - the heroku account's api_key (required)
31
29
  # client - the client, used for DI in tests
32
30
  def initialize(opts)
33
- @user = opts.fetch(:user)
34
- @password = opts.fetch(:password)
31
+ @api_key = opts.fetch(:api_key)
35
32
  @client = opts[:client]
36
33
  @notifications = []
37
34
  end
@@ -95,7 +92,7 @@ class Keikokuc::NotificationList
95
92
 
96
93
  private
97
94
  def client # :nodoc:
98
- @client ||= Keikokuc::Client.new(:user => user,
99
- :password => password)
95
+ @client ||= Keikokuc::Client.new(:user => '',
96
+ :api_key => api_key)
100
97
  end
101
98
  end
@@ -1,3 +1,3 @@
1
1
  module Keikokuc
2
- VERSION = "0.7"
2
+ VERSION = "0.8"
3
3
  end
@@ -11,8 +11,8 @@ module Keikokuc
11
11
 
12
12
  it 'publishes a new notification' do
13
13
  ShamRack.mount(fake_keikoku, "keikoku.herokuapp.com", 443)
14
- fake_keikoku.register_publisher({:api_key => 'abc'})
15
- client = Client.new(:producer_api_key => 'abc')
14
+ fake_keikoku.register_producer({:api_key => 'abc', :username => 'heroku-postgres'})
15
+ client = Client.new(:api_key => 'abc', :username => 'heroku-postgres')
16
16
  result, error = client.post_notification(:message => 'hello',
17
17
  :severity => 'info')
18
18
  expect(result[:id]).not_to be_nil
@@ -24,15 +24,15 @@ module Keikokuc
24
24
  [422, {}, StringIO.new(OkJson.encode({ 'errors' => 'srorre' }))]
25
25
  end
26
26
 
27
- response, error = Client.new.post_notification({})
27
+ response, error = Client.new(:api_key => 'key').post_notification({})
28
28
  expect(error).to be Client::InvalidNotification
29
29
  expect(response[:errors]).to eq('srorre')
30
30
  end
31
31
 
32
32
  it 'handles authentication failures' do
33
33
  ShamRack.mount(fake_keikoku, "keikoku.herokuapp.com", 443)
34
- fake_keikoku.register_publisher({:api_key => 'abc'})
35
- client = Client.new(:producer_api_key => 'bad one')
34
+ fake_keikoku.register_producer({:api_key => 'abc', :username => 'heroku-postgres'})
35
+ client = Client.new(:api_key => 'bad one', :username => 'heroku-postgres')
36
36
  result, error = client.post_notification(:message => 'hello',
37
37
  :severity => 'info')
38
38
  expect(result[:id]).to be_nil
@@ -41,7 +41,7 @@ module Keikokuc
41
41
 
42
42
  it 'handles timeouts' do
43
43
  RestClient::Resource.any_instance.stub(:post).and_raise Timeout::Error
44
- response, error = Client.new.post_notification({})
44
+ response, error = Client.new(:api_key => 'key').post_notification({})
45
45
  expect(response).to be_nil
46
46
  expect(error).to eq(Client::RequestTimeout)
47
47
  end
@@ -52,12 +52,14 @@ module Keikokuc
52
52
 
53
53
  it 'gets all notifications for a user' do
54
54
  ShamRack.mount(fake_keikoku, "keikoku.herokuapp.com", 443)
55
- fake_keikoku.register_publisher(:api_key => 'abc')
56
- fake_keikoku.register_user(:email => 'harold@heroku.com', :password => 'pass')
57
- build_notification(:account_email => 'harold@heroku.com', :message => 'find me!', :producer_api_key => 'abc').publish
58
- build_notification(:account_email => 'another@heroku.com', :producer_api_key => 'abc').publish
55
+ fake_keikoku.register_producer(:username => 'heroku-postgres', :api_key => 'abc')
56
+ fake_keikoku.register_user(:api_key => 'api-key', :account_email => 'harold@heroku.com')
57
+ build_notification(:account_email => 'harold@heroku.com', :message => 'find me!',
58
+ :producer_password => 'abc', :username => 'heroku-postgres').publish
59
+ build_notification(:account_email => 'another@heroku.com', :producer_password => 'abc',
60
+ :username => 'heroku-postgres').publish
59
61
 
60
- client = Client.new(:user => 'harold@heroku.com', :password => 'pass')
62
+ client = Client.new(:api_key => 'api-key')
61
63
 
62
64
  notifications, error = client.get_notifications
63
65
 
@@ -69,15 +71,15 @@ module Keikokuc
69
71
 
70
72
  it 'handles timeouts' do
71
73
  RestClient::Resource.any_instance.stub(:get).and_raise Timeout::Error
72
- response, error = Client.new.get_notifications
74
+ response, error = Client.new(:api_key => 'key').get_notifications
73
75
  expect(response).to be_nil
74
76
  expect(error).to eq(Client::RequestTimeout)
75
77
  end
76
78
 
77
79
  it 'handles authentication failures' do
78
80
  ShamRack.mount(fake_keikoku, "keikoku.herokuapp.com", 443)
79
- fake_keikoku.register_user(:email => 'harold@heroku.com', :password => 'pass')
80
- client = Client.new(:user => 'harold@heroku.com', :password => 'bad-pass')
81
+ fake_keikoku.register_user(:api_key => 'api-key', :account_email => 'harold@heroku.com')
82
+ client = Client.new(:api_key => 'bad-api-key')
81
83
 
82
84
  response, error = client.get_notifications
83
85
 
@@ -88,28 +90,28 @@ module Keikokuc
88
90
 
89
91
  describe Client, '#read_notification' do
90
92
  include_context 'client specs'
93
+
91
94
  it 'marks the notification as read' do
92
95
  ShamRack.mount(fake_keikoku, "keikoku.herokuapp.com", 443)
93
- fake_keikoku.register_publisher(:api_key => 'abc')
94
- fake_keikoku.register_user(:email => 'harold@heroku.com', :password => 'pass')
95
- client = Client.new(:user => 'harold@heroku.com', :password => 'pass')
96
- notification = build_notification(:account_email => 'harold@heroku.com',
97
- :producer_api_key => 'abc')
96
+
97
+ fake_keikoku.register_producer(:username => 'heroku-postgres', :api_key => 'abc')
98
+ fake_keikoku.register_user(:api_key => 'api-key', :account_email => 'harold@heroku.com')
99
+ client = Client.new(:api_key => 'api-key')
100
+ notification = build_notification(:account_email => 'harold@heroku.com',
101
+ :producer_password => 'abc',
102
+ :username => 'heroku-postgres')
98
103
  notification.publish or raise "Notification publish error"
99
104
 
100
105
  response, error = client.read_notification(notification.remote_id)
101
106
  expect(error).to be_nil
102
107
 
103
- expect(response[:read_by]).to eq('harold@heroku.com')
104
- expect(response[:read_at]).to be_within(1).of(DateTime.now)
108
+ expect(response[:status]).to eq('ok')
105
109
  end
106
110
 
107
111
  it 'handles authentication errors' do
108
112
  ShamRack.mount(fake_keikoku, "keikoku.herokuapp.com", 443)
109
- fake_keikoku.register_user(:email => 'harold@heroku.com',
110
- :password => 'pass')
111
- client = Client.new(:user => 'harold@heroku.com',
112
- :password => 'bad-pass')
113
+ fake_keikoku.register_user(:api_key => 'api-key', :account_email => 'harold@heroku.com')
114
+ client = Client.new(:api_key => 'bad-api-key')
113
115
  response, error = client.read_notification(1)
114
116
  expect(response).to be_empty
115
117
  expect(error).to eq(Client::Unauthorized)
@@ -117,7 +119,7 @@ module Keikokuc
117
119
 
118
120
  it 'handles timeouts' do
119
121
  RestClient::Resource.any_instance.stub(:post).and_raise Timeout::Error
120
- response, error = Client.new.read_notification(1)
122
+ response, error = Client.new(:api_key => 'key').read_notification(1)
121
123
  expect(response).to be_nil
122
124
  expect(error).to eq(Client::RequestTimeout)
123
125
  end
@@ -65,11 +65,6 @@ module Keikokuc
65
65
  list.fetch or raise "error fetching"
66
66
 
67
67
  expect(list.read_all).to be_true
68
-
69
- list.each do |notification|
70
- expect(notification).to be_read
71
- expect(notification.read_at).to eq(now)
72
- end
73
68
  end
74
69
 
75
70
  it 'returns false if any notification fails to be marked as read' do
@@ -51,15 +51,12 @@ module Keikokuc
51
51
 
52
52
  fake_client.should_receive(:read_notification).
53
53
  with('1234').
54
- and_return([{:read_at => Time.now}, nil])
54
+ and_return([{'status' => 'ok'}, nil])
55
55
 
56
56
  notification = Notification.new(:remote_id => '1234', :client => fake_client)
57
57
 
58
58
  result = notification.read
59
59
  expect(result).to be_true
60
-
61
- expect(notification.read_at).to be_within(1).of(Time.now)
62
- expect(notification).to be_read
63
60
  end
64
61
 
65
62
  it 'handles errors' do
@@ -73,28 +70,15 @@ module Keikokuc
73
70
 
74
71
  result = notification.read
75
72
  expect(result).to be_false
76
-
77
- expect(notification.read_at).to be_nil
78
- expect(notification).not_to be_read
79
- end
80
- end
81
-
82
- describe Notification, '#read?' do
83
- it 'is true if the read_at is known' do
84
- notification = build_notification(:read_at => nil)
85
- expect(notification.read?).to be_false
86
-
87
- notification.read_at = Time.now
88
-
89
- expect(notification.read?).to be_true
90
73
  end
91
74
  end
92
75
 
93
76
  describe Notification, '#client' do
94
77
  it 'defaults to a properly constructer Keikokuc::Client' do
95
- notification = build_notification(:producer_api_key => 'fake-api-key')
78
+ notification = build_notification(:producer_password => 'fake-api-key', :username => 'heroku-postgres')
96
79
  expect(notification.client).to be_kind_of(Keikokuc::Client)
97
- expect(notification.client.producer_api_key).to eq('fake-api-key')
80
+ expect(notification.client.api_key).to eq('fake-api-key')
81
+ expect(notification.client.username).to eq('heroku-postgres')
98
82
  end
99
83
 
100
84
  it 'can be injected' do
@@ -11,8 +11,7 @@ module Factories
11
11
 
12
12
  def build_notification_list(opts = {})
13
13
  defaults = {
14
- :user => 'user@example.com',
15
- :password => 'pass'
14
+ :api_key => 'api-key'
16
15
  }
17
16
  Keikokuc::NotificationList.new(defaults.merge(opts))
18
17
  end
@@ -1,24 +1,28 @@
1
1
  class FakeKeikoku
2
2
  def initialize
3
- @publishers = []
3
+ @producers = []
4
4
  @notifications = []
5
5
  @users = []
6
6
  end
7
7
 
8
- def register_publisher(opts)
9
- @publishers << opts
8
+ def register_producer(opts)
9
+ @producers << opts
10
10
  end
11
11
 
12
12
  def register_user(opts)
13
13
  @users << opts
14
14
  end
15
15
 
16
- def publisher_by_api_key(api_key)
17
- @publishers.detect { |p| p[:api_key] == api_key }
16
+ def producer_by_api_key(api_key)
17
+ @producers.detect { |p| p[:api_key] == api_key }
18
18
  end
19
19
 
20
- def find_user(user, pass)
21
- @users.detect { |u| u[:email] == user && u[:password] == pass }
20
+ def find_user(api_key)
21
+ @users.detect { |u| u[:api_key] == api_key }
22
+ end
23
+
24
+ def find_producer(user, api_key)
25
+ @producers.detect { |p| p[:username] == user && p[:api_key] == api_key }
22
26
  end
23
27
 
24
28
  def notifications_for_user(email)
@@ -30,7 +34,7 @@ class FakeKeikoku
30
34
  def call(env)
31
35
  with_rack_env(env) do
32
36
  if request_path == '/api/v1/notifications' && request_verb == 'POST'
33
- if publisher_by_api_key(request_api_key)
37
+ if authenticate_producer
34
38
  notification = Notification.new({:id => next_id}.merge(request_body))
35
39
  @notifications << notification
36
40
  [200, { }, [Keikokuc::OkJson.encode({'id' => notification.id})]]
@@ -38,8 +42,8 @@ class FakeKeikoku
38
42
  [401, { }, ["Not authorized"]]
39
43
  end
40
44
  elsif request_path == '/api/v1/notifications' && request_verb == 'GET'
41
- if current_user = authenticate_consumer
42
- notifications = notifications_for_user(current_user).map(&:to_hash)
45
+ if api_key = authenticate_consumer
46
+ notifications = notifications_for_user(api_key).map(&:to_hash)
43
47
  [200, { }, [Keikokuc::OkJson.encode(notifications)]]
44
48
  else
45
49
  [401, { }, ["Not authorized"]]
@@ -50,7 +54,7 @@ class FakeKeikoku
50
54
  notification.to_hash['id'].to_s == $1.to_s
51
55
  end
52
56
  notification.mark_read_by!(current_user)
53
- [200, {}, [Keikokuc::OkJson.encode({'read_by' => current_user, 'read_at' => Time.now.to_s})]]
57
+ [200, {}, [Keikokuc::OkJson.encode({'status' => 'ok'})]]
54
58
  else
55
59
  [401, { }, ["Not authorized"]]
56
60
  end
@@ -85,16 +89,21 @@ private
85
89
  Keikokuc::OkJson.decode(raw_body)
86
90
  end
87
91
 
88
- def request_api_key
89
- rack_env["HTTP_X_KEIKOKU_AUTH"]
92
+ def authenticate_producer
93
+ auth = Rack::Auth::Basic::Request.new(rack_env)
94
+ if auth.provided? && auth.basic? && creds = auth.credentials
95
+ if find_producer(*creds)
96
+ creds.first
97
+ end
98
+ end
90
99
  end
91
100
 
92
101
  def authenticate_consumer
93
102
  auth = Rack::Auth::Basic::Request.new(rack_env)
94
103
  if auth.provided? && auth.basic? && creds = auth.credentials
95
- # creds looks like [user, password]
96
- if find_user(*creds)
97
- creds.first
104
+ # creds looks like [user, password] (technically ['', api_key])
105
+ if user = find_user(creds[-1])
106
+ user[:account_email]
98
107
  end
99
108
  end
100
109
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keikokuc
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.7'
4
+ version: '0.8'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-20 00:00:00.000000000 Z
12
+ date: 2013-01-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rest-client
16
- requirement: &70175173062000 !ruby/object:Gem::Requirement
16
+ requirement: &70304476052160 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70175173062000
24
+ version_requirements: *70304476052160
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec
27
- requirement: &70175173061580 !ruby/object:Gem::Requirement
27
+ requirement: &70304475972260 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70175173061580
35
+ version_requirements: *70304475972260
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: sham_rack
38
- requirement: &70175173061160 !ruby/object:Gem::Requirement
38
+ requirement: &70304475971620 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,7 +43,7 @@ dependencies:
43
43
  version: '0'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *70175173061160
46
+ version_requirements: *70304475971620
47
47
  description: Keikoku client
48
48
  email:
49
49
  - harold.gimenez@gmail.com
@@ -104,3 +104,4 @@ test_files:
104
104
  - spec/spec_helper.rb
105
105
  - spec/support/factories.rb
106
106
  - spec/support/fake_keikoku.rb
107
+ has_rdoc: