active_doc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/Gemfile +6 -0
- data/History.txt +38 -0
- data/LICENSE +22 -0
- data/README.rdoc +202 -0
- data/active_doc.gemspec +32 -0
- data/lib/active_doc.rb +83 -0
- data/lib/active_doc/described_method.rb +38 -0
- data/lib/active_doc/descriptions.rb +1 -0
- data/lib/active_doc/descriptions/method_argument_description.rb +426 -0
- data/lib/active_doc/rake/task.rb +48 -0
- data/lib/active_doc/rdoc_generator.rb +21 -0
- data/spec/active_doc/method_argument_description_spec.rb +341 -0
- data/spec/active_doc/rdoc_generator_spec.rb +85 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/support/documented_class.rb +53 -0
- metadata +116 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/History.txt
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
== 0.1.0
|
2
|
+
|
3
|
+
=== New features
|
4
|
+
* Duck-typing - defining methods for argument to respond_to?
|
5
|
+
|
6
|
+
=== TODO:
|
7
|
+
* Synopsis for this gem
|
8
|
+
* Duck-typing - defining methods for argument to respond_to?
|
9
|
+
|
10
|
+
== 0.1.0.beta.4 (2011-03-06)
|
11
|
+
|
12
|
+
=== New features
|
13
|
+
* Referencing to another definition
|
14
|
+
* Specifying argument expectation is not required
|
15
|
+
* Raw expectations in block for +takes+
|
16
|
+
* Expectation by array of options
|
17
|
+
|
18
|
+
== 0.1.0.beta.3 (2011-03-05)
|
19
|
+
More options for describing arguments
|
20
|
+
|
21
|
+
=== New features
|
22
|
+
* Describe argument with regexp
|
23
|
+
* Describe hash argument with nested expectations - suitable for options argument
|
24
|
+
|
25
|
+
== 0.1.0.beta.2 (2011-03-03)
|
26
|
+
Generating RDOC
|
27
|
+
|
28
|
+
=== New features
|
29
|
+
* Generate rdoc for method descriptions
|
30
|
+
* Rake task for generating RDOC
|
31
|
+
|
32
|
+
== 0.1.0.beta.1 (2011-03-01)
|
33
|
+
|
34
|
+
First gem pre-release!
|
35
|
+
|
36
|
+
=== New features
|
37
|
+
* arguments descriptions for instance and class methods
|
38
|
+
* basic architecture
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2011 Ivan Nečas
|
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
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,202 @@
|
|
1
|
+
= ActiveDoc
|
2
|
+
|
3
|
+
DSL for code description. It allows to create 'executable documentation'.
|
4
|
+
|
5
|
+
== Synopsis
|
6
|
+
|
7
|
+
class Mailer
|
8
|
+
include ActiveDoc
|
9
|
+
|
10
|
+
takes :to, /^[a-z.]+@[a-z.]+\.[a-z]+$/, :desc => "Receiver address"
|
11
|
+
def send(to)
|
12
|
+
#...
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Mailer.new.send("address@example.com") # => ok
|
17
|
+
Mailer.new.send("fake.address.org") # => raises ArgumentError
|
18
|
+
|
19
|
+
== Features
|
20
|
+
|
21
|
+
* Describe code with code
|
22
|
+
|
23
|
+
* Generate RDoc comments
|
24
|
+
|
25
|
+
* DRY your documentation
|
26
|
+
|
27
|
+
* Hash arguments description
|
28
|
+
|
29
|
+
* Really up-to-date
|
30
|
+
|
31
|
+
|
32
|
+
== Installation
|
33
|
+
|
34
|
+
gem install active_doc --pre
|
35
|
+
|
36
|
+
To use rake task, put something like this to your Rakefile
|
37
|
+
|
38
|
+
require 'rubygems'
|
39
|
+
require 'bundler'
|
40
|
+
Bundler.setup
|
41
|
+
|
42
|
+
require 'active_doc/rake/task'
|
43
|
+
|
44
|
+
ActiveDoc::Rake::Task.new do
|
45
|
+
# here you can put additional requirement files
|
46
|
+
require File.expand_path("lib/phone_book.rb", File.dirname(__FILE__))
|
47
|
+
end
|
48
|
+
|
49
|
+
This adds task +active_doc+ to generate RDoc comments from ActiveDoc.
|
50
|
+
|
51
|
+
== Usage
|
52
|
+
|
53
|
+
To use ActiveDoc DSL in your class:
|
54
|
+
include ActiveDoc
|
55
|
+
|
56
|
+
|
57
|
+
=== Method Arguments Description
|
58
|
+
|
59
|
+
Validations based on descriptions are checked every time the method is called.
|
60
|
+
|
61
|
+
When generating RDoc comments, the space between last argument description and method definition is used:
|
62
|
+
|
63
|
+
takes :name, String
|
64
|
+
# ==== Arguments:
|
65
|
+
# * +name+ :: (String)
|
66
|
+
def say_hallo_to(name)
|
67
|
+
...
|
68
|
+
end
|
69
|
+
|
70
|
+
<b>NOTICE: anything else in this space will be replaced.</b>
|
71
|
+
|
72
|
+
==== Describe by Type:
|
73
|
+
|
74
|
+
takes :name, String
|
75
|
+
def say_hallo_to(name)
|
76
|
+
...
|
77
|
+
end
|
78
|
+
|
79
|
+
* Validation: :: Raises ArgumentError, when +name+ is not of type +String+
|
80
|
+
|
81
|
+
* RDoc:
|
82
|
+
# ==== Arguments:
|
83
|
+
# * +name+ :: (String)
|
84
|
+
|
85
|
+
==== Describe by Duck Type:
|
86
|
+
|
87
|
+
takes :name, :duck => :upcase
|
88
|
+
def say_hallo_to(name)
|
89
|
+
...
|
90
|
+
end
|
91
|
+
|
92
|
+
* Validation: :: Raises ArgumentError, when +name+ does not respond to +:upcase+
|
93
|
+
|
94
|
+
* RDoc:
|
95
|
+
# ==== Arguments:
|
96
|
+
# * +name+ :: (respond to :upcase)
|
97
|
+
|
98
|
+
==== Describe by Regexp:
|
99
|
+
|
100
|
+
takes :phone_number, /^[0-9]{9}$/
|
101
|
+
def call_to(phone_number)
|
102
|
+
...
|
103
|
+
end
|
104
|
+
|
105
|
+
* Validation: :: Raises ArgumentError, when regexp does not match +phone_number+
|
106
|
+
|
107
|
+
* RDoc:
|
108
|
+
# ==== Arguments:
|
109
|
+
# * +phone_number+ :: (/^[0-9]{9}$/)
|
110
|
+
|
111
|
+
==== Describe by Enumeration:
|
112
|
+
|
113
|
+
takes :position, [:start, :middle, :end]
|
114
|
+
def jump_to(position)
|
115
|
+
...
|
116
|
+
end
|
117
|
+
|
118
|
+
* Validation: :: Raises ArgumentError, when positions is not included in [:start, :middle, :end]
|
119
|
+
|
120
|
+
* RDoc:
|
121
|
+
# ==== Arguments:
|
122
|
+
# * +position+ :: ([:start, :middle, :end])
|
123
|
+
|
124
|
+
==== Describe Options Hash:
|
125
|
+
|
126
|
+
takes :options, Hash do
|
127
|
+
takes :format, [:csv, :ods, :xls]
|
128
|
+
end
|
129
|
+
def export(options)
|
130
|
+
...
|
131
|
+
end
|
132
|
+
|
133
|
+
* Validation: :: Raises ArgumentError, when:
|
134
|
+
* +options[:format]+ is not included in [:csv, :ods, :xls]
|
135
|
+
* +options+ contains a key not mentioned in argument description
|
136
|
+
|
137
|
+
This differs from describing method arguments, where argument description is optional. Here it's required.
|
138
|
+
The reason is to prevent from (perhaps mistakenly) passing unexpected option.
|
139
|
+
|
140
|
+
* RDoc:
|
141
|
+
# ==== Arguments:
|
142
|
+
# * +options+:
|
143
|
+
# * +:format+ :: ([:csv, :ods, :xls])
|
144
|
+
|
145
|
+
==== Describe by Proc:
|
146
|
+
When passing proc taking an argument, this proc is used to validate value of this method argument.
|
147
|
+
|
148
|
+
takes :number {|args| args[:number] != 0}
|
149
|
+
def divide(number)
|
150
|
+
...
|
151
|
+
end
|
152
|
+
|
153
|
+
* Validation: :: Raises ArgumentError, unless proc.call(position)
|
154
|
+
|
155
|
+
* RDoc:
|
156
|
+
# ==== Arguments:
|
157
|
+
# * +number+ :: (Complex Condition)
|
158
|
+
|
159
|
+
=== Compatibility
|
160
|
+
|
161
|
+
<b>
|
162
|
+
This version was tested on and should be compatible with Ruby 1.9.2.
|
163
|
+
It uses some features introduced in Ruby 1.9.
|
164
|
+
Compatibility with 1.8.7 to be fixed in future releases.
|
165
|
+
</b>
|
166
|
+
|
167
|
+
=== Usage Notice
|
168
|
+
<b>
|
169
|
+
Bear in mind: This gem is in early-stages of development and was not sufficiently tested in external projects.
|
170
|
+
</b>
|
171
|
+
|
172
|
+
=== Road Map
|
173
|
+
|
174
|
+
* Support for 1.8.7
|
175
|
+
* Combine argument expectations with AND and OR conjunctions
|
176
|
+
* More types of descriptions (for modules, mixins...)
|
177
|
+
|
178
|
+
=== Contribution
|
179
|
+
|
180
|
+
Every bug report, bug fix, feature request or already implemented feature is welcome.
|
181
|
+
|
182
|
+
To report a bug or request a feature, create an issue in Github.
|
183
|
+
|
184
|
+
When contributing, please follow following instructions:
|
185
|
+
|
186
|
+
* Write spec for your contribution. It ensures everything works in future.
|
187
|
+
* Do not bother with Rakefile, version or history. It's OK to have your own, but if so, keep it in separated commit
|
188
|
+
to be easily ignored, when pulling.
|
189
|
+
* Bonus points for topic branch.
|
190
|
+
|
191
|
+
<b>
|
192
|
+
NOTICE: I am quite new to open source development, so I appreciate every input from more experienced contributor.
|
193
|
+
Contact me on e-mail.
|
194
|
+
</b>
|
195
|
+
|
196
|
+
|
197
|
+
|
198
|
+
|
199
|
+
== Copyright
|
200
|
+
|
201
|
+
Copyright (c) 2011 Ivan Nečas. See LICENSE for details.
|
202
|
+
|
data/active_doc.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
3
|
+
require "active_doc"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = 'active_doc'
|
7
|
+
s.version = ActiveDoc::VERSION
|
8
|
+
s.authors = ["Ivan Nečas"]
|
9
|
+
s.description = 'DSL for executable documentation of your code.'
|
10
|
+
s.summary = "active_doc-#{s.version}"
|
11
|
+
s.email = 'necasik@gmail.com'
|
12
|
+
s.homepage = "http://github.com/necasik/active_doc"
|
13
|
+
|
14
|
+
s.post_install_message = %{
|
15
|
+
(::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::)
|
16
|
+
|
17
|
+
Thank you for installing active_doc-#{ActiveDoc::VERSION}.
|
18
|
+
(::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::) (::)
|
19
|
+
|
20
|
+
}
|
21
|
+
|
22
|
+
s.add_development_dependency 'rake', '~> 0.8.7'
|
23
|
+
s.add_development_dependency 'rspec', '~> 2.3.0'
|
24
|
+
|
25
|
+
s.rubygems_version = "1.3.7"
|
26
|
+
s.files = `git ls-files`.split("\n")
|
27
|
+
s.test_files = `git ls-files -- {spec,features}/*`.split("\n")
|
28
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
29
|
+
s.extra_rdoc_files = ["LICENSE", "README.rdoc", "History.txt"]
|
30
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
31
|
+
s.require_path = "lib"
|
32
|
+
end
|
data/lib/active_doc.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'active_doc/described_method'
|
2
|
+
module ActiveDoc
|
3
|
+
VERSION = "0.1.0"
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.extend(Dsl)
|
7
|
+
base.class_eval do
|
8
|
+
class << self
|
9
|
+
extend(Dsl)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def register_description(description)
|
16
|
+
@current_descriptions ||= []
|
17
|
+
@current_descriptions << description
|
18
|
+
end
|
19
|
+
|
20
|
+
def pop_current_descriptions
|
21
|
+
current_descriptions = @current_descriptions
|
22
|
+
@current_descriptions = nil
|
23
|
+
return current_descriptions
|
24
|
+
end
|
25
|
+
|
26
|
+
def nested_descriptions
|
27
|
+
before_nesting_descriptions = self.pop_current_descriptions
|
28
|
+
yield
|
29
|
+
after_nesting_descriptions = self.pop_current_descriptions
|
30
|
+
@current_descriptions = before_nesting_descriptions
|
31
|
+
return after_nesting_descriptions
|
32
|
+
end
|
33
|
+
|
34
|
+
def describe(base, method_name, origin)
|
35
|
+
if current_descriptions = pop_current_descriptions
|
36
|
+
@descriptions ||= {}
|
37
|
+
@descriptions[[base, method_name]] = ActiveDoc::DescribedMethod.new(base, method_name, current_descriptions, origin)
|
38
|
+
before_method(base, method_name) do |method, args|
|
39
|
+
args_with_vals = {}
|
40
|
+
method.parameters.each_with_index { |(arg, name), i| args_with_vals[name] = {:val => args[i], :required => (arg != :opt), :defined => (i < args.size)} }
|
41
|
+
current_descriptions.each { |description| description.validate(args_with_vals) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def documented_method(base, method_name)
|
47
|
+
@descriptions && @descriptions[[base,method_name]]
|
48
|
+
end
|
49
|
+
|
50
|
+
def documented_methods
|
51
|
+
@descriptions.values
|
52
|
+
end
|
53
|
+
|
54
|
+
def before_method(base, method_name)
|
55
|
+
method = base.instance_method(method_name)
|
56
|
+
base.class_eval do
|
57
|
+
self.send(:define_method, "#{method_name}_with_validation") do |*args|
|
58
|
+
yield method, args
|
59
|
+
self.send("#{method_name}_without_validation", *args)
|
60
|
+
end
|
61
|
+
self.send(:alias_method, :"#{method_name}_without_validation", method_name)
|
62
|
+
self.send(:alias_method, method_name, :"#{method_name}_with_validation")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
module Dsl
|
69
|
+
end
|
70
|
+
|
71
|
+
module ClassMethods
|
72
|
+
def method_added(method_name)
|
73
|
+
ActiveDoc.describe(self, method_name, caller.first)
|
74
|
+
end
|
75
|
+
|
76
|
+
def singleton_method_added(method_name)
|
77
|
+
ActiveDoc.describe(self.singleton_class, method_name, caller.first)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
require 'active_doc/descriptions'
|
82
|
+
require 'active_doc/rdoc_generator'
|
83
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveDoc
|
2
|
+
class DescribedMethod
|
3
|
+
attr_reader :origin_file, :origin_line, :descriptions
|
4
|
+
def initialize(base, method_name, descriptions, origin)
|
5
|
+
@base, @method_name, @descriptions = base, method_name, descriptions
|
6
|
+
@origin_file, @origin_line = origin.split(":")
|
7
|
+
@origin_line = @origin_line.to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_rdoc
|
11
|
+
rdoc_lines = descriptions.map {|x| x.to_rdoc.lines.map{ |l| "# #{l.chomp}" }}.flatten
|
12
|
+
rdoc_lines.unshift("# ==== Attributes:")
|
13
|
+
return rdoc_lines.join("\n") << "\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_rdoc(offset)
|
17
|
+
File.open(@origin_file, "r+") do |f|
|
18
|
+
lines = f.readlines
|
19
|
+
rdoc_lines = to_rdoc.lines.to_a
|
20
|
+
rdoc_space_range = rdoc_space_range(offset)
|
21
|
+
lines[rdoc_space_range] = rdoc_lines
|
22
|
+
offset += rdoc_lines.size - rdoc_space_range.to_a.size
|
23
|
+
f.pos = 0
|
24
|
+
lines.each do |line|
|
25
|
+
f.print line
|
26
|
+
end
|
27
|
+
f.truncate(f.pos)
|
28
|
+
end
|
29
|
+
return offset
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
def rdoc_space_range(offset)
|
34
|
+
(descriptions.last.last_line + offset)...(@origin_line + offset-1)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'active_doc/descriptions/method_argument_description'
|
@@ -0,0 +1,426 @@
|
|
1
|
+
module ActiveDoc
|
2
|
+
module Descriptions
|
3
|
+
class MethodArgumentDescription
|
4
|
+
module Dsl
|
5
|
+
# Describes method argument.
|
6
|
+
#
|
7
|
+
# ==== Attributes:
|
8
|
+
# * +name+ :: name of method argument.
|
9
|
+
# * +argument_expectation+ :: expected +Class+, +Regexp+ (or another values see +ArgumentExpectation+ subclasses
|
10
|
+
# for more details).
|
11
|
+
# * +options+:
|
12
|
+
# * +:ref+ :: (/^\\S+#\\S+$/) :: Reference to another method with description of this argument. Suitable when
|
13
|
+
# passing argument to another method.
|
14
|
+
# * +:desc+ :: Textual additional description and explanation of argument
|
15
|
+
#
|
16
|
+
# === Validation
|
17
|
+
#
|
18
|
+
# When method is described, checking routines are attached to original method. When some argument
|
19
|
+
# does not meet expectations, ArgumentError is raised.
|
20
|
+
#
|
21
|
+
# Argument description is not compulsory, e.g. when you not specify +takes+ for some argument, nothing
|
22
|
+
# happens.
|
23
|
+
#
|
24
|
+
# ==== Example:
|
25
|
+
#
|
26
|
+
# takes :contact_name, String, :desc => "Last name of contact person"
|
27
|
+
# takes :number, /[0-9]{6}/
|
28
|
+
# def add(contact_name, number)
|
29
|
+
# ...
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# This adds to +add+ methods routines checking, that value of +contact_name+ ia_a? String and
|
33
|
+
# value of +add+ =~ /[0-9]{6}/.
|
34
|
+
#
|
35
|
+
# === Nesting:
|
36
|
+
# When describing Hash, it can take a block, that allows additional description of argument
|
37
|
+
# using the same DSL.
|
38
|
+
#
|
39
|
+
# ==== Example:
|
40
|
+
#
|
41
|
+
# takes :options, Hash do
|
42
|
+
# takes :category, String
|
43
|
+
# end
|
44
|
+
# def add(number, options)
|
45
|
+
# ...
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# ==== Hash validation:
|
49
|
+
# In current implementation, when describing hash argument with nested description, every expected
|
50
|
+
# key must be mentioned. Reason: preventing from mistakenly sending unexpected options.
|
51
|
+
#
|
52
|
+
# === RDoc generating
|
53
|
+
#
|
54
|
+
# When generating +RDoc+ comments from +active_doc+, space between last +takes+ description an method definition
|
55
|
+
# is used. *Bear in mind: Everything in this space will be replaced with generated comments*
|
56
|
+
# ==== Example:
|
57
|
+
#
|
58
|
+
# takes :contact_name, String, :desc => "Last name of contact person"
|
59
|
+
# takes :number, /[0-9]{6}/
|
60
|
+
# takes :options, Hash do
|
61
|
+
# takes :category, String
|
62
|
+
# end
|
63
|
+
# # this comment was there before
|
64
|
+
# def add(contact_name, number, options)
|
65
|
+
# ...
|
66
|
+
# end
|
67
|
+
#
|
68
|
+
# After running rake task for RDoc comments, it's changed to:
|
69
|
+
#
|
70
|
+
# takes :contact_name, String, :desc => "Last name of contact person"
|
71
|
+
# takes :number, /[0-9]{6}/
|
72
|
+
# takes :options, Hash do
|
73
|
+
# takes :category, String
|
74
|
+
# end
|
75
|
+
# # ==== Arguments:
|
76
|
+
# # *+contact_name+ :: (String) :: Last name of contact person
|
77
|
+
# # *+number+ :: (/[0-9]{6}/)
|
78
|
+
# # *+options+ :
|
79
|
+
# # * +:category+ :: (String)
|
80
|
+
# def add(contact_name, number, options)
|
81
|
+
# ...
|
82
|
+
# end
|
83
|
+
def takes(name, *args, &block)
|
84
|
+
if args.size > 1 || !args.first.is_a?(Hash)
|
85
|
+
argument_expectation = args.shift || nil
|
86
|
+
else
|
87
|
+
argument_expectation = nil
|
88
|
+
end
|
89
|
+
options = args.pop || {}
|
90
|
+
if ref_string = options[:ref]
|
91
|
+
ActiveDoc.register_description(ActiveDoc::Descriptions::MethodArgumentDescription::Reference.new(name, ref_string, caller.first, options, &block))
|
92
|
+
else
|
93
|
+
ActiveDoc.register_description(ActiveDoc::Descriptions::MethodArgumentDescription.new(name, argument_expectation, caller.first, options, &block))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class ArgumentExpectation
|
99
|
+
def self.inherited(subclass)
|
100
|
+
@possible_argument_expectations ||= []
|
101
|
+
@possible_argument_expectations << subclass
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.find(argument, options, proc)
|
105
|
+
@possible_argument_expectations.each do |expectation|
|
106
|
+
if suitable_expectation = expectation.from(argument, options, proc)
|
107
|
+
return suitable_expectation
|
108
|
+
end
|
109
|
+
end
|
110
|
+
return nil
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
def fulfilled?(value, args_with_vals)
|
115
|
+
if self.condition?(value, args_with_vals)
|
116
|
+
@failed_value = nil
|
117
|
+
return true
|
118
|
+
else
|
119
|
+
@failed_value = value
|
120
|
+
return false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# to be inserted after argument description in rdoc
|
125
|
+
def additional_rdoc
|
126
|
+
return nil
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class TypeArgumentExpectation < ArgumentExpectation
|
131
|
+
def initialize(argument)
|
132
|
+
@type = argument
|
133
|
+
end
|
134
|
+
|
135
|
+
def condition?(value, args_with_vals)
|
136
|
+
value.is_a? @type
|
137
|
+
end
|
138
|
+
|
139
|
+
# Expected to...
|
140
|
+
def expectation_fail_to_s
|
141
|
+
"be #{@type.name}, got #{@failed_value.class.name}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def to_rdoc
|
145
|
+
@type.name
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.from(argument, options, proc)
|
149
|
+
if argument.is_a?(Class) && proc.nil?
|
150
|
+
self.new(argument)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
class RegexpArgumentExpectation < ArgumentExpectation
|
156
|
+
def initialize(argument)
|
157
|
+
@regexp = argument
|
158
|
+
end
|
159
|
+
|
160
|
+
def condition?(value, args_with_vals)
|
161
|
+
value =~ @regexp
|
162
|
+
end
|
163
|
+
|
164
|
+
# Expected to...
|
165
|
+
# NOTE: Possible thread-safe problem
|
166
|
+
def expectation_fail_to_s
|
167
|
+
"match #{@regexp}, got '#{@failed_value.inspect}'"
|
168
|
+
end
|
169
|
+
|
170
|
+
def to_rdoc
|
171
|
+
@regexp.inspect.gsub('\\') { '\\\\' }
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.from(argument, options, proc)
|
175
|
+
if argument.is_a? Regexp
|
176
|
+
self.new(argument)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
class ArrayArgumentExpectation < ArgumentExpectation
|
182
|
+
def initialize(argument)
|
183
|
+
@array = argument
|
184
|
+
end
|
185
|
+
|
186
|
+
def condition?(value, args_with_vals)
|
187
|
+
@array.include?(value)
|
188
|
+
end
|
189
|
+
|
190
|
+
# Expected to...
|
191
|
+
# NOTE: Possible thread-safe problem
|
192
|
+
def expectation_fail_to_s
|
193
|
+
"be included in #{@array.inspect}, got #{@failed_value.inspect}"
|
194
|
+
end
|
195
|
+
|
196
|
+
def to_rdoc
|
197
|
+
@array.inspect
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.from(argument, options, proc)
|
201
|
+
if argument.is_a? Array
|
202
|
+
self.new(argument)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
class ComplexConditionArgumentExpectation < ArgumentExpectation
|
208
|
+
def initialize(argument)
|
209
|
+
@proc = argument
|
210
|
+
end
|
211
|
+
|
212
|
+
def condition?(value, args_with_vals)
|
213
|
+
other_values = args_with_vals.inject({}) { |h, (k, v)| h[k] = v[:val];h }
|
214
|
+
@proc.call(other_values)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Expected to...
|
218
|
+
def expectation_fail_to_s
|
219
|
+
"satisfy given condition, got #{@failed_value.inspect}"
|
220
|
+
end
|
221
|
+
|
222
|
+
def to_rdoc
|
223
|
+
"Complex Condition"
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.from(argument, options, proc)
|
227
|
+
if proc.is_a?(Proc) && proc.arity == 1
|
228
|
+
self.new(proc)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class OptionsHashArgumentExpectation < ArgumentExpectation
|
234
|
+
def initialize(argument)
|
235
|
+
@proc = argument
|
236
|
+
|
237
|
+
@hash_descriptions = ActiveDoc.nested_descriptions do
|
238
|
+
Class.new.extend(Dsl).class_exec(&@proc)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def condition?(value, args_with_vals)
|
243
|
+
if @hash_descriptions
|
244
|
+
raise "Only hash is supported for nested argument documentation" unless value.is_a? Hash
|
245
|
+
hash_args_with_vals = value.inject(Hash.new{|h,k| h[k] = {:defined => false}}) do |hash, (key,val)|
|
246
|
+
hash[key] = {:val => val, :defined => true}
|
247
|
+
hash
|
248
|
+
end
|
249
|
+
described_keys = @hash_descriptions.map do |hash_description|
|
250
|
+
hash_description.validate(hash_args_with_vals)
|
251
|
+
end
|
252
|
+
undescribed_keys = value.keys - described_keys
|
253
|
+
unless undescribed_keys.empty?
|
254
|
+
raise ArgumentError.new("Inconsistent options definition with active doc. Hash was not expected to have arguments '#{undescribed_keys.join(", ")}'")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
return true
|
258
|
+
end
|
259
|
+
|
260
|
+
# Expected to...
|
261
|
+
def expectation_fail_to_s
|
262
|
+
"contain described keys, got #{@failed_value.inspect}"
|
263
|
+
end
|
264
|
+
|
265
|
+
def to_rdoc
|
266
|
+
return "Hash"
|
267
|
+
end
|
268
|
+
|
269
|
+
def additional_rdoc
|
270
|
+
if @hash_descriptions
|
271
|
+
ret = @hash_descriptions.map { |x| " #{x.to_rdoc(true)}" }.join("\n")
|
272
|
+
ret.insert(0, ":\n")
|
273
|
+
ret
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def last_line
|
278
|
+
@hash_descriptions && @hash_descriptions.last && (@hash_descriptions.last.last_line + 1)
|
279
|
+
end
|
280
|
+
|
281
|
+
def self.from(argument, options, proc)
|
282
|
+
if proc.is_a?(Proc) && proc.arity == 0 && argument == Hash
|
283
|
+
self.new(proc)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
class DuckArgumentExpectation < ArgumentExpectation
|
289
|
+
def initialize(argument)
|
290
|
+
@respond_to = argument
|
291
|
+
@respond_to = [@respond_to] unless @respond_to.is_a? Array
|
292
|
+
end
|
293
|
+
|
294
|
+
def condition?(value, args_with_vals)
|
295
|
+
@failed_quacks = @respond_to.find_all {|quack| not value.respond_to? quack}
|
296
|
+
@failed_quacks.empty?
|
297
|
+
end
|
298
|
+
|
299
|
+
# Expected to...
|
300
|
+
# NOTE: Possible thread-safe problem
|
301
|
+
def expectation_fail_to_s
|
302
|
+
"be respond to #{@respond_to.inspect}, missing #{@failed_quacks.inspect}"
|
303
|
+
end
|
304
|
+
|
305
|
+
def to_rdoc
|
306
|
+
respond_to = @respond_to
|
307
|
+
respond_to = respond_to.first if respond_to.size == 1
|
308
|
+
"respond to #{respond_to.inspect}"
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.from(argument, options, proc)
|
312
|
+
if options[:duck]
|
313
|
+
self.new(options[:duck])
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
module Traceable
|
320
|
+
def origin_file
|
321
|
+
@origin.split(":").first
|
322
|
+
end
|
323
|
+
|
324
|
+
def origin_line
|
325
|
+
@origin.split(":")[1].to_i
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
attr_reader :name, :origin_file
|
330
|
+
attr_accessor :conjunction
|
331
|
+
include Traceable
|
332
|
+
|
333
|
+
def initialize(name, argument_expectation, origin, options = {}, &block)
|
334
|
+
@name, @origin, @description = name, origin, options[:desc]
|
335
|
+
@argument_expectations = []
|
336
|
+
if found_expectation = ArgumentExpectation.find(argument_expectation, options, block)
|
337
|
+
@argument_expectations << found_expectation
|
338
|
+
elsif block
|
339
|
+
raise "We haven't fount suitable argument expectations for given parameters"
|
340
|
+
end
|
341
|
+
|
342
|
+
if @argument_expectations.last.respond_to?(:last_line)
|
343
|
+
@last_line = @argument_expectations.last.last_line
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
def validate(args_with_vals)
|
348
|
+
argument_name = @name
|
349
|
+
if arg_attributes = args_with_vals[@name]
|
350
|
+
if arg_attributes[:required] || arg_attributes[:defined]
|
351
|
+
current_value = arg_attributes[:val]
|
352
|
+
failed_expectations = @argument_expectations.find_all { |expectation| not expectation.fulfilled?(current_value, args_with_vals) }
|
353
|
+
if !failed_expectations.empty?
|
354
|
+
raise ArgumentError.new("Wrong value for argument '#{argument_name}'. Expected to #{failed_expectations.map { |expectation| expectation.expectation_fail_to_s }.join(",")}")
|
355
|
+
end
|
356
|
+
end
|
357
|
+
else
|
358
|
+
raise ArgumentError.new("Inconsistent method definition with active doc. Method was expected to have argument '#{argument_name}'")
|
359
|
+
end
|
360
|
+
return argument_name
|
361
|
+
end
|
362
|
+
|
363
|
+
def to_rdoc(hash = false)
|
364
|
+
name = hash ? @name.inspect : @name
|
365
|
+
ret = "* +#{name}+"
|
366
|
+
ret << expectations_to_rdoc.to_s
|
367
|
+
ret << desc_to_rdoc.to_s
|
368
|
+
ret << expectations_to_additional_rdoc.to_s
|
369
|
+
return ret
|
370
|
+
end
|
371
|
+
|
372
|
+
def last_line
|
373
|
+
return @last_line || self.origin_line
|
374
|
+
end
|
375
|
+
|
376
|
+
private
|
377
|
+
|
378
|
+
def expectations_to_rdoc
|
379
|
+
expectations_rdocs = @argument_expectations.map { |x| x.to_rdoc }.compact
|
380
|
+
" :: (#{expectations_rdocs.join(", ")})" unless expectations_rdocs.empty?
|
381
|
+
end
|
382
|
+
|
383
|
+
def expectations_to_additional_rdoc
|
384
|
+
@argument_expectations.map { |argument_expectation| argument_expectation.additional_rdoc }.compact.join
|
385
|
+
end
|
386
|
+
|
387
|
+
def desc_to_rdoc
|
388
|
+
" :: #{@description}" if @description
|
389
|
+
end
|
390
|
+
|
391
|
+
class Reference < MethodArgumentDescription
|
392
|
+
include Traceable
|
393
|
+
def initialize(name, target_description, origin, options)
|
394
|
+
@name = name
|
395
|
+
@klass, @method = target_description.split("#")
|
396
|
+
@klass = Object.const_get(@klass)
|
397
|
+
@method = @method.to_sym
|
398
|
+
@origin = origin
|
399
|
+
end
|
400
|
+
|
401
|
+
def validate(*args)
|
402
|
+
# we validate only in target method
|
403
|
+
return @name
|
404
|
+
end
|
405
|
+
|
406
|
+
def to_rdoc(*args)
|
407
|
+
referenced_argument_description.to_rdoc
|
408
|
+
end
|
409
|
+
|
410
|
+
private
|
411
|
+
|
412
|
+
def referenced_argument_description
|
413
|
+
if referenced_described_method = ActiveDoc.documented_method(@klass, @method)
|
414
|
+
if referenced_argument_description = referenced_described_method.descriptions.find { |description| description.name == @name }
|
415
|
+
return referenced_argument_description
|
416
|
+
end
|
417
|
+
end
|
418
|
+
raise "Missing referenced argument description '#{@klass.name}##{@method}'"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
ActiveDoc::Dsl.send(:include, ActiveDoc::Descriptions::MethodArgumentDescription::Dsl)
|