scalingo-ruby-api 1.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e414ecac6ae02a9b85d286fdfea4bb6c9a8f3c4d
4
+ data.tar.gz: b01e544df9c7ea1b4bc39f62ee68699764305d7e
5
+ SHA512:
6
+ metadata.gz: c14839d474e165ebe4deaba60e98c2193aa654a2018df1b9955da73292042a8c17c8f92f8f8a9a0842093a5cf02616eff38180c7eb6cd2f6518abfb02b174aea
7
+ data.tar.gz: 40ac42171d6091944d4d540647d132fa4bfc061e28d6932a4ea00f5512b064ffdfea75221e18732af0a4c756eee8f04f55dd223f9e65858b98142c34f5509897
@@ -0,0 +1,47 @@
1
+ require 'faraday'
2
+
3
+ module FaradayMiddleware
4
+ class RaiseHttpException < Faraday::Middleware
5
+ def call(env)
6
+ @app.call(env).on_complete do |response|
7
+ case response[:status].to_i
8
+ when 400
9
+ raise Scalingo::BadRequest, error_message_400(response)
10
+ when 404
11
+ raise Scalingo::NotFound, error_message_400(response)
12
+ when 500
13
+ raise Scalingo::InternalServerError, error_message_500(response, "Something is technically wrong.")
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(app)
19
+ super app
20
+ @parser = nil
21
+ end
22
+
23
+ private
24
+
25
+ def error_message_400(response)
26
+ "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{response[:status]}#{error_body(response[:body])}"
27
+ end
28
+
29
+ def error_body(body)
30
+ # body gets passed as a string, not sure if it is passed as something else from other spots?
31
+ if not body.nil? and not body.empty? and body.kind_of?(String)
32
+ # removed multi_json thanks to wesnolte's commit
33
+ body = ::JSON.parse(body)
34
+ end
35
+
36
+ if body.nil?
37
+ nil
38
+ elsif body['meta'] and body['meta']['error_message'] and not body['meta']['error_message'].empty?
39
+ ": #{body['meta']['error_message']}"
40
+ end
41
+ end
42
+
43
+ def error_message_500(response, body=nil)
44
+ "#{response[:method].to_s.upcase} #{response[:url].to_s}: #{[response[:status].to_s + ':', body].compact.join(' ')}"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,22 @@
1
+ require_relative 'connection'
2
+ require_relative 'request'
3
+ require_relative 'configuration'
4
+ require_relative 'endpoint'
5
+
6
+ module Scalingo
7
+ class Api
8
+ attr_accessor(*Configuration::VALID_OPTIONS_KEYS)
9
+
10
+ def initialize(options = {})
11
+ options = Scalingo.options.merge(options)
12
+ Configuration::VALID_OPTIONS_KEYS.each do |key|
13
+ send("#{key}=", options[key])
14
+ end
15
+ end
16
+
17
+ include Connection
18
+ include Request
19
+ include Endpoint
20
+ end
21
+ end
22
+
@@ -0,0 +1,5 @@
1
+ module Scalingo
2
+ class Client < Api
3
+ end
4
+ end
5
+
@@ -0,0 +1,65 @@
1
+ require 'faraday'
2
+ require_relative 'version'
3
+
4
+ module Scalingo
5
+ # Defines constants and methods related to configuration
6
+ module Configuration
7
+ # An array of valid keys in the options hash when configuring a {Scalingo::Api}
8
+ VALID_OPTIONS_KEYS = [
9
+ :adapter,
10
+ :token,
11
+ :endpoint,
12
+ :user_agent,
13
+ :proxy
14
+ ].freeze
15
+
16
+ # The adapter that will be used to connect if none is set
17
+ #
18
+ # @note The default faraday adapter is Net::HTTP.
19
+ DEFAULT_ADAPTER = Faraday.default_adapter
20
+
21
+ # By default, don't set an token
22
+ DEFAULT_TOKEN = nil
23
+
24
+ # The endpoint that will be used to connect if none is set
25
+ #
26
+ # @note There is no reason to use any other endpoint at this time
27
+ DEFAULT_ENDPOINT = 'https://api.scalingo.com/v1/'.freeze
28
+
29
+ # By default, don't use a proxy server
30
+ DEFAULT_PROXY = nil
31
+
32
+ # The user agent that will be sent to the Api endpoint if none is set
33
+ DEFAULT_USER_AGENT = "Scalingo Ruby Gem #{Scalingo::VERSION}".freeze
34
+
35
+ # @private
36
+ attr_accessor *VALID_OPTIONS_KEYS
37
+
38
+ # When this module is extended, set all configuration options to their default values
39
+ def self.extended(base)
40
+ base.reset
41
+ end
42
+
43
+ # Convenience method to allow configuration options to be set in a block
44
+ def configure
45
+ yield self
46
+ end
47
+
48
+ # Create a hash of options and their values
49
+ def options
50
+ VALID_OPTIONS_KEYS.inject({}) do |option, key|
51
+ option.merge!(key => send(key))
52
+ end
53
+ end
54
+
55
+ # Reset all configuration options to defaults
56
+ def reset
57
+ self.adapter = DEFAULT_ADAPTER
58
+ self.token = DEFAULT_TOKEN
59
+ self.endpoint = DEFAULT_ENDPOINT
60
+ self.user_agent = DEFAULT_USER_AGENT
61
+ self.proxy = DEFAULT_PROXY
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,34 @@
1
+ require 'faraday_middleware'
2
+ Dir[File.expand_path('../../faraday/*.rb', __FILE__)].each{|f| require f}
3
+
4
+ module Scalingo
5
+ module Connection
6
+ protected
7
+ def parse_json
8
+ true
9
+ end
10
+
11
+ private
12
+ def connection
13
+ options = {
14
+ :headers => {
15
+ 'Accept' => 'application/json; charset=utf-8',
16
+ 'Content-Type' => 'application/json',
17
+ 'User-Agent' => user_agent,
18
+ },
19
+ :proxy => proxy,
20
+ :url => endpoint,
21
+ }
22
+
23
+ Faraday::Connection.new(options) do |connection|
24
+ connection.use Faraday::Request::Multipart
25
+ connection.use Faraday::Request::UrlEncoded
26
+ connection.use Faraday::Response::ParseJson if parse_json
27
+ connection.use FaradayMiddleware::RaiseHttpException
28
+ connection.adapter(adapter)
29
+ connection.basic_auth('', token) if token
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,23 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class AccountKeys < Collection
4
+ def initialize(api)
5
+ super(api, 'account/keys')
6
+ end
7
+
8
+ def collection_name
9
+ 'keys'
10
+ end
11
+
12
+ def create(name, content)
13
+ post(nil, {key: {name: name, content: content}})
14
+ end
15
+ end
16
+ class AccountKey < Resource
17
+ def destroy
18
+ delete
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,7 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class AddonCategories < Collection
4
+ end
5
+ end
6
+ end
7
+
@@ -0,0 +1,7 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class AddonProviders < Collection
4
+ end
5
+ end
6
+ end
7
+
@@ -0,0 +1,19 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Addons < Collection
4
+ def create(addon_provider_id, plan_id)
5
+ post(nil, {addon:{addon_provider_id: addon_provider_id, plan_id: plan_id}})
6
+ end
7
+ end
8
+ class Addon < Resource
9
+ def update(plan_id)
10
+ patch(nil, {addon: {plan_id: plan_id}})
11
+ end
12
+
13
+ def destroy
14
+ delete
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,61 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Apps < Collection
4
+ def create(name)
5
+ post(nil, {app: {name: name}})
6
+ end
7
+
8
+ def find_by
9
+ 'name'
10
+ end
11
+ end
12
+ class App < Resource
13
+ def scale(containers)
14
+ post('scale', {containers: containers})
15
+ end
16
+
17
+ def restart(*scopes)
18
+ post('restart', {scope: scopes})
19
+ end
20
+
21
+ def destroy(current_name)
22
+ delete(nil, {current_name: current_name})
23
+ end
24
+
25
+ def destroy!
26
+ destroy(prefix)
27
+ end
28
+
29
+ def rename(new_name, current_name)
30
+ post('rename', {new_name: new_name, current_name: current_name})
31
+ end
32
+ def rename!(new_name)
33
+ rename(new_name, prefix)
34
+ end
35
+
36
+ def transfer(email)
37
+ patch(nil, {app: {owner: {email: email}}})
38
+ end
39
+
40
+ def logs_url
41
+ get('logs')['logs_url']
42
+ end
43
+
44
+ def logs
45
+ Scalingo::Logs.new(self)
46
+ end
47
+
48
+ def run(command, env = {})
49
+ post('run', {command: command, env: env})
50
+ end
51
+
52
+ resources :addons
53
+ resources :collaborators
54
+ resources :deployments
55
+ resources :domains
56
+ resources :variables
57
+ resources :events, collection_only: true
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,15 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Collaborators < Collection
4
+ def create(email)
5
+ post(nil, {collaborator: {email: email}})
6
+ end
7
+ end
8
+ class Collaborator < Resource
9
+ def destroy
10
+ delete
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,12 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Deployments < Collection
4
+ end
5
+ class Deployment < Resource
6
+ def output
7
+ get('output')
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,23 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Domains < Collection
4
+ def create(name, tlscert = nil, tlskey = nil)
5
+ if tlskey
6
+ post(nil, {domain: {name: name, tlscert: tlscert, tlskey: tlskey}})
7
+ else
8
+ post(nil, {domain: {name: name}})
9
+ end
10
+ end
11
+ end
12
+ class Domain < Resource
13
+ def update(tlscert, tlskey)
14
+ path(nil, {domain: {tlscert: tlscert, tlskey: tlskey}})
15
+ end
16
+
17
+ def destroy
18
+ delete
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,7 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Events < Collection
4
+ end
5
+ end
6
+ end
7
+
@@ -0,0 +1,23 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ class Variables < Collection
4
+ def all(aliases = true)
5
+ get(nil, {aliases: aliases})[collection_name]
6
+ end
7
+
8
+ def create(name, value)
9
+ post(nil, {variable: {name: name, value: value}})
10
+ end
11
+ end
12
+ class Variable < Resource
13
+ def update(value)
14
+ patch(nil, {variable: {value: value}})
15
+ end
16
+
17
+ def destroy
18
+ delete
19
+ end
20
+ end
21
+ end
22
+ end
23
+
@@ -0,0 +1,94 @@
1
+ module Scalingo
2
+ module Endpoint
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ module ClassMethods
8
+ def resources(name, opts = {})
9
+ name = name.to_s
10
+ define_method(name.pluralize.underscore) do
11
+ Scalingo::Endpoint.const_get(name.pluralize.camelize).new(self)
12
+ end
13
+
14
+ return if opts[:collection_only]
15
+
16
+ define_method(name.singularize.underscore) do |id|
17
+ send(name.pluralize.underscore).find(id)
18
+ end
19
+ end
20
+ end
21
+
22
+ extend ClassMethods
23
+ resources :apps
24
+ resources :account_keys
25
+ resources :addon_providers, collection_only: true
26
+ resources :addon_categories, collection_only: true
27
+
28
+ module Base
29
+ attr_accessor :api
30
+ attr_accessor :prefix
31
+
32
+ def initialize(api, prefix = nil)
33
+ self.api = api
34
+ self.prefix = prefix || self.class.name.split('::').last.underscore.pluralize
35
+ end
36
+
37
+ Request::REQUEST_METHODS.each do |method|
38
+ define_method(method) do |path = nil, options = {}|
39
+ api.send(method, "#{prefix}/#{path}", options)
40
+ end
41
+ end
42
+ end
43
+
44
+ class Resource < OpenStruct
45
+ include Base
46
+ include Endpoint
47
+
48
+ def initialize(api, prefix, data = {})
49
+ Base.instance_method(:initialize).bind(self).call(api, prefix)
50
+ OpenStruct.instance_method(:initialize).bind(self).call(data)
51
+ end
52
+ end
53
+
54
+ class Collection
55
+ include Base
56
+ include Endpoint
57
+ include Enumerable
58
+
59
+ def all
60
+ get[collection_name].map{|r| resource_class.new(self, r[find_by], r)}
61
+ end
62
+ alias_method :to_a, :all
63
+
64
+ def each
65
+ block_given? ? all.each(&Proc.new) : all.each
66
+ end
67
+
68
+ def find(id)
69
+ detect{|r| r[find_by] == id}
70
+ end
71
+
72
+ def collection_name
73
+ @collection_name ||= self.class.name.underscore.split('/').last
74
+ end
75
+
76
+ def resource_class
77
+ @resource_class ||= begin
78
+ Scalingo::Endpoint.const_get(self.class.name.singularize.split('::').last)
79
+ rescue
80
+ OpenStruct
81
+ end
82
+ end
83
+
84
+ def find_by
85
+ 'id'
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ Dir[File.join(File.dirname(__FILE__), 'endpoint', '*.rb')].each do |endpoint|
92
+ require endpoint
93
+ end
94
+
@@ -0,0 +1,13 @@
1
+ module Scalingo
2
+ # Custom error class for rescuing from all Scalingo errors
3
+ class Error < StandardError; end
4
+
5
+ # Raised when Scalingo returns the HTTP status code 400
6
+ class BadRequest < Error; end
7
+
8
+ # Raised when Scalingo returns the HTTP status code 404
9
+ class NotFound < Error; end
10
+
11
+ # Raised when Scalingo returns the HTTP status code 500
12
+ class InternalServerError < Error; end
13
+ end
@@ -0,0 +1,36 @@
1
+ require_relative 'realtime/logs'
2
+
3
+ module Scalingo
4
+ class Logs < Client
5
+ attr_reader :app
6
+
7
+ def initialize(app)
8
+ super({endpoint: ''})
9
+ @app = app
10
+ end
11
+
12
+ def dump(lines = 10)
13
+ get('', n: lines)
14
+ end
15
+
16
+ def realtime
17
+ Scalingo::Realtime::Logs.new(app)
18
+ end
19
+
20
+ protected
21
+ def log_token
22
+ self.endpoint, log_token = app.logs_url.split('?')
23
+ @log_token = log_token.split('=').last
24
+ end
25
+
26
+ def parse_json
27
+ false
28
+ end
29
+
30
+ def request(method, path, options)
31
+ options.merge!(token: log_token)
32
+ super(method, path, options)
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,50 @@
1
+ require 'faye/websocket'
2
+ require 'eventmachine'
3
+
4
+ module Scalingo
5
+ module Realtime
6
+ class Logs
7
+ attr_reader :app
8
+
9
+ def initialize(app)
10
+ @app = app
11
+ @callbacks = []
12
+ end
13
+
14
+ def each_line(&block)
15
+ @callbacks << block
16
+ end
17
+
18
+ def start
19
+ each_line(&Proc.new) if block_given?
20
+
21
+ EM.run do
22
+ ws = Faye::WebSocket::Client.new(url)
23
+
24
+ ws.on :open do |event|
25
+ end
26
+
27
+ ws.on :message do |event|
28
+ data = JSON.parse(event.data)
29
+ if data['event'] == 'log'
30
+ @callbacks.each{|c| c.call(data['log'])}
31
+ end
32
+ end
33
+
34
+ ws.on :close do |event|
35
+ ws = nil
36
+ end
37
+
38
+ Signal.trap('INT') { EM.stop }
39
+ Signal.trap('TERM') { EM.stop }
40
+ end
41
+ end
42
+
43
+ protected
44
+ def url
45
+ "#{app.logs_url}&stream=true"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,32 @@
1
+ module Scalingo
2
+ module Request
3
+ REQUEST_METHODS = [
4
+ :get,
5
+ :post,
6
+ :patch,
7
+ :put,
8
+ :delete
9
+ ]
10
+
11
+ REQUEST_METHODS.each do |method|
12
+ define_method(method) do |path, options = {}|
13
+ request(method, path, options)
14
+ end
15
+ end
16
+
17
+ protected
18
+ def request(method, path, options)
19
+ response = connection.send(method) do |request|
20
+ case method
21
+ when :get, :delete
22
+ request.url(path, options)
23
+ when :post, :patch, :put
24
+ request.path = path
25
+ request.body = options if !options.empty?
26
+ end
27
+ end
28
+ return response.body
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,4 @@
1
+ module Scalingo
2
+ VERSION = '1.0.0.alpha1'
3
+ end
4
+
data/lib/scalingo.rb ADDED
@@ -0,0 +1,26 @@
1
+ require 'active_support/inflector'
2
+
3
+ require_relative 'scalingo/error'
4
+ require_relative 'scalingo/configuration'
5
+ require_relative 'scalingo/api'
6
+ require_relative 'scalingo/client'
7
+ require_relative 'scalingo/logs'
8
+ require_relative 'scalingo/version'
9
+
10
+ module Scalingo
11
+ extend Configuration
12
+
13
+ def self.client(options = {})
14
+ Scalingo::Client.new(options)
15
+ end
16
+
17
+ def self.method_missing(method, *args, &block)
18
+ return super unless client.respond_to?(method)
19
+ client.send(method, *args, &block)
20
+ end
21
+
22
+ def self.respond_to?(method)
23
+ return client.respond_to?(method) || super
24
+ end
25
+ end
26
+
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: scalingo-ruby-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.alpha1
5
+ platform: ruby
6
+ authors:
7
+ - Geoffroy Planquart
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ - - <=
21
+ - !ruby/object:Gem::Version
22
+ version: '0.9'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0.7'
30
+ - - <=
31
+ - !ruby/object:Gem::Version
32
+ version: '0.9'
33
+ - !ruby/object:Gem::Dependency
34
+ name: faraday_middleware
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: '0.8'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '0.8'
47
+ - !ruby/object:Gem::Dependency
48
+ name: multi_json
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.3
54
+ - - ~>
55
+ - !ruby/object:Gem::Version
56
+ version: '1.0'
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '>='
62
+ - !ruby/object:Gem::Version
63
+ version: 1.0.3
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: '1.0'
67
+ - !ruby/object:Gem::Dependency
68
+ name: faye-websocket
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ~>
72
+ - !ruby/object:Gem::Version
73
+ version: 0.9.2
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ~>
79
+ - !ruby/object:Gem::Version
80
+ version: 0.9.2
81
+ - !ruby/object:Gem::Dependency
82
+ name: activesupport
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ~>
86
+ - !ruby/object:Gem::Version
87
+ version: '3'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ version: '3'
95
+ description: Ruby wrapper around the web API of scalingo.io
96
+ email:
97
+ - geoffroy@planquart.fr
98
+ executables: []
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - lib/faraday/raise_http_exception.rb
103
+ - lib/scalingo.rb
104
+ - lib/scalingo/api.rb
105
+ - lib/scalingo/client.rb
106
+ - lib/scalingo/configuration.rb
107
+ - lib/scalingo/connection.rb
108
+ - lib/scalingo/endpoint.rb
109
+ - lib/scalingo/endpoint/account_keys.rb
110
+ - lib/scalingo/endpoint/addon_categories.rb
111
+ - lib/scalingo/endpoint/addon_provider.rb
112
+ - lib/scalingo/endpoint/addons.rb
113
+ - lib/scalingo/endpoint/apps.rb
114
+ - lib/scalingo/endpoint/collaborators.rb
115
+ - lib/scalingo/endpoint/deployments.rb
116
+ - lib/scalingo/endpoint/domains.rb
117
+ - lib/scalingo/endpoint/events.rb
118
+ - lib/scalingo/endpoint/variables.rb
119
+ - lib/scalingo/error.rb
120
+ - lib/scalingo/logs.rb
121
+ - lib/scalingo/realtime/logs.rb
122
+ - lib/scalingo/request.rb
123
+ - lib/scalingo/version.rb
124
+ homepage: https://github.com/Aethelflaed/scalingo-ruby-api
125
+ licenses:
126
+ - MIT
127
+ metadata: {}
128
+ post_install_message:
129
+ rdoc_options: []
130
+ require_paths:
131
+ - lib
132
+ required_ruby_version: !ruby/object:Gem::Requirement
133
+ requirements:
134
+ - - '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ required_rubygems_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - '>'
140
+ - !ruby/object:Gem::Version
141
+ version: 1.3.1
142
+ requirements: []
143
+ rubyforge_project:
144
+ rubygems_version: 2.4.6
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Ruby API for the awesome scalingo project !
148
+ test_files: []
149
+ has_rdoc: