Module: Apiculture
- Defined in:
- lib/apiculture.rb,
lib/apiculture/version.rb
Overview
Allows brief definitions of APIs for documentation and parameter checks
Defined Under Namespace
Modules: SinatraInstanceMethods Classes: Action, ActionDefinition, AppDocumentation, ConflictingParameter, MarkdownSegment, MethodDocumentation, MissingParameter, Parameter, ParameterTypeMismatch, PossibleResponse, ReservedParameter, RouteParameter, RouteParameterNotInPath, TimestampPromise
Constant Summary collapse
- IDENTITY_PROC =
->(arg) { arg }
- AC_APPLY_TYPECAST_PROC =
->(cast_proc_or_method, v) { cast_proc_or_method.is_a?(Symbol) ? v.public_send(cast_proc_or_method) : cast_proc_or_method.call(v) }
- AC_CHECK_PRESENCE_PROC =
->(name_as_string, params) { params.has_key?(name_as_string) or raise MissingParameter.new(name_as_string) }
- AC_CHECK_TYPE_PROC =
->(param, value) { param.matchable === value or raise ParameterTypeMismatch.new(param, value.class) }
- DefinitionError =
Class.new(StandardError)
- ValidationError =
Class.new(StandardError)
- VERSION =
'0.0.19'
Class Method Summary collapse
Instance Method Summary collapse
-
#api_documentation ⇒ Object
Returns an
AppDocumentation
object for all actions defined so far. -
#api_method(http_verb, path, options = {}, &blk) ⇒ Object
Define an API method.
- #apiculture_stack ⇒ Object
-
#desc(action_description) ⇒ Object
Describe the API method that is going to be defined.
-
#documentation_build_time! ⇒ Object
Inserts the generation timestamp into the documentation at this point.
-
#markdown_file(path_to_markdown) ⇒ Object
Inserts the contents of the file at
path
into the documentation, usingmarkdown_string
. -
#markdown_string(str) ⇒ Object
Inserts a literal Markdown string into the documentation at this point.
-
#mounted_at(path) ⇒ Object
Indicates where this API will be mounted.
-
#param(name, description, matchable, cast: IDENTITY_PROC) ⇒ Object
Add an optional parameter for the API call.
-
#parametric_validator_proc_from(parametric_validators, implicitly_defined_route_parameter_names) ⇒ Object
Returns a Proc that calls the strong parameters to check the presence/types.
-
#required_param(name, description, matchable, cast: IDENTITY_PROC) ⇒ Object
Add a requred parameter for the API call.
-
#responds_with(http_status, description, example_jsonable_object = nil) ⇒ Object
Add a possible response, specifying the code and the JSON Response by example.
-
#route_param(name, description, matchable = String, cast: IDENTITY_PROC) ⇒ Object
Describe a parameter that has to be included in the URL of the API call.
-
#serve_api_documentation_at(url) ⇒ Object
Serve the documentation for the API at the given URL.
Class Method Details
.extended(in_class) ⇒ Object
10 11 12 13 |
# File 'lib/apiculture.rb', line 10 def self.extended(in_class) in_class.send(:include, SinatraInstanceMethods) super end |
Instance Method Details
#api_documentation ⇒ Object
Returns an AppDocumentation
object for all actions defined so far.
MyApi.api_documentation.to_markdown #=> "..."
MyApi.api_documentation.to_html #=> "..."
202 203 204 205 |
# File 'lib/apiculture.rb', line 202 def api_documentation require_relative 'apiculture/app_documentation' AppDocumentation.new(self, @apiculture_mounted_at.to_s, @apiculture_actions_and_docs || []) end |
#api_method(http_verb, path, options = {}, &blk) ⇒ Object
Define an API method. Under the hood will call the related methods in Sinatra to define the route.
209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/apiculture.rb', line 209 def api_method(http_verb, path, ={}, &blk) action_def = (@apiculture_action_definition || ActionDefinition.new) action_def.http_verb = http_verb action_def.path = path # Ensure no reserved Sinatra parameters are used all_parameter_names = action_def.all_parameter_names_as_strings %w( splat captures ).each do | reserved_param | if all_parameter_names.include?(reserved_param) raise ReservedParameter.new(":#{reserved_param} is a reserved magic parameter name in Sinatra") end end # Ensure no conflations between route/req params seen_params = {} all_parameter_names.each do |e| if seen_params[e] raise ConflictingParameter.new(":#{e} mentioned twice as a possible parameter. Note that URL" + " parameters and request parameters share a namespace.") else seen_params[e] = true end end # Ensure the path has the route parameters that were predeclared action_def.route_parameters.map(&:name).each do | route_parameter_key | unless path.include?(':%s' % route_parameter_key) raise RouteParameterNotInPath.new("Parameter :#{route_parameter_key} not present in path #{path.inspect}") end end # TODO: ensure all route parameters are documented # Pick out all the defined parameters and set up a block that can validate them # when the action is called. With that, set up the actual Sinatra method that will # respond to the request. We take care to preserve all the params that have NOT been documented # using Apiculture but _were_ in fact specified in the actual path. route_parameter_names = path.scan(/:([^:\/]+)/).flatten.map(&:to_sym) parametric_checker_proc = parametric_validator_proc_from(action_def.parameters + action_def.route_parameters, route_parameter_names) public_send(http_verb, path, ) do |*matched_sinatra_route_params| # Extract all the parameter names from the route path as given to the method route_parameters = Hash[route_parameter_names.zip(matched_sinatra_route_params)] # Apply route parameter checks, but only to params that were defined in the Apiculture action descriptor. # All the other params have to go via bypass. checked_route_parameters = action_def.route_parameters.select {|par| route_parameter_names.include?(par.name) } checked_route_parameters.each do |route_param| # Apply the type cast and save it (since using our override we can mutate the params) value_from_route_params = route_parameters.fetch(route_param.name) value_after_type_cast = AC_APPLY_TYPECAST_PROC.call(route_param.cast_proc_or_method, value_from_route_params) # Ensure the typecast value adheres to the enforced Ruby type AC_CHECK_TYPE_PROC.call(route_param, value_after_type_cast) # ..and overwrite it in the route parameters hash route_parameters[route_param.name] = value_after_type_cast end # Execute parametric checks on all the OTHER params (forms etc.) instance_exec(¶metric_checker_proc) # Execute the original action via instance_exec, passing along the route args instance_exec(*route_parameters.values, &blk) end # Reset for the subsequent action definition @apiculture_action_definition = ActionDefinition.new # and store the just defined action for future use apiculture_stack << action_def end |
#apiculture_stack ⇒ Object
276 277 278 279 |
# File 'lib/apiculture.rb', line 276 def apiculture_stack @apiculture_actions_and_docs ||= [] @apiculture_actions_and_docs end |
#desc(action_description) ⇒ Object
Describe the API method that is going to be defined
96 97 98 99 |
# File 'lib/apiculture.rb', line 96 def desc(action_description) @apiculture_action_definition ||= ActionDefinition.new @apiculture_action_definition.description = action_description.to_s end |
#documentation_build_time! ⇒ Object
Inserts the generation timestamp into the documentation at this point. The timestamp will be not very precise (to the minute) and in UTC time
63 64 65 |
# File 'lib/apiculture.rb', line 63 def documentation_build_time! apiculture_stack << Apiculture::TimestampPromise end |
#markdown_file(path_to_markdown) ⇒ Object
Inserts the contents of the file at path
into the documentation, using markdown_string
. For instance, if used after an API method declaration, it will insert the header between the API methods in the doc.
markdown_file "SECURITY_CONSIDERATIONS.md"
api_method :get, '/bar/thing' do
#...
end
90 91 92 93 |
# File 'lib/apiculture.rb', line 90 def markdown_file(path_to_markdown) md = File.read(path_to_markdown).encode(Encoding::UTF_8) markdown_string(md) end |
#markdown_string(str) ⇒ Object
Inserts a literal Markdown string into the documentation at this point. For instance, if used after an API method declaration, it will insert the header between the API methods in the doc.
api_method :get, '/foo/bar' do
#...
end
markdown_string "# Subsequent methods do thing to Bars"
api_method :get, '/bar/thing' do
#...
end
78 79 80 |
# File 'lib/apiculture.rb', line 78 def markdown_string(str) apiculture_stack << MarkdownSegment.new(str) end |
#mounted_at(path) ⇒ Object
Indicates where this API will be mounted. This is only used for the generated documentation. In general, this should match the SCRIPT_NAME of the Sinatra application when it will be called. For example, if you use this in your config.ru
:
map('/api/v3') { run MyApi }
then it is handy to set that with mounted_at
as well so that the API documentation references the mountpoint:
mounted_at '/api/v3'
Again: this does not change the way requests are handled in any way, it just alters the documentation output.
57 58 59 |
# File 'lib/apiculture.rb', line 57 def mounted_at(path) @apiculture_mounted_at = path.to_s.gsub(/\/$/, '') end |
#param(name, description, matchable, cast: IDENTITY_PROC) ⇒ Object
Add an optional parameter for the API call
102 103 104 105 |
# File 'lib/apiculture.rb', line 102 def param(name, description, matchable, cast: IDENTITY_PROC) @apiculture_action_definition ||= ActionDefinition.new @apiculture_action_definition.parameters << Parameter.new(name, description, required=false, matchable, cast) end |
#parametric_validator_proc_from(parametric_validators, implicitly_defined_route_parameter_names) ⇒ Object
Returns a Proc that calls the strong parameters to check the presence/types
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/apiculture.rb', line 153 def parametric_validator_proc_from(parametric_validators, implicitly_defined_route_parameter_names) required_params = parametric_validators.select{|e| e.required } # Return a lambda that will be called with the Sinatra params parametric_validation_blk = ->{ # Within this block +params+ is the Sinatra's instance params # Ensure the required parameters are present first, before applying casts/validations etc. required_params.each { |param| AC_CHECK_PRESENCE_PROC.call(param.name_as_string, params) } parametric_validators.each do |param| param_name = param.name_as_string next unless params.has_key?(param_name) # this is checked via required_params # Apply the type cast and save it (since using our override we can mutate the params) value_after_type_cast = AC_APPLY_TYPECAST_PROC.call(param.cast_proc_or_method, params[param_name]) params[param_name] = value_after_type_cast # Ensure the typecast value adheres to the enforced Ruby type AC_CHECK_TYPE_PROC.call(param, params[param_name]) end # The following only applies if the app does not use strong_parameters - # this makes use of parameter mutability again to kill the parameters that are not permitted # or mentioned in the API specification. We need to keep the params which are specified in the # route but not documented via Apiculture though unexpected_parameters = Set.new(params.keys.map(&:to_s)) - Set.new(parametric_validators.map(&:name).map(&:to_s)) - Set.new(implicitly_defined_route_parameter_names.map(&:to_s)) unexpected_parameters.each do | parameter_to_discard | # TODO: raise or record a warning if env['rack.logger'].respond_to?(:warn) env['rack.logger'].warn "Discarding disallowed parameter #{parameter_to_discard.inspect}" end params.delete(parameter_to_discard) end } end |
#required_param(name, description, matchable, cast: IDENTITY_PROC) ⇒ Object
Add a requred parameter for the API call
108 109 110 111 |
# File 'lib/apiculture.rb', line 108 def required_param(name, description, matchable, cast: IDENTITY_PROC) @apiculture_action_definition ||= ActionDefinition.new @apiculture_action_definition.parameters << Parameter.new(name, description, required=true, matchable, cast) end |
#responds_with(http_status, description, example_jsonable_object = nil) ⇒ Object
Add a possible response, specifying the code and the JSON Response by example. Multiple response packages can be specified.
124 125 126 127 |
# File 'lib/apiculture.rb', line 124 def responds_with(http_status, description, example_jsonable_object = nil) @apiculture_action_definition ||= ActionDefinition.new @apiculture_action_definition.responses << PossibleResponse.new(http_status, description, example_jsonable_object) end |
#route_param(name, description, matchable = String, cast: IDENTITY_PROC) ⇒ Object
Describe a parameter that has to be included in the URL of the API call. Route parameters are always required, and all the parameters specified using route_param
should also be included in the path given for the route definition
117 118 119 120 |
# File 'lib/apiculture.rb', line 117 def route_param(name, description, matchable = String, cast: IDENTITY_PROC) @apiculture_action_definition ||= ActionDefinition.new @apiculture_action_definition.route_parameters << RouteParameter.new(name, description, required=false, matchable, cast) end |
#serve_api_documentation_at(url) ⇒ Object
Serve the documentation for the API at the given URL
191 192 193 194 195 196 |
# File 'lib/apiculture.rb', line 191 def serve_api_documentation_at(url) get(url) do content_type :html self.class.api_documentation.to_html end end |