rspec-apigen 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc ADDED
@@ -0,0 +1,186 @@
1
+ == What is this ?
2
+
3
+ A tool that generates an API documentation using RSpec and some DSL magic.
4
+ If successful I might use it for my own project (neo4j.rb)
5
+ <b>This is an early experiment !</b>
6
+
7
+ === How ?
8
+
9
+ Instead of writing specs like this:
10
+
11
+ describe Account do
12
+ context "transfering money" do
13
+ it "deposits transfer amount to the other account" do
14
+ source = Account.new(50, :USD)
15
+ target = mock('target account')
16
+ target.should_receive(:deposit).with(Money.new(5, :USD))
17
+ source.transfer(5, :USD).to(target)
18
+ end
19
+
20
+ it "reduces its balance by the transfer amount" do
21
+ source = Account.new(50, :USD)
22
+ target = stub('target account')
23
+ source.transfer(5, :USD).to(target)
24
+ source.balance.should == Money.new(45, :USD)
25
+ end
26
+ end
27
+ end
28
+
29
+ Which generates the following output
30
+
31
+ $ spec ./spec/account_spec.rb --format nested
32
+ Account
33
+ transfering money
34
+ deposits transfer amount to the other account
35
+ reduces its balance by the transfer amount
36
+
37
+ 2 examples, 0 failures
38
+
39
+ I (also) want to generate a detailed API documentation something like this from a RSpec Macro DSL:
40
+ Account
41
+ Public Static Methods
42
+ #new ()
43
+ Given
44
+ no arguments
45
+ Then
46
+ Return Account with 0 USD
47
+ has #balance == 0
48
+ has #currency == 'USD
49
+ #new (amount,currency)
50
+ Scenario account and currency has valid values
51
+ Given
52
+ arguments 50, USD
53
+ Then
54
+ Return an Account with given amount and currency
55
+ should == 50
56
+ should == "USD"
57
+ Public Instance Methods
58
+ #transfer (amount,currency)
59
+ Scenario transfer amount and currency have valid values
60
+ Given
61
+ arguments 5, USD
62
+ Then
63
+ should not change subject
64
+ Return A transfer of 5 USD from Account with 50 USD
65
+ should be a kind of TransferDSL
66
+ Finished in 0.00412 seconds
67
+ 9 examples, 0 failures
68
+
69
+ TransferDSL
70
+ Public Static Methods
71
+ #new (source_account,amount,currency)
72
+ Given
73
+ arguments Account balance: 50 USD, 5, USD
74
+ Then
75
+ Return An TransferDSL instance with initialized source account, amount and currency
76
+ should == 50
77
+ Public Instance Methods
78
+ #to (target_account)
79
+ Given
80
+ arguments Account balance: 10 USD
81
+ Then it should add money on the target account
82
+ should add money to the target account
83
+
84
+ Finished in 0.00225 seconds
85
+ 4 examples, 0 failures
86
+
87
+ The above is generated from the following RSpec file:
88
+
89
+ describe Account do
90
+
91
+ static_methods do
92
+ new do
93
+ Return("Account with 0 USD") do
94
+ # it_behaves_like "Account with 0 USD" can also be used
95
+ it("has #balance == 0") { subject.balance.should == 0 }
96
+ it("has #currency == 'USD") { subject.currency.should == 'USD' }
97
+ end
98
+ end
99
+
100
+ new(arg(:amount), arg(:currency)) do
101
+ Scenario 'account and currency has valid values' do
102
+ Given do
103
+ arg.amount = 50
104
+ arg.currency = 'USD'
105
+ end
106
+
107
+ Return "an Account with given amount and currency" do
108
+ it { subject.balance.should == 50 } #given.amount }
109
+ it { subject.currency.should == 'USD' } # given.currency }
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ instance_methods do
116
+ transfer(arg(:amount), arg(:currency)) do
117
+ # Description 'bla bla bla'
118
+ Scenario 'transfer amount and currency have valid values' do
119
+ subject { Account.new(50, 'USD') }
120
+ Given do
121
+ arg.amount = 5
122
+ arg.currency = 'USD'
123
+ end
124
+ Return "A transfer of 5 USD from Account with 50 USD" do
125
+ it { should be_kind_of(TransferDSL) }
126
+ end
127
+ Then do
128
+ it "should not change subject" do
129
+ given.subject.balance.should == subject.balance
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+
138
+ describe TransferDSL do
139
+ static_methods do
140
+ new(arg(:source_account) do
141
+ Given do
142
+ arg.source_account = Account.new(50, 'USD')
143
+ end
144
+ Returns("A Transfer DSL with the given source account") do
145
+ it {subject.source_account.should == given.source_account}
146
+ end
147
+
148
+ end
149
+
150
+ instance_methods do
151
+ to(arg(:target_account, arg(:amount), arg(:currency)) do
152
+ Scenario 'source account has enough money' do
153
+ Given do
154
+ arg.target_account = Account.new(50, 'USD')
155
+ arg.amount = 5
156
+ arg.currency = 'USD'
157
+ subject{TransferDSL.new(Account.new(50, 'USD')}
158
+ end
159
+
160
+ # the following line describes the subject's source_account method
161
+ Then "it added money to the target account" do
162
+ it { subject.target_account.amount.should == given.arg.source_account.amount + given.arg.amount }
163
+ end
164
+ end
165
+
166
+ Scenario 'source account does NOT have enough money' do
167
+ Given do
168
+ arg.target_account = Account.new(50, 'USD')
169
+ arg.amount = 100
170
+ arg.currency = 'USD'
171
+ subject{TransferDSL.new(Account.new(10, 'USD')}
172
+ end
173
+ Throws(Error)
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+
180
+ TODO: I will implement a new RSpec HTML formatter which will generate something similar to RDoc. I want
181
+ to write specification in my rspec code instead of using RDoc.
182
+
183
+
184
+ === Example
185
+ gem install rspec --prerelease (2.0.0.beta.19)
186
+ rspec -f d -c spec
@@ -0,0 +1,114 @@
1
+ module RSpec::ApiGen
2
+
3
+ def run_scenario(method, args, block)
4
+ # have we defined any scenarios ?
5
+ MetaHelper.create_singleton_method(self, :Scenario) do |*scenario_desc, &scenario_block|
6
+ context "Scenario #{scenario_desc[0]}" do
7
+ run_scenario(method, args, scenario_block)
8
+ end
9
+ end
10
+
11
+ # create method to set the describe_return variable
12
+ describe_return = nil
13
+ MetaHelper.create_singleton_method(self, :Return) do |*example_desc, &example|
14
+ describe_return = {:example => example, :example_desc => example_desc}
15
+ end
16
+
17
+ # create method to set the given_block variable
18
+ given_block = nil
19
+ given_caller = nil # so that we can print correct line number if there is an exception
20
+ MetaHelper.create_singleton_method(self, :Given) { |*a, &b| given_block = b; given_caller = b.send(:caller)}
21
+
22
+ MetaHelper.create_singleton_method(self, :Description) { |desc| context(desc){} }
23
+
24
+
25
+ # create method to set the given_block variable
26
+ then_block = nil
27
+ then_desc = nil
28
+ MetaHelper.create_singleton_method(self, :Then) { |*desc, &b| then_block = b; then_desc = desc[0] if desc.size > 0}
29
+
30
+ # eval and set the given_block and describe_return variables
31
+ self.instance_eval(&block)
32
+
33
+ # if there are no then_block or describe_return then there is nothing to do
34
+ return if describe_return.nil? && then_block.nil? && then_desc.nil?
35
+
36
+ given = nil
37
+ context "Given" do
38
+ given = Given.new(self, method, args, given_caller, &given_block)
39
+ end
40
+
41
+ # todo should be possible to have several Then
42
+ context "Then #{then_desc}" do
43
+ self.send(:define_method, :given) { given }
44
+ self.send(:define_method, :arg) { given.arg }
45
+ self.send(:define_method, :fixtures) { given.fixtures }
46
+
47
+
48
+ # use the same subject as we used when calling the method on it in the given block
49
+ # self.send(:define_method, :subject) { given.subject }
50
+
51
+ context "Return #{describe_return[:example_desc][0]}" do
52
+ subject { given.return }
53
+
54
+ self.instance_eval(&describe_return[:example])
55
+ end if describe_return
56
+
57
+ self.instance_eval(&then_block) if then_block
58
+ end
59
+ end
60
+
61
+ def create_scenarios_for(method, param, &block)
62
+ args = param[:args]
63
+ context "##{method}", Argument.describe(args) do
64
+ run_scenario(method, args, block)
65
+ end
66
+ end
67
+
68
+
69
+ def static_methods(&block)
70
+ clazz = describes
71
+ describe "Public Static Methods" do
72
+ static_context = Method.new
73
+
74
+ # TODO - how do I find which methods was defined on the clazz and not inherited ?
75
+ def_methods = clazz.public_methods - Object.methods + %w[new]
76
+ current_context = self
77
+
78
+ # add methods on the context - one for each public static method
79
+ def_methods.each do |meth_name|
80
+ MetaHelper.create_singleton_method(static_context, meth_name) do |*args, &example_group|
81
+ current_context.subject { clazz }
82
+ current_context.create_scenarios_for(meth_name, :args => args, &example_group)
83
+ end
84
+ end
85
+ static_context.instance_eval(&block)
86
+ end
87
+ end
88
+
89
+ def instance_methods(&block)
90
+ clazz = describes
91
+ describe "Public Instance Methods" do
92
+ # Check if we are testing a module - in that case construct a new class that includes that Module
93
+ # so that we can test this Module
94
+ subject do
95
+ x = Class.new
96
+ x.send(:include, clazz)
97
+ x.new
98
+ end if clazz.class == Module
99
+
100
+ meth_ctx = Method.new
101
+
102
+ # TODO - how do I find which methods was defined on the clazz and not inherited ?
103
+ def_methods = clazz.public_instance_methods - Object.public_instance_methods
104
+ current_context = self
105
+ def_methods.each do |meth_name|
106
+ MetaHelper.create_singleton_method(meth_ctx, meth_name) do |*args, &example_group|
107
+ current_context.create_scenarios_for(meth_name, :args => args, &example_group)
108
+ end
109
+ end
110
+ meth_ctx.instance_eval(&block)
111
+ end
112
+ end
113
+
114
+ end
@@ -0,0 +1,38 @@
1
+ module RSpec::ApiGen
2
+ class Argument
3
+ attr_reader :name, :description
4
+
5
+ def initialize(name, description, accept_block = false)
6
+ @name = name
7
+ @description = description
8
+ @accept_block = accept_block
9
+ end
10
+
11
+ def accept_block?
12
+ @accept_block
13
+ end
14
+
15
+ def to_s
16
+ "Arg #{name}"
17
+ end
18
+
19
+ def self.inspect_args(args)
20
+ args.collect do |x|
21
+ case x
22
+ when Argument
23
+ x.name
24
+ when RSpec::Mocks::Mock
25
+ x.instance_variable_get('@name')
26
+ else
27
+ "#{x}:#{x.class}"
28
+ end
29
+ end
30
+ end
31
+
32
+ def self.describe(args)
33
+ "(#{inspect_args(args).join(', ')})"
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,5 @@
1
+ # Make the RSpec::ApiGen module available in all 'spec/api' test folders automatically'
2
+
3
+ RSpec.configure do |c|
4
+ c.extend RSpec::ApiGen, :example_group => { :file_path => %r{\b/spec/api} }
5
+ end
@@ -0,0 +1,25 @@
1
+ module RSpec::ApiGen
2
+ class Fixture
3
+ attr_reader :description
4
+ attr_reader :create_proc
5
+ attr_reader :destroy_proc
6
+
7
+ #def initialize(description, create_proc, destroy_proc = nil)
8
+ def initialize(description, &block)
9
+ @description = description && ""
10
+
11
+ # initialize this instance
12
+ self.instance_eval &block
13
+ end
14
+
15
+ def create(&block)
16
+ block.nil? ? @create_proc.call : @create_proc = block
17
+ end
18
+
19
+ def destroy(&block)
20
+ block.nil? ? @destroy_proc.call : @destroy_proc = block
21
+ end
22
+
23
+ end
24
+
25
+ end
@@ -0,0 +1,56 @@
1
+ module RSpec::ApiGen
2
+ class Formatter
3
+ def method_missing(m, *a, &b)
4
+ puts "CALLED #{m}"
5
+ end
6
+
7
+ def initialize(output )
8
+ end
9
+
10
+ def start(example_count)
11
+ puts "start #{example_count}"
12
+ @current_method = nil
13
+ end
14
+
15
+ def example_group_started(example_group)
16
+ # is it a new class ?
17
+ if @current_class != example_group.describes
18
+ @current_class = example_group.describes
19
+ @current_method = nil
20
+ puts "#{@current_class.class} #{example_group.describes}"
21
+ elsif example_group.description == "Public Static Methods"
22
+ puts " Static Static Methods"
23
+ elsif example_group.description == "Public Instance Methods"
24
+ puts " Static Instance Methods"
25
+ elsif @current_method
26
+ puts " #{example_group.description}"
27
+ else
28
+ puts " Method #{example_group.description}"
29
+ @current_method = example_group.description
30
+ end
31
+ # puts "example_groupstarted #{example_group.describes.class} #{example_group.describes.object_id} #{example_group.description}"
32
+ end
33
+
34
+ def start_dump
35
+ puts "start dump"
36
+ end
37
+
38
+ def example_started(example)
39
+ end
40
+
41
+ def example_passed(example)
42
+ puts " #{example.metadata[:description]}"
43
+ #puts " #{example.pretty_inspect}"
44
+ end
45
+
46
+ def example_failed(example)
47
+ puts "failed"
48
+ end
49
+
50
+ def example_pending(example)
51
+ puts "pending"
52
+ end
53
+
54
+
55
+ end
56
+ end
@@ -0,0 +1,91 @@
1
+ module RSpec::ApiGen
2
+ class Given
3
+ attr_reader :args # contains name of argument and its value
4
+ attr_reader :arg # the DSL object used to set the args
5
+ attr_reader :fixtures
6
+ attr_accessor :subject
7
+ attr_accessor :return
8
+
9
+
10
+ # list_of_args - the list of arguments current method accept
11
+ # When the given block is evaluated in this method
12
+ # the Given#args will return a hash of name or argument and its value.
13
+ def initialize(context, method, list_of_args, given_caller, &block)
14
+ this = self # so we can access it as closure
15
+ @arg = Object.new
16
+ @args = {}
17
+ @fixtures = {}
18
+ block_arg = nil
19
+
20
+ list_of_args.find_all { |a| a.kind_of?(Argument) }.each do |a|
21
+ MetaHelper.create_singleton_method(@arg, "#{a.name}=") do |val|
22
+ this.args[a.name] = val
23
+ end
24
+ if (a.accept_block?)
25
+ MetaHelper.create_singleton_method(@arg, "#{a.name}") do | &bl |
26
+ block_arg = bl
27
+ end
28
+ else
29
+ MetaHelper.create_singleton_method(@arg, "#{a.name}") do
30
+ this.args[a.name]
31
+ end
32
+ end
33
+ end
34
+
35
+ context.it "no arguments" do
36
+ # create the arguments
37
+ MetaHelper.create_singleton_method(self, :arg) { this.arg }
38
+
39
+ # accessor for setting fixture
40
+ MetaHelper.create_singleton_method(self, :fixtures) { this.fixtures }
41
+
42
+ begin
43
+ self.instance_eval &block
44
+ rescue Exception => e
45
+ this.set_backtrace(example, e, given_caller)
46
+ end if block
47
+
48
+ list_of_args.delete_if { |a| a.kind_of?(Argument) && a.accept_block? }
49
+
50
+ # now, the args hash should have been populated
51
+ # for each param we replace the args with the real value
52
+ list_of_args.collect! { |a| a.kind_of?(Argument) ? this.args[a.name] : a }
53
+
54
+ example.metadata[:description] = "arguments #{Argument.describe(list_of_args)}" unless list_of_args.empty?
55
+
56
+ # get the subject which we will use to test the method on, and store it so we can check it
57
+ this.subject = subject
58
+
59
+ begin
60
+ example.execution_result[:exception_encountered] = given_caller
61
+
62
+ if (block_arg)
63
+ # call the method on this instance which will will test
64
+ this.return = this.subject.send(method, *list_of_args, &block_arg)
65
+ else
66
+ this.return = this.subject.send(method, *list_of_args)
67
+ end
68
+ rescue Exception => e
69
+ this.set_backtrace(example, e, given_caller)
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+ def set_backtrace(example, e, given_caller)
76
+ trace = e.backtrace
77
+
78
+ # TODO very ugly - don't know how to filter rspec own backtrace
79
+ # remove all lines containing ../lib/rspec
80
+ trace.delete_if {|line| line =~ /\/lib\/rspec\//}
81
+ trace.delete_if {|line| line =~ /\/rubygems\/custom_require/}
82
+ trace.delete_if {|line| line =~ /\/bin\/rspec/}
83
+
84
+ bt = trace + given_caller
85
+ e.set_backtrace(bt)
86
+ example.set_exception(e)
87
+ example.execution_result[:exception_encountered] = bt
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,14 @@
1
+ module RSpec::ApiGen
2
+ module MetaHelper
3
+ def self.singleton_of(obj)
4
+ class << obj;
5
+ self;
6
+ end
7
+ end
8
+
9
+ def self.create_singleton_method(obj, name, &block)
10
+ singleton_of(obj).send(:define_method, name, &block)
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module RSpec::ApiGen
2
+
3
+ class Method
4
+ def arg(name, description='')
5
+ Argument.new(name, description)
6
+ end
7
+
8
+ def arg_block(name, description='')
9
+ Argument.new(name, description, true)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,6 @@
1
+ puts "IN VERSION"
2
+ module RSpec
3
+ module ApiGen
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module RSpec::ApiGen
2
+ VERSION = 0.0.1
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec-apigen/meta_helper'
2
+ require 'rspec-apigen/apigen'
3
+ require 'rspec-apigen/method'
4
+ require 'rspec-apigen/argument'
5
+ require 'rspec-apigen/fixture'
6
+ require 'rspec-apigen/given'
7
+ require 'rspec-apigen/config.rb'
8
+ require 'rspec-apigen/formatter'
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec-apigen
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Andreas Ronge
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-09-03 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 0
30
+ - 0
31
+ - beta
32
+ - 20
33
+ version: 2.0.0.beta.20
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ description: Write your API documentation using a custom RSpec DSL instead of using RDoc
37
+ email:
38
+ - andreas.ronge@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - lib/rspec-apigen.rb
47
+ - lib/rspec-apigen/given.rb
48
+ - lib/rspec-apigen/config.rb
49
+ - lib/rspec-apigen/argument.rb
50
+ - lib/rspec-apigen/version.rb~
51
+ - lib/rspec-apigen/apigen.rb
52
+ - lib/rspec-apigen/meta_helper.rb
53
+ - lib/rspec-apigen/method.rb
54
+ - lib/rspec-apigen/formatter.rb
55
+ - lib/rspec-apigen/fixture.rb
56
+ - lib/rspec-apigen/version.rb
57
+ - README.rdoc
58
+ has_rdoc: true
59
+ homepage: http://github.com/andreasronge/rspec-apigen
60
+ licenses: []
61
+
62
+ post_install_message:
63
+ rdoc_options: []
64
+
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ segments:
79
+ - 1
80
+ - 3
81
+ - 6
82
+ version: 1.3.6
83
+ requirements: []
84
+
85
+ rubyforge_project:
86
+ rubygems_version: 1.3.6
87
+ signing_key:
88
+ specification_version: 3
89
+ summary: A plugin for RSpec for generating an API documentation
90
+ test_files: []
91
+