apiculture 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cea2109a603040e6f683ded9261b52bc2d03e53a
4
+ data.tar.gz: 8b99f10724eb4cbd0484ca8ecd85feedf24e3e1f
5
+ SHA512:
6
+ metadata.gz: 90e06f221ad4da20e8ba09a51bde34d62761aad0374a9dd306db8fb3550d52a75fc8c24362277be2a542550de1dbff70443c99a14f998fc4444e292aa785cc5d
7
+ data.tar.gz: fe654fa36f250506311098ff322d3202f6b67d7d1c390fb8241eaddd7608b104e4e40725814e74ff063b1d87914c2a08ffa74e99ed27e5e029d935deb67bb2ec
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+ gem 'sinatra', '~> 1.4', :require => 'sinatra/base'
3
+ gem 'builder'
4
+ gem 'rdiscount'
5
+ gem 'github-markup'
6
+ gem "mustache"
7
+
8
+ group :development do
9
+ gem 'rack-test'
10
+ gem "rspec", "~> 3.1", '< 3.2'
11
+ gem "rdoc", "~> 3.12"
12
+ gem "bundler", "~> 1.0"
13
+ gem "jeweler", "~> 2.0.1"
14
+ gem "simplecov", ">= 0"
15
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2015 WeTransfer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
1
+ # apiculture
2
+
3
+ A little toolkit for building RESTful API backends on top of Sinatra.
4
+
5
+ ## Ideas
6
+
7
+ A simple API definition DSL with simple premises:
8
+
9
+ * Endpoint URLs should be _visible_ in the actual code. The reason for that is with nested
10
+ blocks you inevitably end up setting up context somewhere far away from the terminal route
11
+ that ends up using that context.
12
+ * Explicit allowed/required parameters (both payload/query string and body)
13
+ * Explicit description in front of the API action definition
14
+ * Wrap the actual work into Actions, so that the API definition is mostly routes
15
+
16
+ ## A taste of honey
17
+
18
+ class Api::V2 < Sinatra::Base
19
+
20
+ use Rack::Parser, :content_types => {
21
+ 'application/json' => JSON.method(:load).to_proc
22
+ }
23
+
24
+ extend Apiculture
25
+
26
+ desc 'Create a Contact'
27
+ required_param :name, 'Name of the person', String
28
+ param :email, 'Email address of the person', String
29
+ param :phone, 'Phone number', String, cast: ->(v) { v.scan(/\d/).flatten.join }
30
+ param :notes, 'Notes about this person', String
31
+ api_method :post, '/contacts' do
32
+ # anything allowed within Sinatra actions is allowed here, and
33
+ # works exactly the same - but we suggest using Actions instead.
34
+ action_result CreateContact # uses Api::V2::CreateContact
35
+ end
36
+
37
+ desc 'Fetch a Contact'
38
+ route_param :id, 'ID of the person'
39
+ responds_with 200, 'Contact data', {name: 'John Appleseed', id: "ac19...fefg"}
40
+ api_method :get, '/contacts/:id' do | person_id |
41
+ json Person.find(person_id).to_json
42
+ end
43
+ end
44
+
45
+ ## Generating documentation
46
+
47
+ For the aforementioned example:
48
+
49
+ File.open('API.html', 'w') do |f|
50
+ f << Api::V2.api_documentation.to_html
51
+ end
52
+
53
+ or to get it in Markdown:
54
+
55
+ File.open('API.md', 'w') do |f|
56
+ f << Api::V2.api_documentation.to_markdown
57
+ end
58
+
59
+ ## Running the tests
60
+
61
+ $bundle exec rspec
62
+
63
+ If you want to also examine the HTML documentation that gets built during the test, set `SHOW_TEST_DOC` in env:
64
+
65
+ $SHOW_TEST_DOC=yes bundle exec rspec
66
+
67
+ Note that this requires presence of the `open` commandline utility (should be available on both OSX and Linux).
68
+
69
+ ## Contributing to apiculture
70
+
71
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
72
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
73
+ * Fork the project.
74
+ * Start a feature/bugfix branch.
75
+ * Commit and push until you are happy with your contribution.
76
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
77
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
78
+
79
+ ## Copyright
80
+
81
+ Copyright (c) 2015 WeTransfer. See LICENSE.txt for
82
+ further details.
83
+
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require_relative 'lib/apiculture/version'
15
+ require 'jeweler'
16
+ Jeweler::Tasks.new do |gem|
17
+ gem.name = "apiculture"
18
+ gem.version = Apiculture::VERSION
19
+ gem.homepage = "https://github.com/WeTransfer/apiculture"
20
+ gem.license = "MIT"
21
+ gem.description = %Q{A toolkit for building REST APIs on top of Sinatra}
22
+ gem.summary = %Q{Sweet API sauce on top of Sintra}
23
+ gem.email = "me@julik.nl"
24
+ gem.authors = ["Julik Tarkhanov", "WeTransfer"]
25
+ # dependencies defined in Gemfile
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.pattern = FileList['spec/**/*_spec.rb']
33
+ end
34
+
35
+ task :default => :spec
36
+
37
+ require 'rdoc/task'
38
+ Rake::RDocTask.new do |rdoc|
39
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
40
+
41
+ rdoc.rdoc_dir = 'rdoc'
42
+ rdoc.title = "apiculture #{version}"
43
+ rdoc.rdoc_files.include('README*')
44
+ rdoc.rdoc_files.include('lib/**/*.rb')
45
+ end
@@ -0,0 +1,90 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+ # stub: apiculture 0.0.12 ruby lib
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "apiculture"
9
+ s.version = "0.0.12"
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.require_paths = ["lib"]
13
+ s.authors = ["Julik Tarkhanov", "WeTransfer"]
14
+ s.date = "2015-11-16"
15
+ s.description = "A toolkit for building REST APIs on top of Sinatra"
16
+ s.email = "me@julik.nl"
17
+ s.extra_rdoc_files = [
18
+ "LICENSE.txt",
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ "Gemfile",
23
+ "LICENSE.txt",
24
+ "README.md",
25
+ "Rakefile",
26
+ "apiculture.gemspec",
27
+ "lib/apiculture.rb",
28
+ "lib/apiculture/action.rb",
29
+ "lib/apiculture/action_definition.rb",
30
+ "lib/apiculture/app_documentation.rb",
31
+ "lib/apiculture/app_documentation_tpl.mustache",
32
+ "lib/apiculture/markdown_segment.rb",
33
+ "lib/apiculture/method_documentation.rb",
34
+ "lib/apiculture/sinatra_instance_methods.rb",
35
+ "lib/apiculture/timestamp_promise.rb",
36
+ "lib/apiculture/version.rb",
37
+ "spec/apiculture/action_spec.rb",
38
+ "spec/apiculture/app_documentation_spec.rb",
39
+ "spec/apiculture/method_documentation_spec.rb",
40
+ "spec/apiculture_spec.rb",
41
+ "spec/spec_helper.rb"
42
+ ]
43
+ s.homepage = "https://github.com/WeTransfer/apiculture"
44
+ s.licenses = ["MIT"]
45
+ s.rubygems_version = "2.2.2"
46
+ s.summary = "Sweet API sauce on top of Sintra"
47
+
48
+ if s.respond_to? :specification_version then
49
+ s.specification_version = 4
50
+
51
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
52
+ s.add_runtime_dependency(%q<sinatra>, ["~> 1.4"])
53
+ s.add_runtime_dependency(%q<builder>, [">= 0"])
54
+ s.add_runtime_dependency(%q<rdiscount>, [">= 0"])
55
+ s.add_runtime_dependency(%q<github-markup>, [">= 0"])
56
+ s.add_runtime_dependency(%q<mustache>, [">= 0"])
57
+ s.add_development_dependency(%q<rack-test>, [">= 0"])
58
+ s.add_development_dependency(%q<rspec>, ["< 3.2", "~> 3.1"])
59
+ s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
60
+ s.add_development_dependency(%q<bundler>, ["~> 1.0"])
61
+ s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
62
+ s.add_development_dependency(%q<simplecov>, [">= 0"])
63
+ else
64
+ s.add_dependency(%q<sinatra>, ["~> 1.4"])
65
+ s.add_dependency(%q<builder>, [">= 0"])
66
+ s.add_dependency(%q<rdiscount>, [">= 0"])
67
+ s.add_dependency(%q<github-markup>, [">= 0"])
68
+ s.add_dependency(%q<mustache>, [">= 0"])
69
+ s.add_dependency(%q<rack-test>, [">= 0"])
70
+ s.add_dependency(%q<rspec>, ["< 3.2", "~> 3.1"])
71
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
72
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
73
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
74
+ s.add_dependency(%q<simplecov>, [">= 0"])
75
+ end
76
+ else
77
+ s.add_dependency(%q<sinatra>, ["~> 1.4"])
78
+ s.add_dependency(%q<builder>, [">= 0"])
79
+ s.add_dependency(%q<rdiscount>, [">= 0"])
80
+ s.add_dependency(%q<github-markup>, [">= 0"])
81
+ s.add_dependency(%q<mustache>, [">= 0"])
82
+ s.add_dependency(%q<rack-test>, [">= 0"])
83
+ s.add_dependency(%q<rspec>, ["< 3.2", "~> 3.1"])
84
+ s.add_dependency(%q<rdoc>, ["~> 3.12"])
85
+ s.add_dependency(%q<bundler>, ["~> 1.0"])
86
+ s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
87
+ s.add_dependency(%q<simplecov>, [">= 0"])
88
+ end
89
+ end
90
+
@@ -0,0 +1,266 @@
1
+ # Allows brief definitions of APIs for documentation and parameter checks
2
+ module Apiculture
3
+ require_relative 'apiculture/version'
4
+ require_relative 'apiculture/action'
5
+ require_relative 'apiculture/sinatra_instance_methods'
6
+ require_relative 'apiculture/action_definition'
7
+ require_relative 'apiculture/markdown_segment'
8
+ require_relative 'apiculture/timestamp_promise'
9
+
10
+ def self.extended(in_class)
11
+ in_class.send(:include, SinatraInstanceMethods)
12
+ super
13
+ end
14
+
15
+ IDENTITY_PROC = ->(arg) { arg }
16
+
17
+ AC_APPLY_TYPECAST_PROC = ->(cast_proc_or_method, v) {
18
+ cast_proc_or_method.is_a?(Symbol) ? v.public_send(cast_proc_or_method) : cast_proc_or_method.call(v)
19
+ }
20
+
21
+ AC_CHECK_PRESENCE_PROC = ->(name_as_string, params) {
22
+ params.has_key?(name_as_string) or raise MissingParameter.new(name_as_string)
23
+ }
24
+
25
+ AC_CHECK_TYPE_PROC = ->(param, value) {
26
+ value.is_a?(param.ruby_type) or raise ParameterTypeMismatch.new(param, value.class)
27
+ }
28
+
29
+ AC_PERMIT_PROC = ->(maybe_strong_params, param_name) {
30
+ maybe_strong_params.permit(param_name) if maybe_strong_params.respond_to?(:permit)
31
+ }
32
+
33
+ class Parameter < Struct.new(:name, :description, :required, :ruby_type, :cast_proc_or_method)
34
+ # Return Strings since Sinatra prefers string keys for params{}
35
+ def name_as_string; name.to_s; end
36
+ end
37
+
38
+ class RouteParameter < Struct.new(:name, :description)
39
+ def name_as_string; name.to_s; end
40
+ end
41
+
42
+ class PossibleResponse < Struct.new(:http_status_code, :description, :jsonable_object_example)
43
+ def no_body?
44
+ jsonable_object_example.nil?
45
+ end
46
+ end
47
+
48
+ # Indicates where this API will be mounted. This is only used
49
+ # for the generated documentation. In general, this should match
50
+ # the SCRIPT_NAME of the Sinatra application when it will be called.
51
+ # For example, if you use this in your +config.ru+:
52
+ #
53
+ # map('/api/v3') { run MyApi }
54
+ #
55
+ # then it is handy to set that with +mounted_at+ as well so that the API
56
+ # documentation references the mountpoint:
57
+ #
58
+ # mounted_at '/api/v3'
59
+ #
60
+ # Again: this does not change the way requests are handled in any way,
61
+ # it just alters the documentation output.
62
+ def mounted_at(path)
63
+ @apiculture_mounted_at = path.to_s.gsub(/\/$/, '')
64
+ end
65
+
66
+ # Inserts the generation timestamp into the documentation at this point.
67
+ # The timestamp will be not very precise (to the minute) and in UTC time
68
+ def documentation_build_time!
69
+ apiculture_stack << Apiculture::TimestampPromise
70
+ end
71
+
72
+ # Inserts a literal Markdown string into the documentation at this point.
73
+ # For instance, if used after an API method declaration, it will insert
74
+ # the header between the API methods in the doc.
75
+ #
76
+ # api_method :get, '/foo/bar' do
77
+ # #...
78
+ # end
79
+ # markdown_string "# Subsequent methods do thing to Bars"
80
+ # api_method :get, '/bar/thing' do
81
+ # #...
82
+ # end
83
+ def markdown_string(str)
84
+ apiculture_stack << MarkdownSegment.new(str)
85
+ end
86
+
87
+ # Inserts the contents of the file at +path+ into the documentation, using +markdown_string+.
88
+ # For instance, if used after an API method declaration, it will insert
89
+ # the header between the API methods in the doc.
90
+ #
91
+ # markdown_file "SECURITY_CONSIDERATIONS.md"
92
+ # api_method :get, '/bar/thing' do
93
+ # #...
94
+ # end
95
+ def markdown_file(path_to_markdown)
96
+ md = File.read(path_to_markdown).encode(Encoding::UTF_8)
97
+ markdown_string(md)
98
+ end
99
+
100
+ # Describe the API method that is going to be defined
101
+ def desc(action_description)
102
+ @apiculture_action_definition ||= ActionDefinition.new
103
+ @apiculture_action_definition.description = action_description.to_s
104
+ end
105
+
106
+ # Add an optional parameter for the API call
107
+ def param(name, description, ruby_type, cast: IDENTITY_PROC)
108
+ @apiculture_action_definition ||= ActionDefinition.new
109
+ @apiculture_action_definition.parameters << Parameter.new(name, description, required=false, ruby_type, cast)
110
+ end
111
+
112
+ # Add a requred parameter for the API call
113
+ def required_param(name, description, ruby_type, cast: IDENTITY_PROC)
114
+ @apiculture_action_definition ||= ActionDefinition.new
115
+ @apiculture_action_definition.parameters << Parameter.new(name, description, required=true, ruby_type, cast)
116
+ end
117
+
118
+ # Describe a parameter that has to be included in the URL of the API call.
119
+ # Route parameters are always required, and all the parameters specified
120
+ # using +route_param+ should also be included in the path given for the route
121
+ # definition
122
+ def route_param(name, description)
123
+ @apiculture_action_definition ||= ActionDefinition.new
124
+ @apiculture_action_definition.route_parameters << RouteParameter.new(name, description)
125
+ end
126
+
127
+ # Add a possible response, specifying the code and the JSON Response by example.
128
+ # Multiple response packages can be specified.
129
+ def responds_with(http_status, description, example_jsonable_object = nil)
130
+ @apiculture_action_definition ||= ActionDefinition.new
131
+ @apiculture_action_definition.responses << PossibleResponse.new(http_status, description, example_jsonable_object)
132
+ end
133
+
134
+ DefinitionError = Class.new(StandardError)
135
+ ValidationError = Class.new(StandardError)
136
+
137
+ class RouteParameterNotInPath < DefinitionError; end
138
+ class ReservedParameter < DefinitionError; end
139
+ class ConflictingParameter < DefinitionError; end
140
+
141
+ # Gets raised when a parameter is missing
142
+ class MissingParameter < ValidationError
143
+ def initialize(parameter_name)
144
+ super "Missing parameter :#{parameter_name}"
145
+ end
146
+ end
147
+
148
+ # Gets raised when a parameter is supplied and has a wrong type
149
+ class ParameterTypeMismatch < ValidationError
150
+ def initialize(ac_parameter, received_ruby_type)
151
+ parameter_name, expected_type = ac_parameter.name, ac_parameter.ruby_type
152
+ received_type = received_ruby_type
153
+ super "Received #{received_type}, expected #{expected_type} for :#{parameter_name}"
154
+ end
155
+ end
156
+
157
+ # Returns a Proc that calls the strong parameters to check the presence/types
158
+ def parametric_validator_proc_from(parametric_validators)
159
+ required_params = parametric_validators.select{|e| e.required }
160
+ # Return a lambda that will be called with the Sinatra params
161
+ parametric_validation_blk = ->{
162
+ # Within this block +params+ is the Sinatra's instance params
163
+ # Ensure the required parameters are present first, before applying casts/validations etc.
164
+ required_params.each { |param| AC_CHECK_PRESENCE_PROC.call(param.name_as_string, params) }
165
+ parametric_validators.each do |param|
166
+ param_name = param.name_as_string
167
+ next unless params.has_key?(param_name) # this is checked via required_params
168
+
169
+ # Apply the type cast and save it (since using our override we can mutate the params)
170
+ value_after_type_cast = AC_APPLY_TYPECAST_PROC.call(param.cast_proc_or_method, params[param_name])
171
+ params[param_name] = value_after_type_cast
172
+
173
+ # Ensure the typecast value adheres to the enforced Ruby type
174
+ AC_CHECK_TYPE_PROC.call(param, params[param_name])
175
+ # ..permit it in the strong parameters if we support them
176
+ AC_PERMIT_PROC.call(params, param_name)
177
+ end
178
+
179
+ # The following only applies if the app does not use strong_parameters -
180
+ # this makes use of parameter mutability again to kill the parameters that are not permitted
181
+ # or mentioned in the API specification
182
+ unexpected_parameters = params.keys.map(&:to_s) - parametric_validators.map(&:name).map(&:to_s)
183
+ unexpected_parameters.each do | parameter_to_discard |
184
+ # TODO: raise or record a warning
185
+ if env['rack.logger'].respond_to?(:warn)
186
+ env['rack.logger'].warn "Discarding disallowed parameter #{parameter_to_discard.inspect}"
187
+ end
188
+ params.delete(parameter_to_discard)
189
+ end
190
+ }
191
+ end
192
+
193
+ # Serve the documentation for the API at the given URL
194
+ def serve_api_documentation_at(url)
195
+ get(url) do
196
+ content_type :html
197
+ self.class.api_documentation.to_html
198
+ end
199
+ end
200
+
201
+ # Returns an +AppDocumentation+ object for all actions defined so far.
202
+ #
203
+ # MyApi.api_documentation.to_markdown #=> "..."
204
+ # MyApi.api_documentation.to_html #=> "..."
205
+ def api_documentation
206
+ require_relative 'apiculture/app_documentation'
207
+ AppDocumentation.new(self, @apiculture_mounted_at.to_s, @apiculture_actions_and_docs || [])
208
+ end
209
+
210
+ # Define an API method. Under the hood will call the related methods in Sinatra
211
+ # to define the route.
212
+ def api_method(http_verb, path, options={}, &blk)
213
+ action_def = (@apiculture_action_definition || ActionDefinition.new)
214
+ action_def.http_verb = http_verb
215
+ action_def.path = path
216
+
217
+ # Ensure no reserved Sinatra parameters are used
218
+ all_parameter_names = action_def.all_parameter_names_as_strings
219
+ %w( splat captures ).each do | reserved_param |
220
+ if all_parameter_names.include?(reserved_param)
221
+ raise ReservedParameter.new(":#{reserved_param} is a reserved magic parameter name in Sinatra")
222
+ end
223
+ end
224
+
225
+ # Ensure no conflations between route/req params
226
+ seen_params = {}
227
+ all_parameter_names.each do |e|
228
+ if seen_params[e]
229
+ raise ConflictingParameter.new(":#{e} mentioned twice as a possible parameter. Note that URL" +
230
+ " parameters and request parameters share a namespace.")
231
+ else
232
+ seen_params[e] = true
233
+ end
234
+ end
235
+
236
+ # Ensure the path has the route parameters that were predeclared
237
+ action_def.route_parameters.map(&:name).each do | route_parameter_key |
238
+ unless path.include?(':%s' % route_parameter_key)
239
+ raise RouteParameterNotInPath.new("Parameter :#{route_parameter_key} not present in path #{path.inspect}")
240
+ end
241
+ end
242
+
243
+ # TODO: ensure all route parameters are documented
244
+
245
+ # Pick out all the defined parameters and set up a block that can validate them
246
+ # when the action is called. With that, set up the actual Sinatra method that will
247
+ # respond to the request.
248
+ parametric_checker_proc = parametric_validator_proc_from(action_def.parameters)
249
+ public_send(http_verb, path, options) do |*matched_sinatra_route_params|
250
+ # Verify the parameters first
251
+ instance_exec(&parametric_checker_proc)
252
+ # Execute the original action via instance_exec, passing along the route args
253
+ instance_exec(*matched_sinatra_route_params, &blk)
254
+ end
255
+
256
+ # Reset for the subsequent action definition
257
+ @apiculture_action_definition = ActionDefinition.new
258
+ # and store the just defined action for future use
259
+ apiculture_stack << action_def
260
+ end
261
+
262
+ def apiculture_stack
263
+ @apiculture_actions_and_docs ||= []
264
+ @apiculture_actions_and_docs
265
+ end
266
+ end