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.
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ tmp/*
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ group :development do
4
+ gem 'bundler'
5
+ gem "rspec"
6
+ end
@@ -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.
@@ -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
+
@@ -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
@@ -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)