class HaveAPI::Server

Attributes

action_state[RW]
auth_chain[R]
default_version[RW]
extensions[R]
module_name[R]
root[R]
routes[R]
versions[R]

Public Class Methods

new(module_name = HaveAPI.module_name) click to toggle source
# File lib/haveapi/server.rb, line 184
def initialize(module_name = HaveAPI.module_name)
  @module_name = module_name
  @allowed_headers = ['Content-Type']
  @auth_chain = HaveAPI::Authentication::Chain.new(self)
  @extensions = []
end

Public Instance Methods

add_auth_module(v, name, mod, prefix: '') click to toggle source
# File lib/haveapi/server.rb, line 618
def add_auth_module(v, name, mod, prefix: '')
  @routes[v] ||= { authentication: { name => { resources: {} } } }

  HaveAPI.get_version_resources(mod, v).each do |r|
    mount_resource("#{@root}_auth/#{prefix}/", v, r, @routes[v][:authentication][name][:resources])
  end
end
add_auth_routes(v, provider, prefix: '') click to toggle source

@param v [String] API version @param provider [Authentication::Base] @param prefix [String]

# File lib/haveapi/server.rb, line 614
def add_auth_routes(v, provider, prefix: '')
  provider.register_routes(@sinatra, "#{@root}_auth/#{prefix}")
end
allow_header(name) click to toggle source
# File lib/haveapi/server.rb, line 626
def allow_header(name)
  @allowed_headers << name unless @allowed_headers.include?(name)
  @allowed_headers_str = nil
end
allowed_headers() click to toggle source
# File lib/haveapi/server.rb, line 631
def allowed_headers
  return @allowed_headers_str if @allowed_headers_str

  @allowed_headers_str = @allowed_headers.join(',')
end
app() click to toggle source
# File lib/haveapi/server.rb, line 637
def app
  @sinatra
end
describe(context) click to toggle source
# File lib/haveapi/server.rb, line 565
def describe(context)
  context.version = @default_version

  ret = {
    default_version: @default_version,
    versions: { default: describe_version(context) }
  }

  @versions.each do |v|
    context.version = v
    ret[:versions][v] = describe_version(context)
  end

  ret
end
describe_resource(r, hash, context) click to toggle source
# File lib/haveapi/server.rb, line 603
def describe_resource(r, hash, context)
  r.describe(hash, context)
end
describe_version(context) click to toggle source
# File lib/haveapi/server.rb, line 581
def describe_version(context)
  ret = {
    authentication: @auth_chain.describe(context),
    resources: {},
    meta: Metadata.describe,
    help: version_prefix(context.version)
  }

  # puts JSON.pretty_generate(@routes)

  @routes[context.version][:resources].each do |resource, children|
    r_name = resource.resource_name.underscore
    r_desc = describe_resource(resource, children, context)

    unless r_desc[:actions].empty? && r_desc[:resources].empty?
      ret[:resources][r_name] = r_desc
    end
  end

  ret
end
mount(prefix = '/') click to toggle source

Load routes for all resource from included API versions. All routes are mounted under prefix ‘path`. If no default version is set, the last included version is used.

# File lib/haveapi/server.rb, line 216
def mount(prefix = '/')
  @root = prefix

  @sinatra = Sinatra.new do
    # Preload template engine for .md -- without this, tilt will not search
    # for markdown files with extension .md, only .markdown
    Tilt[:md]

    set :views, "#{settings.root}/views"
    set :public_folder, "#{settings.root}/public"
    set :bind, '0.0.0.0'

    if settings.development?
      set :dump_errors, true
      set :raise_errors, true
      set :show_exceptions, false
    end

    helpers Sinatra::Cookies
    helpers ServerHelpers
    helpers DocHelpers

    before do
      if request.env['HTTP_ORIGIN']
        headers 'access-control-allow-origin' => '*',
                'access-control-allow-credentials' => 'false'
      end
    end

    not_found do
      setup_formatter
      report_error(404, {}, 'Action not found') unless @halted
    end

    after do
      if Object.const_defined?(:ActiveRecord)
        ActiveRecord::Base.connection_handler.clear_active_connections!
      end
    end
  end

  @sinatra.set(:api_server, self)

  @routes = {}
  @default_version ||= @versions.last

  # Mount root
  @sinatra.get @root do
    authenticated?(settings.api_server.default_version)

    @api = settings.api_server.describe(Context.new(
                                          settings.api_server,
                                          user: current_user,
                                          params:
                                        ))

    content_type 'text/html'
    erb :index, layout: :main_layout
  end

  @sinatra.options @root do
    setup_formatter
    access_control
    authenticated?(settings.api_server.default_version)
    ret = nil

    ret = case params[:describe]
          when 'versions'
            {
              versions: settings.api_server.versions,
              default: settings.api_server.default_version
            }

          when 'default'
            settings.api_server.describe_version(Context.new(
                                                   settings.api_server,
                                                   version: settings.api_server.default_version,
                                                   user: current_user, params:
                                                 ))

          else
            settings.api_server.describe(Context.new(
                                           settings.api_server,
                                           user: current_user,
                                           params:
                                         ))
          end

    @formatter.format(true, ret)
  end

  # Doc
  @sinatra.get "#{@root}doc" do
    content_type 'text/html'
    erb :main_layout do
      doc(:index)
    end
  end

  @sinatra.get "#{@root}doc/readme" do
    content_type 'text/html'

    erb :main_layout do
      GitHub::Markdown.render(File.new("#{settings.views}/../../../README.md").read)
    end
  end

  @sinatra.get "#{@root}doc/json-schema" do
    content_type 'text/html'
    erb :doc_layout, layout: :main_layout do
      @content = File.read(File.join(settings.root, '../../doc/json-schema.html'))
      @sidebar = erb :'doc_sidebars/json-schema'
    end
  end

  @sinatra.get %r{#{@root}doc/([^\.]+)(\.md)?} do |f, _|
    content_type 'text/html'
    erb :doc_layout, layout: :main_layout do
      begin
        @content = doc(f)
      rescue Errno::ENOENT
        halt 404
      end

      @sidebar = erb :"doc_sidebars/#{f}"
    end
  end

  # Login/logout links
  @sinatra.get "#{root}_login" do
    if current_user
      redirect back
    else
      authenticate!(settings.api_server.default_version) # FIXME
    end
  end

  @sinatra.get "#{root}_logout" do
    require_auth!
  end

  @auth_chain << HaveAPI.default_authenticate if @auth_chain.empty?
  @auth_chain.setup(@versions)

  @extensions.each { |e| e.enabled(self) }

  call_hooks_for(:pre_mount, args: [self, @sinatra])

  # Mount default version first
  mount_version(@root, @default_version)

  @versions.each do |v|
    mount_version(version_prefix(v), v)
  end

  call_hooks_for(:post_mount, args: [self, @sinatra])
end
mount_action(v, route) click to toggle source
# File lib/haveapi/server.rb, line 467
def mount_action(v, route)
  @sinatra.method(route.http_method).call(route.sinatra_path) do
    setup_formatter

    if route.action.auth
      authenticate!(v)
    else
      authenticated?(v)
    end

    begin
      body = request.body.read

      body = if body.empty?
               nil
             else
               JSON.parse(body, symbolize_names: true)
             end
    rescue StandardError => e
      report_error(400, {}, 'Bad JSON syntax')
    end

    action = route.action.new(request, v, params, body, Context.new(
                                                          settings.api_server,
                                                          version: v,
                                                          request: self,
                                                          action: route.action,
                                                          path: route.path,
                                                          params:,
                                                          user: current_user,
                                                          endpoint: true,
                                                          resource_path: route.resource_path
                                                        ))

    unless action.authorized?(current_user)
      report_error(403, {}, 'Access denied. Insufficient permissions.')
    end

    status, reply, errors, http_status = action.safe_exec
    @halted = true

    [
      http_status || 200,
      @formatter.format(
        status,
        status ? reply : nil,
        status ? nil : reply,
        errors,
        version: false
      )
    ]
  end

  @sinatra.options route.sinatra_path do |*args|
    setup_formatter
    access_control
    route_method = route.http_method.to_s.upcase

    pass if params[:method] && params[:method] != route_method

    if route.action.auth
      authenticate!(v)
    else
      authenticated?(v)
    end

    ctx = Context.new(
      settings.api_server,
      version: v,
      request: self,
      action: route.action,
      path: route.path,
      args:,
      params:,
      user: current_user,
      endpoint: true,
      resource_path: route.resource_path
    )

    begin
      desc = route.action.describe(ctx)

      unless desc
        report_error(403, {}, 'Access denied. Insufficient permissions.')
      end
    rescue StandardError => e
      tmp = settings.api_server.call_hooks_for(:description_exception, args: [ctx, e])
      report_error(
        tmp[:http_status] || 500,
        {},
        tmp[:message] || 'Server error occured'
      )
    end

    @formatter.format(true, desc)
  end
end
mount_nested_resource(v, routes) click to toggle source
# File lib/haveapi/server.rb, line 451
def mount_nested_resource(v, routes)
  ret = { resources: {}, actions: {} }

  routes.each do |route|
    if route.is_a?(Hash)
      ret[:resources][route.keys.first] = mount_nested_resource(v, route.values.first)

    else
      ret[:actions][route.action] = route.path
      mount_action(v, route)
    end
  end

  ret
end
mount_resource(prefix, v, resource, hash) click to toggle source
# File lib/haveapi/server.rb, line 434
def mount_resource(prefix, v, resource, hash)
  hash[resource] = { resources: {}, actions: {} }

  resource.routes(prefix).each do |route|
    if route.is_a?(Hash)
      hash[resource][:resources][route.keys.first] = mount_nested_resource(
        v,
        route.values.first
      )

    else
      hash[resource][:actions][route.action] = route.path
      mount_action(v, route)
    end
  end
end
mount_version(prefix, v) click to toggle source
# File lib/haveapi/server.rb, line 374
def mount_version(prefix, v)
  @routes[v] ||= {}
  @routes[v][:resources] = {}

  @sinatra.get prefix do
    authenticated?(v)

    @v = v
    @help = settings.api_server.describe_version(Context.new(
                                                   settings.api_server,
                                                   version: v,
                                                   user: current_user,
                                                   params:
                                                 ))

    content_type 'text/html'
    erb :doc_layout, layout: :main_layout do
      @content = erb :version_page
      @sidebar = erb :version_sidebar
    end
  end

  @sinatra.options prefix do
    setup_formatter
    access_control
    authenticated?(v)

    @formatter.format(true, settings.api_server.describe_version(Context.new(
                                                                   settings.api_server,
                                                                   version: v,
                                                                   user: current_user,
                                                                   params:
                                                                 )))
  end

  # Register blocking resource
  HaveAPI.get_version_resources(@module_name, v).each do |resource|
    mount_resource(prefix, v, resource, @routes[v][:resources])
  end

  if action_state
    mount_resource(
      prefix,
      v,
      HaveAPI::Resources::ActionState,
      @routes[v][:resources]
    )
  end

  validate_resources(@routes[v][:resources])
end
start!() click to toggle source
# File lib/haveapi/server.rb, line 641
def start!
  @sinatra.run!
end
use_version(v, default: false) click to toggle source

Include specific version ‘v` of API.

‘default` is set only when including concrete version. Use {set_default_version} otherwise.

@param v [:all, Array<String>, String]

# File lib/haveapi/server.rb, line 197
def use_version(v, default: false)
  @versions ||= []

  if v == :all
    @versions = HaveAPI.versions(@module_name)
  elsif v.is_a?(Array)
    @versions += v
    @versions.uniq!
  else
    @versions << v
    @default_version = v if default
  end
end
validate_resources(resources) click to toggle source
# File lib/haveapi/server.rb, line 426
def validate_resources(resources)
  resources.each_value do |r|
    r[:actions].each_key(&:validate_build)

    validate_resources(r[:resources])
  end
end
version_prefix(v) click to toggle source
# File lib/haveapi/server.rb, line 607
def version_prefix(v)
  "#{@root}v#{v}/"
end

Private Instance Methods

do_authenticate(v, request) click to toggle source
# File lib/haveapi/server.rb, line 647
def do_authenticate(v, request)
  @auth_chain.authenticate(v, request)
end