active_doc 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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)