xively-cli 0.0.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.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright © Heroku 2008 - 2012
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # Xively CLI
2
+
3
+ A Xively client tool
4
+
5
+ [![Build Status](https://secure.travis-ci.org/levent/xively-cli.png)](http://travis-ci.org/levent/xively-cli)
6
+ [![Dependency Status](https://gemnasium.com/levent/xively-cli.png)](https://gemnasium.com/levent/xively-cli)
7
+ [![Coverage Status](https://coveralls.io/repos/levent/xively-cli/badge.png)](https://coveralls.io/r/levent/xively-cli)
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ gem install xively
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ### Connect to the socket server
18
+
19
+ You can connect to a feed or datastream and receive realtime json updates in your terminal
20
+
21
+ ```bash
22
+ xively subscribe -k YOUR_API_KEY -f FEED_ID -d DATASTREAM_ID
23
+ ```
24
+
data/bin/xively ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ begin
5
+ # resolve bin path, ignoring symlinks
6
+ require "pathname"
7
+ bin_file = Pathname.new(__FILE__).realpath
8
+
9
+ # add self to libpath
10
+ $:.unshift File.expand_path("../../lib", bin_file)
11
+
12
+ # start up the CLI
13
+ require "xively-cli"
14
+ Xively::CLI.start(*ARGV)
15
+ rescue Interrupt
16
+ `stty icanon echo`
17
+ puts("\n ! Command cancelled.")
18
+ end
data/lib/xively-cli.rb ADDED
@@ -0,0 +1,13 @@
1
+ require "xively-cli/command"
2
+
3
+ module Xively
4
+ class CLI
5
+
6
+ def self.start(*args)
7
+ command = args.shift.strip rescue "help"
8
+ Xively::Command.load
9
+ Xively::Command.run(command, args)
10
+ end
11
+
12
+ end
13
+ end
File without changes
@@ -0,0 +1,168 @@
1
+ require 'xively-cli/version'
2
+ require "optparse"
3
+
4
+ module Xively
5
+ module Command
6
+ class CommandFailed < RuntimeError; end
7
+
8
+ def self.load
9
+ Dir[File.join(File.dirname(__FILE__), "command", "*.rb")].each do |file|
10
+ require file
11
+ end
12
+ end
13
+
14
+ def self.commands
15
+ @@commands ||= {}
16
+ end
17
+
18
+ def self.command_aliases
19
+ @@command_aliases ||= {}
20
+ end
21
+
22
+ def self.files
23
+ @@files ||= Hash.new {|hash,key| hash[key] = File.readlines(key).map {|line| line.strip}}
24
+ end
25
+
26
+ def self.namespaces
27
+ @@namespaces ||= {}
28
+ end
29
+
30
+ def self.register_command(command)
31
+ commands[command[:command]] = command
32
+ end
33
+
34
+ def self.register_namespace(namespace)
35
+ namespaces[namespace[:name]] = namespace
36
+ end
37
+
38
+ def self.current_command
39
+ @current_command
40
+ end
41
+
42
+ def self.current_command=(new_current_command)
43
+ @current_command = new_current_command
44
+ end
45
+
46
+ def self.global_options
47
+ @global_options ||= []
48
+ end
49
+
50
+ def self.invalid_arguments
51
+ @invalid_arguments
52
+ end
53
+
54
+ def self.shift_argument
55
+ @invalid_arguments.shift rescue nil
56
+ end
57
+
58
+ def self.validate_arguments!
59
+ unless invalid_arguments.empty?
60
+ arguments = invalid_arguments.map {|arg| "\"#{arg}\""}
61
+ if arguments.length == 1
62
+ message = "Invalid argument: #{arguments.first}"
63
+ elsif arguments.length > 1
64
+ message = "Invalid arguments: "
65
+ message << arguments[0...-1].join(", ")
66
+ message << " and "
67
+ message << arguments[-1]
68
+ end
69
+ $stderr.puts(message)
70
+ run(current_command, ["--help"])
71
+ exit(1)
72
+ end
73
+ end
74
+
75
+ def self.warnings
76
+ @warnings ||= []
77
+ end
78
+
79
+ def self.display_warnings
80
+ unless warnings.empty?
81
+ $stderr.puts(warnings.map {|warning| " ! #{warning}"}.join("\n"))
82
+ end
83
+ end
84
+
85
+ def self.global_option(name, *args, &blk)
86
+ global_options << { :name => name, :args => args, :proc => blk }
87
+ end
88
+
89
+ global_option :key, "--key API_KEY", "-k"
90
+ global_option :help, "--help", "-h"
91
+
92
+ def self.prepare_run(cmd, args=[])
93
+ command = parse(cmd)
94
+
95
+ unless command
96
+ if %w( -v --version ).include?(cmd)
97
+ command = parse('version')
98
+ else
99
+ $stderr.puts(["`#{cmd}` is not a xively command.", "See `xively help` for a list of available commands."].join("\n"))
100
+ exit(1)
101
+ end
102
+ end
103
+
104
+ @current_command = cmd
105
+
106
+ opts = {}
107
+ invalid_options = []
108
+
109
+ parser = OptionParser.new do |parser|
110
+ # overwrite OptionParsers Officious['version'] to avoid conflicts
111
+ # see: https://github.com/ruby/ruby/blob/trunk/lib/optparse.rb#L814
112
+ parser.on("--version") do |value|
113
+ invalid_options << "--version"
114
+ end
115
+ global_options.each do |global_option|
116
+ parser.on(*global_option[:args]) do |value|
117
+ global_option[:proc].call(value) if global_option[:proc]
118
+ opts[global_option[:name]] = value
119
+ end
120
+ end
121
+ command[:options].each do |name, option|
122
+ parser.on("-#{option[:short]}", "--#{option[:long]}", option[:desc]) do |value|
123
+ opts[name.gsub("-", "_").to_sym] = value
124
+ end
125
+ end
126
+ end
127
+
128
+ begin
129
+ parser.order!(args) do |nonopt|
130
+ invalid_options << nonopt
131
+ end
132
+ rescue OptionParser::InvalidOption => ex
133
+ invalid_options << ex.args.first
134
+ retry
135
+ end
136
+
137
+ if opts[:help]
138
+ args.unshift cmd unless cmd =~ /^-.*/
139
+ cmd = "help"
140
+ command = parse(cmd)
141
+ end
142
+
143
+ args.concat(invalid_options)
144
+
145
+ @current_args = args
146
+ @current_options = opts
147
+ @invalid_arguments = invalid_options
148
+
149
+ [ command[:klass].new(args.dup, opts.dup), command[:method] ]
150
+ end
151
+
152
+ def self.run(cmd, arguments=[])
153
+ object, method = prepare_run(cmd, arguments.dup)
154
+ object.send(method)
155
+ rescue CommandFailed => e
156
+ $stderr.puts e.message
157
+ rescue OptionParser::ParseError
158
+ commands[cmd] ? run("help", [cmd]) : run("help")
159
+ ensure
160
+ display_warnings
161
+ end
162
+
163
+ def self.parse(cmd)
164
+ commands[cmd] || commands[command_aliases[cmd]]
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,148 @@
1
+ require "fileutils"
2
+ require "xively-cli/command"
3
+
4
+ class Xively::Command::Base
5
+
6
+ def self.namespace
7
+ self.to_s.split("::").last.downcase
8
+ end
9
+
10
+ attr_reader :args
11
+ attr_reader :options
12
+
13
+ def initialize(args=[], options={})
14
+ @args = args
15
+ @options = options
16
+ end
17
+
18
+ protected
19
+
20
+ def self.inherited(klass)
21
+ unless klass == Xively::Command::Base
22
+ help = extract_help_from_caller(caller.first)
23
+
24
+ Xively::Command.register_namespace(
25
+ :name => klass.namespace,
26
+ :description => help.first
27
+ )
28
+ end
29
+ end
30
+
31
+ def self.method_added(method)
32
+ return if self == Xively::Command::Base
33
+ return if private_method_defined?(method)
34
+ return if protected_method_defined?(method)
35
+
36
+ help = extract_help_from_caller(caller.first)
37
+ resolved_method = (method.to_s == "index") ? nil : method.to_s
38
+ command = [ self.namespace, resolved_method ].compact.join(":")
39
+ banner = extract_banner(help) || command
40
+
41
+ Xively::Command.register_command(
42
+ :klass => self,
43
+ :method => method,
44
+ :namespace => self.namespace,
45
+ :command => command,
46
+ :banner => banner.strip,
47
+ :help => help.join("\n"),
48
+ :summary => extract_summary(help),
49
+ :description => extract_description(help),
50
+ :options => extract_options(help)
51
+ )
52
+ end
53
+
54
+ def self.alias_command(new, old)
55
+ raise "no such command: #{old}" unless Xively::Command.commands[old]
56
+ Xively::Command.command_aliases[new] = old
57
+ end
58
+
59
+ #
60
+ # Parse the caller format and identify the file and line number as identified
61
+ # in : http://www.ruby-doc.org/core/classes/Kernel.html#M001397. This will
62
+ # look for a colon followed by a digit as the delimiter. The biggest
63
+ # complication is windows paths, which have a color after the drive letter.
64
+ # This regex will match paths as anything from the beginning to a colon
65
+ # directly followed by a number (the line number).
66
+ #
67
+ def self.extract_help_from_caller(line)
68
+ # pull out of the caller the information for the file path and line number
69
+ if line =~ /^(.+?):(\d+)/
70
+ extract_help($1, $2)
71
+ else
72
+ raise("unable to extract help from caller: #{line}")
73
+ end
74
+ end
75
+
76
+ def self.extract_help(file, line_number)
77
+ buffer = []
78
+ lines = Xively::Command.files[file]
79
+
80
+ (line_number.to_i-2).downto(0) do |i|
81
+ line = lines[i]
82
+ case line[0..0]
83
+ when ""
84
+ when "#"
85
+ buffer.unshift(line[1..-1])
86
+ else
87
+ break
88
+ end
89
+ end
90
+
91
+ buffer
92
+ end
93
+
94
+ def self.extract_banner(help)
95
+ help.first
96
+ end
97
+
98
+ def self.extract_summary(help)
99
+ extract_description(help).split("\n")[2].to_s.split("\n").first
100
+ end
101
+
102
+ def self.extract_description(help)
103
+ help.reject do |line|
104
+ line =~ /^\s+-(.+)#(.+)/
105
+ end.join("\n")
106
+ end
107
+
108
+ def self.extract_options(help)
109
+ help.select do |line|
110
+ line =~ /^\s+-(.+)#(.+)/
111
+ end.inject({}) do |hash, line|
112
+ description = line.split("#", 2).last
113
+ long = line.match(/--([A-Za-z\- ]+)/)[1].strip
114
+ short = line.match(/-([A-Za-z ])[ ,]/) && $1 && $1.strip
115
+ hash.update(long.split(" ").first => { :desc => description, :short => short, :long => long })
116
+ end
117
+ end
118
+
119
+ def current_command
120
+ Xively::Command.current_command
121
+ end
122
+
123
+ def extract_option(key)
124
+ options[key.dup.gsub('-','').to_sym]
125
+ end
126
+
127
+ def invalid_arguments
128
+ Xively::Command.invalid_arguments
129
+ end
130
+
131
+ def shift_argument
132
+ Xively::Command.shift_argument
133
+ end
134
+
135
+ def validate_arguments!
136
+ Xively::Command.validate_arguments!
137
+ end
138
+
139
+ def escape(value)
140
+ xively.escape(value)
141
+ end
142
+ end
143
+
144
+ module Xively::Command
145
+ unless const_defined?(:BaseWithApp)
146
+ BaseWithApp = Base
147
+ end
148
+ end
@@ -0,0 +1,46 @@
1
+ require 'xively-cli/command/base'
2
+ require 'xively-rb'
3
+
4
+ # Create a Xively feed
5
+ #
6
+ class Xively::Command::Feeds < Xively::Command::Base
7
+
8
+ # feeds:create [TITLE]
9
+ #
10
+ # create a Xively feed
11
+ #
12
+ # -k, --key API_KEY # your api key
13
+ #
14
+ #Examples:
15
+ #
16
+ # $ xively feeds:create "Home energy monitor" -k ABCD1234
17
+ #
18
+ def create
19
+ title = shift_argument
20
+ api_key = options[:key]
21
+ validate_arguments!
22
+
23
+ if title.nil?
24
+ $stderr.puts "Please provide a title"
25
+ exit(1)
26
+ end
27
+
28
+ if api_key.nil?
29
+ $stderr.puts Xively::Command::Help.usage_for_command("feeds:create")
30
+ exit(1)
31
+ end
32
+
33
+ puts "Creating feed \"#{title}\"..."
34
+
35
+ feed = Xively::Feed.new
36
+ feed.title = title
37
+
38
+ response = Xively::Client.post('/v2/feeds.json', :headers => {'X-ApiKey' => api_key}, :body => feed.to_json)
39
+ if response.code == 201
40
+ puts "Your feed has been created at:"
41
+ puts response.headers['location']
42
+ end
43
+ end
44
+
45
+ alias_command "create", "feeds:create"
46
+ end
@@ -0,0 +1,121 @@
1
+ # list commands and display help
2
+ #
3
+ class Xively::Command::Help < Xively::Command::Base
4
+
5
+ PRIMARY_NAMESPACES = %w( subscribe )
6
+
7
+ # help [COMMAND]
8
+ #
9
+ # list available commands or display help for a specific command
10
+ #
11
+ #Examples:
12
+ #
13
+ # $ xively help
14
+ # Usage: xively COMMAND [--key API_KEY] [command-specific-options]
15
+ #
16
+ # Primary help topics, type "xively help TOPIC" for more details:
17
+ #
18
+ # subscribe # subscribe to a datastream
19
+ # ...
20
+ #
21
+ def index
22
+ if command = args.shift
23
+ help_for_command(command)
24
+ else
25
+ help_for_root
26
+ end
27
+ end
28
+
29
+ alias_command "-h", "help"
30
+ alias_command "--help", "help"
31
+
32
+ def self.usage_for_command(command)
33
+ command = new.send(:commands)[command]
34
+ "Usage: xively #{command[:help]}" if command
35
+ end
36
+
37
+ private
38
+
39
+ def commands_for_namespace(name)
40
+ Xively::Command.commands.values.select do |command|
41
+ command[:namespace] == name && command[:command] != name
42
+ end
43
+ end
44
+
45
+ def namespaces
46
+ namespaces = Xively::Command.namespaces
47
+ namespaces.delete("app")
48
+ namespaces
49
+ end
50
+
51
+ def commands
52
+ Xively::Command.commands
53
+ end
54
+
55
+ def primary_namespaces
56
+ PRIMARY_NAMESPACES.map { |name| namespaces[name] }.compact
57
+ end
58
+
59
+ def additional_namespaces
60
+ (namespaces.values - primary_namespaces)
61
+ end
62
+
63
+ def summary_for_namespaces(namespaces)
64
+ size = (namespaces.map { |n| n[:name] }).map { |i| i.to_s.length }.sort.last
65
+ namespaces.sort_by {|namespace| namespace[:name]}.each do |namespace|
66
+ name = namespace[:name]
67
+ namespace[:description]
68
+ puts " %-#{size}s # %s" % [ name, namespace[:description] ]
69
+ end
70
+ end
71
+
72
+ def help_for_root
73
+ puts "Usage: xively COMMAND [--key API_KEY] [command-specific-options]"
74
+ puts
75
+ puts "Primary help topics, type \"xively help TOPIC\" for more details:"
76
+ puts
77
+ summary_for_namespaces(primary_namespaces)
78
+ puts
79
+ puts "Additional topics:"
80
+ puts
81
+ summary_for_namespaces(additional_namespaces)
82
+ puts
83
+ end
84
+
85
+ def help_for_namespace(name)
86
+ namespace_commands = commands_for_namespace(name)
87
+
88
+ unless namespace_commands.empty?
89
+ size = (namespace_commands.map { |c| c[:banner] }).map { |i| i.to_s.length }.sort.last
90
+ namespace_commands.sort_by { |c| c[:banner].to_s }.each do |command|
91
+ next if command[:help] =~ /DEPRECATED/
92
+ command[:summary]
93
+ puts " %-#{size}s # %s" % [ command[:banner], command[:summary] ]
94
+ end
95
+ end
96
+ end
97
+
98
+ def help_for_command(name)
99
+ if command_alias = Xively::Command.command_aliases[name]
100
+ puts("Alias: #{name} is short for #{command_alias}")
101
+ name = command_alias
102
+ end
103
+ if command = commands[name]
104
+ puts "Usage: xively #{command[:banner]}"
105
+
106
+ if command[:help].strip.length > 0
107
+ puts command[:help].split("\n")[1..-1].join("\n")
108
+ end
109
+ puts
110
+ end
111
+
112
+ if commands_for_namespace(name).size > 0
113
+ puts "Additional commands, type \"xively help COMMAND\" for more details:"
114
+ puts
115
+ help_for_namespace(name)
116
+ puts
117
+ elsif command.nil?
118
+ puts "#{name} is not a xively command. See `xively help`."
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,70 @@
1
+ require 'xively-cli/command/base'
2
+ require 'socket'
3
+
4
+ # Subscribe to a feed or datastream via the Xively Socket Server
5
+ #
6
+ class Xively::Command::Subscribe < Xively::Command::Base
7
+
8
+ # subscribe
9
+ #
10
+ # connect to a tcp socket for a feed or datastream
11
+ #
12
+ # -k, --key API_KEY # your api key
13
+ # -f, --feed FEED # the feed id
14
+ # -d, --datastream DATASTREAM # the datastream id (optional)
15
+ #
16
+ #Example:
17
+ #
18
+ # $ xively subscribe -k ABCD1234 -f 504 -d 0 # subscribe to datastream
19
+ # $ xively subscribe -k ABCD1234 -f 504 # subscribe to feed
20
+ #
21
+ def index
22
+ api_key = options[:key]
23
+ feed_id = options[:feed]
24
+ datastream_id = options[:datastream]
25
+
26
+ validate_arguments!
27
+
28
+ unless api_key && feed_id
29
+ $stderr.puts Xively::Command::Help.usage_for_command("subscribe")
30
+ exit(1)
31
+ end
32
+
33
+ resource = "/feeds/#{feed_id}"
34
+ resource += "/datastreams/#{datastream_id}" if datastream_id
35
+
36
+ puts "Subscribing to updates for #{resource}"
37
+
38
+ subscribe = "{\"method\":\"subscribe\", \"resource\":\"#{resource}\", \"headers\":{\"X-ApiKey\":\"#{api_key}\"}}"
39
+ s = TCPSocket.new 'api.xively.com', 8081
40
+ s.puts subscribe
41
+ while line = s.gets
42
+ parse_data(line, s)
43
+ end
44
+ s.close
45
+
46
+ puts "Connection closed"
47
+
48
+ # EventMachine.run {
49
+ # EventMachine::connect 'api.xively.com', 8081, XivelySocket, options
50
+ # }
51
+ rescue Interrupt => e
52
+ puts "Closing connection"
53
+ s.close
54
+ raise e
55
+ end
56
+
57
+ alias_command "sub", "subscribe"
58
+
59
+ protected
60
+
61
+ def parse_data(string, socket)
62
+ puts(string)
63
+ if string =~ /"status":40/
64
+ socket.close
65
+ exit(1)
66
+ end
67
+ end
68
+
69
+ end
70
+
@@ -0,0 +1,23 @@
1
+ require "xively-cli/command/base"
2
+ require "xively-cli/version"
3
+
4
+ # display version
5
+ #
6
+ class Xively::Command::Version < Xively::Command::Base
7
+
8
+ # version
9
+ #
10
+ # show xively client version
11
+ #
12
+ #Example:
13
+ #
14
+ # $ xively version
15
+ # v0.0.1
16
+ #
17
+ def index
18
+ validate_arguments!
19
+
20
+ puts(Xively::VERSION)
21
+ end
22
+
23
+ end
@@ -0,0 +1,5 @@
1
+ module Xively
2
+ class CLI
3
+ VERSION = "0.0.9"
4
+ end
5
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xively-cli
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.9
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Levent Ali
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: xively-rb
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0.2'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0.2'
30
+ description: Xively CLI
31
+ email: lebreeze@gmail.com
32
+ executables:
33
+ - xively
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - lib/xively-cli/cli.rb
38
+ - lib/xively-cli/command/base.rb
39
+ - lib/xively-cli/command/feeds.rb
40
+ - lib/xively-cli/command/help.rb
41
+ - lib/xively-cli/command/subscribe.rb
42
+ - lib/xively-cli/command/version.rb
43
+ - lib/xively-cli/command.rb
44
+ - lib/xively-cli/version.rb
45
+ - lib/xively-cli.rb
46
+ - LICENSE
47
+ - README.md
48
+ - bin/xively
49
+ homepage: https://github.com/levent/xively-cli
50
+ licenses:
51
+ - MIT
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ segments:
63
+ - 0
64
+ hash: -2041254343982566204
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ segments:
72
+ - 0
73
+ hash: -2041254343982566204
74
+ requirements: []
75
+ rubyforge_project:
76
+ rubygems_version: 1.8.23
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Provides a client to Xively
80
+ test_files: []
81
+ has_rdoc: