module Apiculture

Allows brief definitions of APIs for documentation and parameter checks

Constants

AC_APPLY_TYPECAST_PROC
AC_CHECK_PRESENCE_PROC
AC_CHECK_TYPE_PROC
DefinitionError
IDENTITY_PROC
VERSION
ValidationError

Public Class Methods

extended(in_class) click to toggle source
Calls superclass method
# File lib/apiculture.rb, line 13
def self.extended(in_class)
  in_class.send(:include, SinatraInstanceMethods)
  super
end

Public Instance Methods

api_documentation() click to toggle source

Returns an AppDocumentation object for all actions defined so far.

MyApi.api_documentation.to_markdown #=> "..."
MyApi.api_documentation.to_html #=> "..."
# 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) click to toggle source

Define an API method. Under the hood will call the related methods in Sinatra to define the route.

# File lib/apiculture.rb, line 217
def api_method(http_verb, path, options={}, &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, **options) 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(&parametric_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() click to toggle source
# 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) click to toggle source

Describe the API method that is going to be defined

# 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!() click to toggle source

Inserts the generation timestamp into the documentation at this point. The timestamp will be not very precise (to the minute) and in UTC time

# File lib/apiculture.rb, line 72
def documentation_build_time!
  apiculture_stack << Apiculture::TimestampPromise
end
environment() click to toggle source

Based on the RACK_ENV it will generate documentation or not

# File lib/apiculture.rb, line 294
def environment
  @environment ||= ENV.fetch("RACK_ENV", "development")
end
markdown_file(path_to_markdown) click to toggle source

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
# 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) click to toggle source

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
# File lib/apiculture.rb, line 87
def markdown_string(str)
  apiculture_stack << MarkdownSegment.new(str)
end
mounted_at(path) click to toggle source

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.

# 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) click to toggle source

Add an optional parameter for the API call

# 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) click to toggle source

Returns a Proc that calls the strong parameters to check the presence/types

# 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) click to toggle source

Add a requred parameter for the API call

# 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) click to toggle source

Add a possible response, specifying the code and the JSON Response by example. Multiple response packages can be specified.

# 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) click to toggle source

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

# 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) click to toggle source

Serve the documentation for the API at the given URL

# 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