Module: Apiculture
- Defined in:
- lib/apiculture.rb,
lib/apiculture/version.rb,
lib/apiculture/indifferent_hash.rb
Overview
Allows brief definitions of APIs for documentation and parameter checks
Defined Under Namespace
Modules: SinatraInstanceMethods Classes: Action, ActionDefinition, AppDocumentation, ConflictingParameter, IndifferentHash, MarkdownSegment, MethodDocumentation, MissingParameter, Parameter, ParameterTypeMismatch, PossibleResponse, ReservedParameter, RouteParameter, RouteParameterNotInPath, TimestampPromise, Void
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.2.2'.freeze
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.
-
#environment ⇒ Object
Based on the RACK_ENV it will generate documentation or not.
-
#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
13 14 15 16 |
# File 'lib/apiculture.rb', line 13 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 #=> "..."
211 212 213 |
# File 'lib/apiculture.rb', line 211 def api_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.
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 275 276 277 278 279 280 281 282 |
# File 'lib/apiculture.rb', line 217 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
284 285 286 287 288 289 290 291 |
# File 'lib/apiculture.rb', line 284 def apiculture_stack if environment == "development" @apiculture_actions_and_docs ||= [] else @apiculture_actions_and_docs ||= Void.new end @apiculture_actions_and_docs end |
#desc(action_description) ⇒ Object
Describe the API method that is going to be defined
105 106 107 108 |
# File 'lib/apiculture.rb', line 105 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
72 73 74 |
# File 'lib/apiculture.rb', line 72 def documentation_build_time! apiculture_stack << Apiculture::TimestampPromise end |
#environment ⇒ Object
Based on the RACK_ENV it will generate documentation or not
294 295 296 |
# File 'lib/apiculture.rb', line 294 def environment @environment ||= ENV.fetch("RACK_ENV", "development") 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
99 100 101 102 |
# File 'lib/apiculture.rb', line 99 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
87 88 89 |
# File 'lib/apiculture.rb', line 87 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.
66 67 68 |
# File 'lib/apiculture.rb', line 66 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
111 112 113 114 |
# File 'lib/apiculture.rb', line 111 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
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 189 190 191 192 193 194 195 196 197 |
# File 'lib/apiculture.rb', line 162 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
117 118 119 120 |
# File 'lib/apiculture.rb', line 117 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.
133 134 135 136 |
# File 'lib/apiculture.rb', line 133 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
126 127 128 129 |
# File 'lib/apiculture.rb', line 126 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
200 201 202 203 204 205 |
# File 'lib/apiculture.rb', line 200 def serve_api_documentation_at(url) get(url) do content_type :html self.class.api_documentation.to_html end end |