obvious 0.0.3 → 0.0.5

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/bin/obvious CHANGED
@@ -2,293 +2,6 @@
2
2
 
3
3
  require 'obvious'
4
4
 
5
- require 'yaml'
6
-
7
- module Obvious
8
- # Your code goes here...
9
- def self.generate
10
- puts 'generate the codes!a'
11
-
12
-
13
-
14
- #`rm -rf app`
15
-
16
- app_dir = 'app'
17
-
18
- counter = 1
19
- while File.directory? app_dir
20
- app_dir = "app_#{counter}"
21
- counter += 1
22
- end
23
-
24
- puts "Generating application at: #{app_dir}"
25
-
26
- dirs = ['/', '/actions', '/contracts', '/entities',
27
- '/spec', '/spec/actions', '/spec/contracts', '/spec/entities',
28
- '/spec/doubles']
29
-
30
- dirs.each do |dir|
31
- Dir.mkdir app_dir + dir
32
- end
33
-
34
-
35
- target_path = File.realpath Dir.pwd
36
- spec = Gem::Specification.find_by_name("obvious")
37
- gem_root = spec.gem_dir
38
- gem_lib = gem_root + "/lib"
39
-
40
- #`cp #{gem_lib}/obvious/files/contract.rb #{target_path}/app/contracts/contract.rb`
41
- `cp #{gem_lib}/obvious/files/Rakefile #{target_path}/Rakefile`
42
- entities = Hash.new
43
- jacks = Hash.new
44
-
45
- files = Dir['descriptors/*.yml']
46
-
47
- files.each do |file|
48
- action = YAML.load_file file
49
- code = ''
50
- #puts action.inspect
51
-
52
- local_jacks = Hash.new
53
- local_entities = Hash.new
54
-
55
- action['Code'].each do |entry|
56
- code << " \# #{entry['c']}\n"
57
- code << " \# use: #{entry['requires']}\n" if entry['requires']
58
- code << " \n"
59
-
60
- if entry['requires']
61
- requires = entry['requires'].split(',')
62
- requires.each do |req|
63
- req.strip!
64
- info = req.split '.'
65
-
66
- if info[0].index 'Jack'
67
- unless jacks[info[0]]
68
- jacks[info[0]] = []
69
- end
70
-
71
- unless local_jacks[info[0]]
72
- local_jacks[info[0]] = []
73
- end
74
-
75
- jacks[info[0]] << info[1]
76
- local_jacks[info[0]] << info[1]
77
- else
78
- unless entities[info[0]]
79
- entities[info[0]] = []
80
- end
81
-
82
- unless local_entities[info[0]]
83
- local_entities[info[0]] = []
84
- end
85
-
86
- entities[info[0]] << info[1]
87
- local_entities[info[0]] << info[1]
88
- end
89
-
90
- end
91
- end
92
-
93
- end
94
-
95
-
96
- jack_inputs = ''
97
- jack_assignments = ''
98
-
99
- local_jacks.each do |k, v|
100
- name = k.chomp('Jack').downcase
101
- jack_inputs << "#{name}_jack, "
102
- jack_assignments << " @#{name}_jack = #{name}_jack\n"
103
- end
104
-
105
- jack_inputs.chomp! ', '
106
-
107
- entity_requires = ''
108
-
109
- local_entities.each do |k, v|
110
- name = k.downcase
111
- entity_requires << "require_relative '../entities/#{name}'\n"
112
- end
113
-
114
-
115
- output = <<FIN
116
- #{entity_requires}
117
- class #{action['Action']}
118
-
119
- def initialize #{jack_inputs}
120
- #{jack_assignments} end
121
-
122
- def do input
123
- #{code} end
124
-
125
- end
126
- FIN
127
- snake_name = action['Action'].gsub(/(.)([A-Z])/,'\1_\2').downcase
128
-
129
- filename = "#{app_dir}/actions/#{snake_name}.rb"
130
- File.open(filename, 'w') {|f| f.write(output) }
131
-
132
- #puts output
133
-
134
- output = <<FIN
135
- require_relative '../../actions/#{snake_name}'
136
-
137
- describe #{action['Action']} do
138
-
139
- it '#{action['Description']}'
140
-
141
- it 'should raise an error with invalid input'
142
-
143
- end
144
-
145
-
146
- FIN
147
-
148
- filename = "#{app_dir}/spec/actions/#{snake_name}_spec.rb"
149
- File.open(filename, 'w') {|f| f.write(output) }
150
-
151
- #puts output
152
- end
153
-
154
-
155
- #filter out duplicate methods
156
-
157
- entities.each do |k, v|
158
- v.uniq!
159
- end
160
-
161
- jacks.each do |k,v|
162
- v.uniq!
163
- end
164
-
165
- #puts entities.inspect
166
- #puts jacks.inspect
167
-
168
- entities.each do |k, v|
169
- name = k
170
- method_specs = ''
171
- method_definitions = ''
172
-
173
- v.each do |method|
174
- method_definitions << "
175
- def #{method} input
176
- nil
177
- end
178
- "
179
-
180
- method_specs << "
181
- describe '.#{method}' do
182
- it 'should #{method} with valid input'
183
-
184
- it 'should raise an error with invalid input'
185
-
186
- end
187
- "
188
-
189
- end
190
-
191
- output = <<FIN
192
- class #{name}
193
- #{method_definitions}
194
- end
195
- FIN
196
- snake_name = name.gsub(/(.)([A-Z])/,'\1_\2').downcase
197
-
198
- filename = "#{app_dir}/entities/#{snake_name}.rb"
199
- File.open(filename, 'w') {|f| f.write(output) }
200
-
201
- output = <<FIN
202
- require_relative '../../entities/#{snake_name}'
203
-
204
- describe #{name} do
205
- #{method_specs}
206
- end
207
-
208
-
209
- FIN
210
- filename = "#{app_dir}/spec/entities/#{snake_name}_spec.rb"
211
- File.open(filename, 'w') {|f| f.write(output) }
212
-
213
-
214
- #puts output
215
- end
216
-
217
-
218
-
219
- jacks.each do |k, v|
220
-
221
- name = k.chomp('Jack').downcase
222
-
223
- method_specs = ''
224
- method_definitions = ''
225
-
226
- v.each do |method|
227
-
228
- method_definitions << "
229
- def #{method}_contract input
230
- input_shape = {}
231
- output_shape = {}
232
- call_method :#{method}_alias, input, input_shape, output_shape
233
- end
234
- "
235
-
236
- method_specs << "
237
- describe '.#{method}_contract' do
238
- it 'should #{method} data with valid input'
239
-
240
- it 'should raise an error with invalid input'
241
-
242
- it 'should raise an error with invalid output'
243
-
244
- end
245
- "
246
-
247
- end
248
-
249
-
250
- output = <<FIN
251
- require 'obvious'
252
-
253
- class #{k}Contract < Contract
254
- contracts :#{v.join(', :')}
255
- #{method_definitions}
256
- end
257
- FIN
258
-
259
- snake_name = name.gsub(/(.)([A-Z])/,'\1_\2').downcase
260
-
261
- filename = "#{app_dir}/contracts/#{snake_name}_jack_contract.rb"
262
- File.open(filename, 'w') {|f| f.write(output) }
263
-
264
- #puts output
265
-
266
- output = <<FIN
267
- require_relative '../../contracts/#{snake_name}_jack_contract'
268
-
269
- describe #{k}Contract do
270
- #{method_specs}
271
- end
272
-
273
- FIN
274
-
275
- filename = "#{app_dir}/spec/contracts/#{snake_name}_jack_contract_spec.rb"
276
- File.open(filename, 'w') {|f| f.write(output) }
277
-
278
- #puts output
279
- end
280
-
281
-
282
-
283
-
284
- end
285
- end
286
-
287
-
288
-
289
5
  if ARGV[0] == 'generate'
290
- Obvious::generate
6
+ Obvious::Generators::ApplicationGenerator.generate
291
7
  end
292
-
293
-
294
-
@@ -0,0 +1,159 @@
1
+ require 'yaml'
2
+
3
+ require_relative 'helpers/application'
4
+ require_relative 'descriptor'
5
+
6
+ module Obvious
7
+ module Generators
8
+ class ApplicationGenerator
9
+ class << self
10
+ def generate
11
+ @app = Obvious::Generators::Application.instance
12
+
13
+ puts 'Generating the files...'
14
+
15
+ Obvious::Generators::ApplicationGenerator.create_directories ['/', '/actions', '/contracts', '/entities',
16
+ '/spec', '/spec/actions', '/spec/contracts', '/spec/entities',
17
+ '/spec/doubles']
18
+
19
+ unless File.exist? "#{@app.target_path}/Rakefile"
20
+ puts 'Creating Rakefile...'
21
+ `cp #{@app.lib_path}/obvious/files/Rakefile #{@app.target_path}/Rakefile`
22
+ end
23
+
24
+ unless File.exist? "#{@app.target_path}/external"
25
+ puts 'Creating external directory'
26
+ `cp -r #{@app.lib_path}/obvious/files/external #{@app.target_path}/external`
27
+ end
28
+
29
+ descriptors = Dir['descriptors/*.yml']
30
+
31
+ puts 'Creating actions from descriptors... ' unless descriptors.length.zero?
32
+ descriptors.each do |file|
33
+ descriptor = Obvious::Generators::Descriptor.new file
34
+ descriptor.to_file
35
+ end
36
+
37
+ @app.remove_duplicates
38
+
39
+ puts 'Writing Entities scaffolds... '
40
+ Obvious::Generators::ApplicationGenerator.write_entities
41
+
42
+ puts 'Writing jacks scaffolds... '
43
+ Obvious::Generators::ApplicationGenerator.write_jacks
44
+
45
+ puts "Files are located in the `#{@app.dir}` directory."
46
+ end # ::generate
47
+
48
+ def create_directories directories
49
+ directories.each do |dir|
50
+ Dir.mkdir @app.dir + dir
51
+ end
52
+ end
53
+
54
+ def write_entities
55
+ @app.entities.each do |k, v|
56
+ name = k
57
+ method_specs = ''
58
+ method_defs = ''
59
+
60
+ method_defs << '
61
+ def self.shape
62
+ {}
63
+ end
64
+ '
65
+
66
+ v.each do |method|
67
+ method_defs << "
68
+ def #{method} input
69
+ nil
70
+ end
71
+ "
72
+
73
+ method_specs << "
74
+ describe '.#{method}' do
75
+ it 'should #{method} with valid input'
76
+
77
+ it 'should raise an error with invalid input'
78
+
79
+ end
80
+ "
81
+ end
82
+
83
+ output = %Q{class #{name}
84
+ #{method_defs}
85
+ end
86
+ }
87
+ snake_name = name.gsub(/(.)([A-Z])/,'\1_\2').downcase
88
+
89
+ filename = "#{@app.dir}/entities/#{snake_name}.rb"
90
+ File.open(filename, 'w') { |f| f.write(output) }
91
+
92
+ output = %Q{require_relative '../../entities/#{snake_name}'
93
+
94
+ describe #{name} do
95
+ #{method_specs}
96
+ end
97
+ }
98
+
99
+ filename = "#{@app.dir}/spec/entities/#{snake_name}_spec.rb"
100
+ File.open(filename, 'w') {|f| f.write(output) }
101
+ end
102
+ end #write_entities
103
+
104
+ def write_jacks
105
+ @app.jacks.each do |k, v|
106
+ name = k.chomp('Jack').downcase
107
+ method_specs = ''
108
+ method_defs = ''
109
+
110
+ v.each do |method|
111
+
112
+ method_defs << "
113
+ def #{method}_contract input
114
+ input_shape = {}
115
+ output_shape = {}
116
+ call_method :#{method}_alias, input, input_shape, output_shape
117
+ end
118
+ "
119
+
120
+ method_specs << "
121
+ describe '.#{method}_contract' do
122
+ it 'should #{method} data with valid input'
123
+
124
+ it 'should raise an error with invalid input'
125
+
126
+ it 'should raise an error with invalid output'
127
+
128
+ end
129
+ "
130
+ end
131
+
132
+ output = %Q{require 'obvious'
133
+
134
+ class #{k}Contract < Contract
135
+ contracts :#{ v.join(', :')}
136
+ #{method_defs}
137
+ end
138
+ }
139
+
140
+ snake_name = name.gsub(/(.)([A-Z])/,'\1_\2').downcase
141
+
142
+ filename = "#{@app.dir}/contracts/#{snake_name}_jack_contract.rb"
143
+ File.open(filename, 'w') {|f| f.write(output) }
144
+
145
+ output = %Q{require_relative '../../contracts/#{snake_name}_jack_contract'
146
+
147
+ describe #{k}Contract do
148
+ #{method_specs}
149
+ end
150
+ }
151
+
152
+ filename = "#{@app.dir}/spec/contracts/#{snake_name}_jack_spec.rb"
153
+ File.open(filename, 'w') {|f| f.write(output) }
154
+ end
155
+ end # generate_jacks_code
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,107 @@
1
+ require 'yaml'
2
+
3
+ require_relative 'helpers/application'
4
+
5
+ module Obvious
6
+ module Generators
7
+ class Descriptor
8
+ def initialize descriptor
9
+ @descriptor = descriptor
10
+ end
11
+
12
+ def to_file
13
+ action = YAML.load_file @descriptor
14
+ @jacks, @entities = {}, {}
15
+ @code = ''
16
+
17
+ action['Code'].each do |entry|
18
+ write_comments_for entry
19
+ process_requirements_for entry if entry['requires']
20
+ end
21
+
22
+ write_action action
23
+ end
24
+
25
+ private
26
+ def write_comments_for entry
27
+ @code << " \# #{entry['c']}\n"
28
+ @code << " \# use: #{entry['requires']}\n" if entry['requires']
29
+ @code << " \n"
30
+ end
31
+
32
+ def process_requirements_for entry
33
+ app = Obvious::Generators::Application.instance
34
+ requires = entry['requires'].split ','
35
+
36
+ requires.each do |req|
37
+ req.strip!
38
+ infos = req.split '.'
39
+
40
+ if infos[0].index 'Jack'
41
+ app.jacks[infos[0]] = [] unless app.jacks[infos[0]]
42
+ @jacks[infos[0]] = [] unless @jacks[infos[0]]
43
+
44
+ app.jacks[infos[0]] << infos[1]
45
+ @jacks[infos[0]] << infos[1]
46
+ else
47
+ app.entities[infos[0]] = [] unless app.entities[infos[0]]
48
+ @entities[infos[0]] = [] unless @entities[infos[0]]
49
+
50
+ app.entities[infos[0]] << infos[1]
51
+ @entities[infos[0]] << infos[1]
52
+ end
53
+ end
54
+ end # #process_requirements_for
55
+
56
+ def write_action action
57
+ jacks_data = process_jacks
58
+ requirements = require_entities
59
+
60
+ output = %Q{#{requirements}
61
+ class #{action['Action']}
62
+
63
+ def initialize #{jacks_data[:inputs]}
64
+ #{jacks_data[:assignments]} end
65
+
66
+ def execute input
67
+ #{@code} end
68
+ end
69
+ }
70
+
71
+ snake_name = action['Action'].gsub(/(.)([A-Z])/,'\1_\2').downcase
72
+
73
+ filename = "#{Obvious::Generators::Application.instance.dir}/actions/#{snake_name}.rb"
74
+ File.open(filename, 'w') {|f| f.write(output) }
75
+ end
76
+
77
+ def process_jacks
78
+ jack_inputs = ''
79
+ jack_assignments = ''
80
+
81
+ @jacks.each do |k, v|
82
+ name = k.chomp('Jack').downcase
83
+ jack_inputs << "#{name}_jack, "
84
+ jack_assignments << " @#{name}_jack = #{name}_jack\n"
85
+ end
86
+
87
+ jack_inputs.chomp! ', '
88
+
89
+ {
90
+ inputs: jack_inputs,
91
+ assignments: jack_assignments
92
+ }
93
+ end
94
+
95
+ def require_entities
96
+ entity_requires = ''
97
+
98
+ @entities.each do |k, v|
99
+ name = k.downcase
100
+ entity_requires << "require_relative '../entities/#{name}'\n"
101
+ end
102
+
103
+ entity_requires
104
+ end
105
+ end # ::Descriptor
106
+ end
107
+ end
@@ -0,0 +1,47 @@
1
+ require 'singleton'
2
+
3
+ module Obvious
4
+ module Generators
5
+ class Application
6
+ include Singleton
7
+
8
+ attr_reader :jacks, :entities, :dir
9
+
10
+ def initialize
11
+ @dir = 'app'
12
+
13
+ counter = 1
14
+ while File.directory? @dir
15
+ @dir = "app_#{counter}"
16
+ counter += 1
17
+ end
18
+ end
19
+
20
+ def jacks
21
+ @jacks ||= {}
22
+ end
23
+
24
+ def entities
25
+ @entities ||= {}
26
+ end
27
+
28
+ def target_path
29
+ File.realpath Dir.pwd
30
+ end
31
+
32
+ def lib_path
33
+ Gem::Specification.find_by_name("obvious").gem_dir + '/lib'
34
+ end
35
+
36
+ def remove_duplicates
37
+ entities.each do |k, v|
38
+ v.uniq!
39
+ end
40
+
41
+ jacks.each do |k,v|
42
+ v.uniq!
43
+ end
44
+ end
45
+ end # ::Application
46
+ end
47
+ end
data/lib/obvious.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  require 'obvious/version'
2
2
  require 'obvious/contract'
3
-
3
+ require_relative 'generators/application_generator'
@@ -20,7 +20,7 @@ class Contract
20
20
  #
21
21
  # Returns Nothing.
22
22
  def self.contracts *contracts
23
- singleton_class.send :define_method, :contract_list do
23
+ singleton_class.send :define_method, :contract_list do
24
24
  contracts
25
25
  end
26
26
  end
@@ -0,0 +1,94 @@
1
+ require 'json'
2
+
3
+ class FsPlug
4
+
5
+ def initialize filename
6
+ @filename = filename
7
+ end
8
+
9
+ def save input
10
+ # open the file
11
+ contents = File.read @filename
12
+
13
+ data = []
14
+ # parse the json list
15
+ query = JSON.parse contents, :symbolize_names => true
16
+
17
+ new_element = true if input[:id] == -1 # by convention set new element flag if id == -1
18
+
19
+ max_id = -1
20
+ # transform the data if needed
21
+ query.each do |h|
22
+ if input[:id] == h[:id]
23
+ h = input
24
+ end
25
+ max_id = h[:id] if h[:id] > max_id
26
+ data << h
27
+ end
28
+
29
+ # add data to the list if it's a new element
30
+ input[:id] = max_id + 1
31
+ data << input if new_element
32
+
33
+ # save the data back to FS
34
+ json_data = JSON.pretty_generate data
35
+ File.open(@filename, 'w') {|f| f.write(json_data) }
36
+
37
+ # return the transformed data
38
+ input
39
+ end
40
+
41
+ def list
42
+ # open the file
43
+ contents = File.read @filename
44
+
45
+ # parse the json list
46
+ data = JSON.parse contents, :symbolize_names => true
47
+
48
+ # return the transformed data
49
+ data
50
+ end
51
+
52
+ def get input
53
+ # open the file
54
+ contents = File.read @filename
55
+
56
+ data = []
57
+ # parse the json list
58
+ query = JSON.parse contents, :symbolize_names => true
59
+
60
+ # transform the data if needed
61
+ query.each do |h|
62
+ return h if h[:id] == input[:id]
63
+ end
64
+
65
+ {}
66
+ end
67
+
68
+ def remove input
69
+ # open the file
70
+ contents = File.read @filename
71
+
72
+ data = []
73
+ # parse the json list
74
+ query = JSON.parse contents, :symbolize_names => true
75
+
76
+ # transform the data if needed
77
+ query.each do |h|
78
+ unless h[:id] == input[:id]
79
+ data << h
80
+ end
81
+ end
82
+
83
+ # save the data back to FS
84
+ json_data = JSON.pretty_generate data
85
+ File.open(@filename, 'w') {|f| f.write(json_data) }
86
+
87
+ # return true on success
88
+ true
89
+ end
90
+
91
+ end
92
+
93
+
94
+
@@ -0,0 +1,57 @@
1
+ require 'moped'
2
+
3
+ class MongoPlug
4
+ def initialize collection
5
+ @collection = collection
6
+ @session = MONGO_SESSION
7
+ end
8
+
9
+ def list
10
+ result = @session[@collection].find.entries
11
+ result.map! do |entry|
12
+ clean_up entry
13
+ end
14
+
15
+ result
16
+ end
17
+
18
+ def get input
19
+ result = symbolize_keys @session[@collection].find(:id => input[:id]).first
20
+ clean_up result
21
+ end
22
+
23
+ def save input
24
+ if input[:id] == -1
25
+ id = @session[:counters].find(:_id => @collection).modify({ "$inc" => { seq: 1 } }, new:true)["seq"]
26
+ input[:id] = id
27
+ end
28
+ result = @session[@collection].find(:id => input[:id]).modify(input, upsert: true, new: true)
29
+ clean_up result
30
+ end
31
+
32
+ def remove input
33
+ @session[@collection].find(:id => input[:id]).remove
34
+ true
35
+ end
36
+
37
+ def clean_up input
38
+ result = symbolize_keys input
39
+ result.delete :_id
40
+ result
41
+ end
42
+
43
+ def symbolize_keys hash
44
+ hash.inject({}){|result, (key, value)|
45
+ new_key = case key
46
+ when String then key.to_sym
47
+ else key
48
+ end
49
+ new_value = case value
50
+ when Hash then symbolize_keys(value)
51
+ else value
52
+ end
53
+ result[new_key] = new_value
54
+ result
55
+ }
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ require 'sequel'
2
+
3
+ # assume that DB is defined as a global constant elsewhere
4
+
5
+ class MysqlPlug
6
+
7
+ def initialize table
8
+ @table = table
9
+ end
10
+
11
+ def save input
12
+ table = DB[@table]
13
+ input[:id] = nil if input[:id] == -1
14
+
15
+ # this does an upsert
16
+ result = table.on_duplicate_key_update.insert input
17
+
18
+ input[:id] = result
19
+ input
20
+ end
21
+
22
+ def list
23
+ table = DB[@table]
24
+ table.all
25
+ end
26
+
27
+ def get input
28
+ table = DB[@table]
29
+ table.first :id => input[:id]
30
+ end
31
+
32
+ def remove input
33
+ table = DB[@table]
34
+ result = table.where(:id => input[:id]).delete
35
+ return true if result == 1
36
+ false
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module Obvious
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.5"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: obvious
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-01-10 00:00:00.000000000 Z
12
+ date: 2013-01-14 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: A set of tools to build apps using the Obvious Architecture
15
15
  email:
@@ -25,9 +25,15 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - bin/obvious
28
+ - lib/generators/application_generator.rb
29
+ - lib/generators/descriptor.rb
30
+ - lib/generators/helpers/application.rb
28
31
  - lib/obvious.rb
29
32
  - lib/obvious/contract.rb
30
33
  - lib/obvious/files/Rakefile
34
+ - lib/obvious/files/external/fs_plug.rb
35
+ - lib/obvious/files/external/mongo_plug.rb
36
+ - lib/obvious/files/external/mysql_plug.rb
31
37
  - lib/obvious/version.rb
32
38
  - obvious.gemspec
33
39
  homepage: http://obvious.retromocha.com/
@@ -50,8 +56,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
56
  version: '0'
51
57
  requirements: []
52
58
  rubyforge_project:
53
- rubygems_version: 1.8.11
59
+ rubygems_version: 1.8.17
54
60
  signing_key:
55
61
  specification_version: 3
56
62
  summary: Isn't it Obvious?
57
63
  test_files: []
64
+ has_rdoc: