active_doc 0.1.0
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/.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)
|