class Praxis::ActionDefinition

Attributes

doc_decorations[RW]
api_definition[R]
endpoint_definition[R]
metadata[R]

opaque hash of user-defined medata, used to decorate the definition, and also available in the generated JSON documents

name[RW]

Setter/reader for a possible ‘sister’ action that is defined as post, and has the payload with the same structure as this GET action (with the exception of the params in the path attributes)

responses[R]
route[R]
sister_get_action[RW]

Setter/reader for a possible ‘sister’ action that is defined as post, and has the payload with the same structure as this GET action (with the exception of the params in the path attributes)

sister_post_action[RW]

Setter/reader for a possible ‘sister’ action that is defined as post, and has the payload with the same structure as this GET action (with the exception of the params in the path attributes)

traits[R]

Public Class Methods

decorate_docs(&callback) click to toggle source
# File lib/praxis/action_definition.rb, line 30
def self.decorate_docs(&callback)
  doc_decorations << callback
end
new(name, endpoint_definition, **_opts, &block) click to toggle source
# File lib/praxis/action_definition.rb, line 34
def initialize(name, endpoint_definition, **_opts, &block)
  @name = name
  @endpoint_definition = endpoint_definition
  @responses = {}
  @metadata = {}
  @route = nil
  @traits = []

  if (media_type = endpoint_definition.media_type) && (media_type.is_a?(Class) && media_type < Praxis::Types::MediaTypeCommon)
    @reference_media_type = media_type
  end

  version = endpoint_definition.version
  api_info = ApiDefinition.instance.info(endpoint_definition.version)

  route_base = "#{api_info.base_path}#{endpoint_definition.version_prefix}"
  prefix = Array(endpoint_definition.routing_prefix)

  @routing_config = RoutingConfig.new(version: version, base: route_base, prefix: prefix)

  endpoint_definition.traits.each do |trait|
    self.trait(trait)
  end

  endpoint_definition.action_defaults.apply!(self)

  instance_eval(&block) if block_given?
end
url_description(route:, params_example:, params:) click to toggle source
# File lib/praxis/action_definition.rb, line 171
def self.url_description(route:, params_example:, params:)
  route_description = route.describe

  example_hash = params_example ? params_example.dump : {}
  hash = route.example(example_hash: example_hash, params: params)

  query_string = URI.encode_www_form(hash[:query_params])
  url = hash[:url]
  url = [url, query_string].join('?') unless query_string.empty?

  route_description[:example] = url
  route_description
end

Public Instance Methods

_internal_set(**args) click to toggle source
# File lib/praxis/action_definition.rb, line 381
def _internal_set(**args)
  @payload = args[:payload] if args.key?(:payload)
  @params = args[:params] if args.key?(:params)
end
clone_action_as(name:) click to toggle source
# File lib/praxis/action_definition.rb, line 365
def clone_action_as(name:)
  cloned = clone
  cloned.instance_eval do
    @name = name.to_sym
    @description = @description.clone
    @metadata = @metadata.clone
    @params = @params.clone
    @responses = @responses.clone
    @route = @route.clone
    @routing_config = @routing_config.clone
    @sister_post_action = @sister_post_action.clone
    @traits = @traits.clone
  end
  cloned
end
clone_action_as_post(at:) click to toggle source
# File lib/praxis/action_definition.rb, line 328
def clone_action_as_post(at:)
  action_name = name
  cloned = clone_action_as(name: "#{action_name}_with_post")

  # route
  raise "Only GET actions support the 'enable_large_params_proxy_action' DSL. Action #{action_name} is a #{rt.verb}" unless route.verb == 'GET'

  cloned.instance_eval do
    routing do
      # Double slash, as we do know the complete prefixed orig path at this point and we don't want the prefix to be applied again...
      post "/#{at}"
    end
  end

  # Payload
  raise "Using enable_large_params_proxy_action for an action requires the GET payload to be empty. Action #{name} has a payload defined" unless payload.nil?

  route_params = route.path.named_captures.keys.collect(&:to_sym)
  params_in_route = []
  params_in_query = []
  cloned.params.type.attributes.each do |k, _val|
    if route_params.include? k
      params_in_route.push k
    else
      params_in_query.push k
    end
  end

  cloned._internal_set(
    payload: cloned.params.duplicate(type: params.type.clone.slice!(*params_in_query)),
    params: cloned.params.duplicate(type: params.type.clone.slice!(*params_in_route))
  )
  cloned.sister_get_action = self
  self.sister_post_action = cloned
  cloned
end
create_attribute(type = Attributor::Struct, **opts, &block) click to toggle source
# File lib/praxis/action_definition.rb, line 89
def create_attribute(type = Attributor::Struct, **opts, &block)
  opts[:reference] = @reference_media_type if !opts.key?(:reference) && (@reference_media_type && block)

  Attributor::Attribute.new(type, opts, &block)
end
derive_content_type(example, handler_name) click to toggle source

Determine the content_type to report for a given example, using handler_name if possible.

Considers any pre-defined set of values on the content_type attributge of the headers.

# File lib/praxis/action_definition.rb, line 259
def derive_content_type(example, handler_name)
  # MultipartArrays *must* use the provided content_type
  return MediaTypeIdentifier.load(example.content_type) if example.is_a? Praxis::Types::MultipartArray

  _, content_type_attribute = headers&.attributes&.find { |k, _v| k.to_s =~ /^content[-_]{1}type$/i }
  if content_type_attribute&.options&.key?(:values)

    # if any defined value match the preferred handler_name, return it
    content_type_attribute.options[:values].each do |ct|
      mti = MediaTypeIdentifier.load(ct)
      return mti if mti.handler_name == handler_name
    end

    # otherwise, pick the first
    pick = MediaTypeIdentifier.load(content_type_attribute.options[:values].first)

    # and return that one if it already corresponds to a registered handler
    # otherwise, add the encoding
    return pick if Praxis::Application.instance.handlers.include?(pick.handler_name)

    return pick + handler_name
  end

  # generic default encoding
  MediaTypeIdentifier.load("application/#{handler_name}")
end
describe(context: nil) click to toggle source
# File lib/praxis/action_definition.rb, line 185
def describe(context: nil)
  {}.tap do |hash|
    hash[:description] = description
    hash[:name] = name
    hash[:metadata] = metadata
    if headers
      headers_example = headers.example(context)
      hash[:headers] = headers_description(example: headers_example)
    end
    if params
      params_example = params.example(context)
      hash[:params] = params_description(example: params_example)
    end
    if payload
      payload_example = payload.example(context)

      hash[:payload] = payload_description(example: payload_example)
    end

    hash[:responses] = responses.each_with_object({}) do |(_response_name, response), memo|
      memo[response.name] = response.describe(context: context)
    end
    hash[:traits] = traits if traits.any?
    # FIXME: change to :routes along with api browser
    # FIXME: change urls to url ... (along with the browser)
    hash[:urls] = [ActionDefinition.url_description(route: route, params: params, params_example: params_example)]
    self.class.doc_decorations.each do |callback|
      callback.call(self, hash)
    end
  end
end
description(text = nil) click to toggle source
# File lib/praxis/action_definition.rb, line 166
def description(text = nil)
  @description = text if text
  @description
end
enable_large_params_proxy_action(at: true) click to toggle source
# File lib/praxis/action_definition.rb, line 319
def enable_large_params_proxy_action(at: true)
  self.sister_post_action = at # Just true to mark it for now (needs to be lazily evaled)
end
headers(type = nil, **opts, &block) click to toggle source
# File lib/praxis/action_definition.rb, line 129
def headers(type = nil, **opts, &block)
  return @headers unless block

  unless opts.key? :required
    opts[:required] = true # Make the payload required by default
  end

  if @headers
    update_attribute(@headers, opts, block)
  else
    type ||= Attributor::Hash.of(key: String)
    @headers = create_attribute(type,
                                dsl_compiler: HeadersDSLCompiler, case_insensitive_load: true,
                                **opts, &block)

    @headers
  end
  @precomputed_header_keys_for_rack = nil # clear memoized data
end
headers_description(example:) click to toggle source
# File lib/praxis/action_definition.rb, line 217
def headers_description(example:)
  output = headers.describe(example: example)
  required_headers = headers.attributes.select { |_k, attr| attr.options && attr.options[:required] == true }
  output[:example] = required_headers.each_with_object({}) do |(name, _attr), hash|
    hash[name] = example[name].to_s # Some simple types (like Boolean) can be used as header values, but must convert back to s
  end
  output
end
nodoc!() click to toggle source
# File lib/praxis/action_definition.rb, line 315
def nodoc!
  metadata[:doc_visibility] = :none
end
params(type = Attributor::Struct, **opts, &block) click to toggle source
# File lib/praxis/action_definition.rb, line 95
def params(type = Attributor::Struct, **opts, &block)
  return @params if !block && (opts.nil? || opts.empty?) && type == Attributor::Struct

  unless opts.key? :required
    opts[:required] = true # Make the payload required by default
  end

  if @params
    raise Exceptions::InvalidConfiguration, "Invalid type received for extending params: #{type.name}" unless type == Attributor::Struct && @params.type < Attributor::Struct

    update_attribute(@params, opts, block)
  else
    @params = create_attribute(type, **opts, &block)
  end

  @params
end
params_description(example:) click to toggle source
# File lib/praxis/action_definition.rb, line 226
def params_description(example:)
  route_params = []
  if route.nil?
    warn "Warning: No route defined for #{endpoint_definition.name}##{name}."
  else
    route_params = route.path
                        .named_captures
                        .keys
                        .collect(&:to_sym)
  end

  desc = params.describe(example: example)
  desc[:type][:attributes].each_key do |k|
    source = if route_params.include? k
               'url'
             else
               'query'
             end
    desc[:type][:attributes][k][:source] = source
  end
  required_params = desc[:type][:attributes].select { |_k, v| v[:source] == 'query' && v[:required] == true }.keys
  phash = required_params.each_with_object({}) do |name, hash|
    hash[name] = example[name]
  end
  desc[:example] = URI.encode_www_form(phash)
  desc
end
payload(type = Attributor::Struct, **opts, &block) click to toggle source
# File lib/praxis/action_definition.rb, line 113
def payload(type = Attributor::Struct, **opts, &block)
  return @payload if !block && (opts.nil? || opts.empty?) && type == Attributor::Struct

  unless opts.key?(:required)
    opts = { required: true, null: false }.merge(opts) # Make the payload required and non-nullable by default
  end

  if @payload
    raise Exceptions::InvalidConfiguration, "Invalid type received for extending params: #{type.name}" unless type == Attributor::Struct && @payload.type < Attributor::Struct

    update_attribute(@payload, opts, block)
  else
    @payload = create_attribute(type, **opts, &block)
  end
end
payload_description(example:) click to toggle source
# File lib/praxis/action_definition.rb, line 286
def payload_description(example:)
  hash = payload.describe(example: example)

  hash[:examples] = {}

  default_handlers = ApiDefinition.instance.info.consumes

  default_handlers.each do |default_handler|
    dumped_payload = payload.dump(example, default_format: default_handler)

    content_type = derive_content_type(example, default_handler)
    handler = Praxis::Application.instance.handlers[content_type.handler_name]

    # in case handler is nil, use dumped_payload as-is.
    generated_payload = if handler.nil?
                          dumped_payload
                        else
                          handler.generate(dumped_payload)
                        end

    hash[:examples][default_handler] = {
      content_type: content_type.to_s,
      body: generated_payload
    }
  end

  hash
end
precomputed_header_keys_for_rack() click to toggle source

Good optimization to avoid creating lots of strings and comparisons on a per-request basis. However, this is hacky, as it is rack-specific, and does not really belong here

# File lib/praxis/action_definition.rb, line 152
def precomputed_header_keys_for_rack
  @precomputed_header_keys_for_rack ||= @headers.attributes.keys.each_with_object({}) do |key, hash|
    name = key.to_s
    name = "HTTP_#{name.gsub('-', '_').upcase}" unless %w[CONTENT_TYPE CONTENT_LENGTH].include?(name)
    hash[name] = key
  end
end
resource_definition() click to toggle source
DEPRECATED
  • Warn of the change of method name for the transition

# File lib/praxis/action_definition.rb, line 324
def resource_definition
  raise 'Praxis::ActionDefinition does not use `resource_definition` any longer. Use `endpoint_definition` instead.'
end
response(name, type = nil, **args, &block) click to toggle source
# File lib/praxis/action_definition.rb, line 76
def response(name, type = nil, **args, &block)
  if type
    # should verify type is a media type

    type = type.construct(block) if block_given?

    args[:media_type] = type
  end

  template = ApiDefinition.instance.response(name)
  @responses[name] = template.compile(self, **args)
end
routing(&block) click to toggle source
# File lib/praxis/action_definition.rb, line 160
def routing(&block)
  @routing_config.instance_eval(&block)

  @route = @routing_config.route
end
trait(trait_name) click to toggle source
# File lib/praxis/action_definition.rb, line 63
def trait(trait_name)
  raise Exceptions::InvalidTrait, "Trait #{trait_name} not found in the system" unless ApiDefinition.instance.traits.key? trait_name

  trait = ApiDefinition.instance.traits.fetch(trait_name)
  trait.apply!(self)
  traits << trait_name
end
update_attribute(attribute, options, block) click to toggle source
# File lib/praxis/action_definition.rb, line 71
def update_attribute(attribute, options, block)
  attribute.options.merge!(options)
  attribute.type.attributes(**options, &block)
end