class Joshua

reponse from /api/_/foo

Proxy class for simplified more user friendly render

UserApi.render.login(123, foo: 'bar') -> UserApi.render :login, id: 133, params: { foo: 'bar' }

spec/tests/proxy_spec.rb UserApi.render.login(user: 'foo', pass: 'bar') CompanyApi.render.show(1)

Api response is constructed from this object

Attributes

api[R]

Public Class Methods

after(&block) click to toggle source

block execute after any public method or just some member or collection methods used to add meta tags to response

# File lib/joshua/base_class.rb, line 331
def after &block
  set_callback :after, block
end
after_auto_mount(&blok) click to toggle source
# File lib/joshua/base_class.rb, line 159
def after_auto_mount &blok
  @@after_auto_mount = blok
end
allow(type) click to toggle source
# File lib/joshua/base_class.rb, line 298
def allow type
  if @method_type
    @@opts[:allow] = type
  else
    raise ArgumentError.new('allow can only be set on methods')
  end
end
annotation(name, &block) click to toggle source

define method annotations annotation :unsecure! do

@is_unsecure = true

end unsecure! def login

...
# File lib/joshua/base_class.rb, line 212
def annotation name, &block
  ANNOTATIONS[name] = block
  self.define_singleton_method name do |*args|
    unless @method_type
      error 'Annotation "%s" defined outside the API method blocks (member & collections)' % name
    end

    @@opts[:annotations] ||= {}
    @@opts[:annotations][name] = args
  end
end
api_path() click to toggle source
# File lib/joshua/base_class.rb, line 201
def api_path
  to_s.underscore.sub(/_api$/, '')
end
auto_mount(api_host:, mount_on: nil, bearer: nil, development: false) click to toggle source

ApplicationApi.auto_mount request: request, response: response, mount_on: '/api', development: true auto mount to a root

  • display doc in a root

  • call methods if possible /api/v1.comapny/1/show

# File lib/joshua/base_class.rb, line 40
def auto_mount api_host:, mount_on: nil, bearer: nil, development: false
  request  = api_host.request
  response = api_host.response

  mount_on ||= OPTS[:api][:mount_on] || '/'
  mount_on   = [request.base_url, mount_on].join('') unless mount_on.to_s.include?('//')

  if request.url == mount_on && request.request_method == 'GET'
    response.header['Content-Type'] = 'text/html' if response

    Doc.render request: request, bearer: bearer
  else
    response.header['Content-Type'] = 'application/json' if response

    body     = request.body.read.to_s
    body     = body[0] == '{' ? JSON.parse(body) : nil

    # class: klass, params: params, bearer: bearer, request: request, response: response, development: development
    opts = {}
    opts[:api_host]    = api_host
    opts[:development] = development
    opts[:bearer]      = bearer

    action =
    if body
      # {
      #   "id": 'foo',         # unique ID that will be returned, as required by JSON RPC spec
      #   "class": 'v1/users', # v1/users => V1::UsersApi
      #   "action": 'index',   # "index' or "6/info" or [6, "info"]
      #   "token": 'ab12ef',   # api_token (bearer)
      #   "params": {}         # methos params
      # }
      opts[:params] = body['params'] || {}
      opts[:bearer] = body['token'] if body['token']
      opts[:class]  = body['class']

      body['action']
    else
      opts[:params] = request.params || {}
      opts[:bearer] = opts[:params][:api_token] if opts[:params][:api_token]

      mount_on = mount_on+'/' unless mount_on.end_with?('/')
      path     = request.url.split(mount_on, 2).last.split('?').first.to_s
      parts    = path.split('/')

      @@after_auto_mount.call parts, opts if @@after_auto_mount

      opts[:class] = parts.shift
      parts
    end

    opts[:bearer] ||= request.env['HTTP_AUTHORIZATION'].to_s.split('Bearer ')[1]

    api_response = render action, **opts

    if api_response.is_a?(Hash)
      response.status = api_response[:status] if response
      api_response.to_h
    else
      api_response
    end
  end
end
before(&block) click to toggle source

block execute before any public method or just some member or collection methods

# File lib/joshua/base_class.rb, line 325
def before &block
  set_callback :before, block
end
call(env) click to toggle source

perform auto_mount from a rake call

# File lib/joshua/base_class.rb, line 7
def call env
  request = Rack::Request.new env

  if request.path == '/favicon.ico'
    [
      200,
      { 'Cache-Control'=>'public; max-age=1000000' },
      [Doc.misc_file('favicon.png')]
    ]
  else
    data = auto_mount request: request, development: ENV['RACK_ENV'] == 'development'

    if data.is_a?(Hash)
      [
        data[:status] || 200,
        { 'Content-Type' => 'application/json', 'Cache-Control'=>'private; max-age=0' },
        [data.to_json]
      ]
    else
      data = data.to_s
      [
        200,
        { 'Content-Type' => 'text/html', 'Cache-Control'=>'public; max-age=3600' },
        [data]
      ]
    end
  end
end
collection(&block) click to toggle source

/api/companies/list?countrty_id=1

# File lib/joshua/base_class.rb, line 250
def collection &block
  @method_type = :collection
  class_exec &block
  @method_type = nil
end
Also aliased as: collections
collections(&block)
Alias for: collection
define(name, &block) click to toggle source

aleternative way to define a api function members do

define :foo do
  params {}
  proc {}
end

end

# File lib/joshua/base_class.rb, line 231
def define name, &block
  func = class_exec &block

  if func.is_a?(Proc)
    self.define_method(name, func)
  else
    raise 'Member block has to return a Func object'
  end
end
desc(data) click to toggle source

api method description

# File lib/joshua/base_class.rb, line 279
def desc data
  if @method_type
    @@opts[:desc] = data
  else
    set :opts, :desc, data
  end
end
detail(data) click to toggle source

api method detailed description

# File lib/joshua/base_class.rb, line 288
def detail data
  return if data.to_s == ''

  if @method_type
    @@opts[:detail] = data
  else
    set :opts, :detail, data
  end
end
documented() click to toggle source

if you want to make API DOC public use “documented”

# File lib/joshua/base_class.rb, line 193
def documented
  if self == Joshua
    DOCUMENTED.map(&:to_s).sort.map(&:constantize)
  else
    DOCUMENTED.push self unless DOCUMENTED.include?(self)
  end
end
error(desc) click to toggle source

class errors, raised by params validation

# File lib/joshua/base_class.rb, line 172
def error desc
  raise Joshua::Error, desc
end
error_print(error) click to toggle source
# File lib/joshua/base_class.rb, line 176
def error_print error
  puts
  puts 'Joshua error dump'.red
  puts '---'
  puts '%s: %s' % [error.class, error.message]
  puts '---'
  puts error.backtrace
  puts '---'
end
get(*args) click to toggle source
# File lib/joshua/base_class.rb, line 350
def get *args
  opts.dig *args
end
gettable() click to toggle source

method in available for GET requests as well

# File lib/joshua/base_class.rb, line 307
def gettable
  if @method_type
    @@opts[:gettable] = true
  else
    raise ArgumentError.new('gettable can only be set on methods')
  end
end
icon(data) click to toggle source

api method icon you can find great icons at boxicons.com/ - export to svg

# File lib/joshua/base_class.rb, line 270
def icon data
  if @method_type
    raise ArgumentError.new('Icons cant be added on methods')
  else
    set :opts, :icon, data
  end
end
member(&block) click to toggle source

/api/companies/1/show

# File lib/joshua/base_class.rb, line 242
def member &block
  @method_type = :member
  func = class_exec &block
  @method_type = nil
end
Also aliased as: members
members(&block)
Alias for: member
method_added(name) click to toggle source

here we capture member & collection metods

# File lib/joshua/base_class.rb, line 381
def method_added name
  return if name.to_s.start_with?('_api_')
  return unless @method_type

  set @method_type, name, @@opts

  @@opts = {}

  alias_method "_api_#{@method_type}_#{name}", name
  remove_method name
end
model(name, &block) click to toggle source

propagate to typero

# File lib/joshua/base_class.rb, line 376
def model name, &block
  Typero.schema name, &block
end
mount_on(what) click to toggle source

sets api mount point mount_on '/api'

# File lib/joshua/base_class.rb, line 188
def mount_on what
  OPTS[:api][:mount_on] = what
end
new(action, params: {}) click to toggle source
# File lib/joshua/base_instance.rb, line 27
def initialize action, params: {}, opts: {}, development: false, id: nil, bearer: nil, api_host: nil
  @api = INSTANCE.new

  if action.is_a?(Array)
    # unpack id and action is action is given in path form # [123, :show]
    @api.id, @api.action = action[1] ? action : [nil, action[0]]
  else
    @api.action = action
  end

  @api.bearer        = bearer
  @api.id          ||= id
  @api.action        = @api.action.to_sym
  @api.request       = api_host ? api_host.request : nil
  @api.method_opts   = self.class.opts.dig(@api.id ? :member : :collection, @api.action) || {}
  @api.development   = !!development
  @api.params        = HashWia.new params
  @api.opts          = HashWia.new opts
  @api.api_host      = api_host
  @api.response      = ::Joshua::Response.new @api
end
opts() click to toggle source

dig all options for a current class

# File lib/joshua/base_class.rb, line 355
def opts
  out = {}

  # dig down the ancestors tree till Object class
  ancestors.each do |klass|
    break if klass == Object

    # copy all member and collection method options
    keys = (OPTS[klass.to_s] || {}).keys
    keys.each do |type|
      for k, v in (OPTS.dig(klass.to_s, type) || {})
        out[type] ||= {}
        out[type][k] ||= v
      end
    end
  end

  out
end
params(&block) click to toggle source

params do

name? String
email :email

end

# File lib/joshua/base_class.rb, line 261
def params &block
  raise ArgumentError.new('Block not given for Joshua API method params') unless block_given?

  @@opts[:_typero] = Typero.schema &block
  @@opts[:params] = @@opts[:_typero].to_h
end
plugin(name, &block) click to toggle source

simplified module include, masked as plugin Joshua.plugin :foo do … Joshua.plugin :foo

# File lib/joshua/base_class.rb, line 338
def plugin name, &block
  if block_given?
    # if block given, define a plugin
    PLUGINS[name] = block
  else
    # without a block execute it
    blk = PLUGINS[name]
    raise ArgumentError.new('Plugin :%s not defined' % name) unless blk
    instance_exec &blk
  end
end
render(action=nil, opts={}) click to toggle source

renders api doc or calls api class + action

# File lib/joshua/base_class.rb, line 105
def render action=nil, opts={}
  if action
    return error 'Action not defined' unless action[0]
  else
    return RenderProxy.new self
  end

  api_class =
  if klass = opts.delete(:class)
    # /api/_/foo
    if klass == '_'
      klass = Joshua::DocSpecial.new(opts)

      if klass.respond_to?(action.first)
        return klass.send action.first.to_sym
      else
        return error 'Action %s not defined' % action.first
      end
    end

    klass = klass.split('/') if klass.is_a?(String)
    klass[klass.length-1] += '_api'

    begin
      klass.join('/').classify.constantize
    rescue NameError => e
      return error 'API class "%s" not found' % klass
    end
  else
    self
  end

  api = api_class.new action, **opts
  api.execute_call
rescue => error
  error_print error if opts[:development]
  Response.auto_format error
end
rescue_from(klass, desc=nil, &block) click to toggle source

rescue_from CustomError do … for unhandled rescue_from :all do

api.error 500, 'Error happens'

end define handled error code and description error :not_found, 'Document not found' error 404, 'Document not found' in api methods error 404 error :not_found

# File lib/joshua/base_class.rb, line 155
def rescue_from klass, desc=nil, &block
  RESCUE_FROM[klass] = desc || block
end
response_error(text) click to toggle source

show and render single error in class error format usually when API class not found

# File lib/joshua/base_class.rb, line 165
def response_error text
  out = Response.new nil
  out.error text
  out.render
end
unsafe() click to toggle source

allow methods without @api.bearer token set

# File lib/joshua/base_class.rb, line 316
def unsafe
  if @method_type
    @@opts[:unsafe] = true
  else
    raise ArgumentError.new('Only api methods can be unsafe')
  end
end

Private Class Methods

only_in_api_methods!() click to toggle source
# File lib/joshua/base_class.rb, line 395
def only_in_api_methods!
  raise ArgumentError, "Available only inside collection or member block for API methods." unless @method_type
end
set(*args) click to toggle source

generic opts set set :user_name, :email, :baz

# File lib/joshua/base_class.rb, line 407
def set *args
  name, value   = args.pop(2)
  args.unshift to_s
  pointer = OPTS

  for el in args
    pointer[el] ||= {}
    pointer = pointer[el]
  end

  pointer[name] = value
end
set_callback(name, block) click to toggle source
# File lib/joshua/base_class.rb, line 399
def set_callback name, block
  name = [name, @method_type || :all].join('_').to_sym
  set name, []
  OPTS[to_s][name].push block
end

Public Instance Methods

execute_call() click to toggle source
# File lib/joshua/base_instance.rb, line 49
def execute_call
  if !@api.development && @api.request && @api.request.request_method == 'GET' && !@api.method_opts[:gettable]
    response.error 'GET request is not allowed'
  else
    begin
      parse_api_params
      parse_annotations unless response.error?
      resolve_api_body  unless response.error?
    rescue Joshua::Error => error
      # controlled error raised via error "message", ignore
      response.error error.message
    rescue => error
      # uncontrolled error, should be logged
      Joshua.error_print error if @api.development

      block = RESCUE_FROM[error.class] || RESCUE_FROM[:all]

      if block
        instance_exec error, &block
      else
        response.error error.message, status: 500
      end
    end

    # we execute generic after block in case of error or no
    execute_callback :after_all
  end

  @api.raw || response.render
end
list_errors() click to toggle source
# File lib/doc/doc.rb, line 230
def list_errors
  tag.div do |n|
    n.push name_link :api_errors
    n.push icon ICONS[:error][:image], style: 'position: absolute; margin-left: -40px; margin-top: 1px; fill: #777;'
    n.h4 { 'Named errors' }

    n._box do |n|
      if RESCUE_FROM.keys.length == 0
        n.p 'No named errors defiend via'
        n.code "rescue from :name, 'Error description'"
      end

      n._row({ style: 'margin-bottom: 30px;' }) do |n|
        for key, desc in RESCUE_FROM
          next if key == :all
          next unless key.is_a?(Symbol) && desc.is_a?(String)

          n._col_4 { "<code>#{key}</code>" }
          n._col_8 { desc }
        end
      end
    end
  end
end
modal_dialog() click to toggle source
to_h() click to toggle source
# File lib/joshua/base_instance.rb, line 84
def to_h
  execute_call
end
to_json() click to toggle source
# File lib/joshua/base_instance.rb, line 80
def to_json
  execute_call.to_json
end

Private Instance Methods

api_host(&block) click to toggle source

execute actions on api host

# File lib/joshua/base_instance.rb, line 187
def api_host &block
  if block_given? && @api.api_host
    @api.api_host.instance_exec self, &block
  end

  @api.api_host
end
error(text, args={}) click to toggle source

inline error raise

# File lib/joshua/base_instance.rb, line 158
def error text, args={}
  puts 'JOSHUA API Error: %s (%s)' % [text, caller[0]] if @api.development

  if err = RESCUE_FROM[text]
    if err.is_a?(Proc)
      err.call
      return
    else
      response.error err, args
    end
  else
    response.error text, args
  end

  raise Joshua::Error, text
end
execute_callback(name) click to toggle source
# File lib/joshua/base_instance.rb, line 129
def execute_callback name
  self.class.ancestors.reverse.map(&:to_s).each do |klass|
    if before_list = (OPTS.dig(klass, name.to_sym) || [])
      for before in before_list
        instance_exec response.data, &before
      end
    end
  end
end
message(data) click to toggle source
# File lib/joshua/base_instance.rb, line 175
def message data
  response.message data
end
params() click to toggle source
# File lib/joshua/base_instance.rb, line 153
def params
  @api.params
end
parse_annotations() click to toggle source
# File lib/joshua/base_instance.rb, line 123
def parse_annotations
  for key, opts in (@api.method_opts[:annotations] || {})
    instance_exec *opts, &ANNOTATIONS[key]
  end
end
parse_api_params() click to toggle source
# File lib/joshua/base_instance.rb, line 90
def parse_api_params
  params = @api.method_opts[:params]
  typero = @api.method_opts[:_typero]

  if params && typero
    # add validation errors
    typero.validate @api.params do |name, error|
      response.error_detail name, error
    end
  end
end
resolve_api_body(&block) click to toggle source
# File lib/joshua/base_instance.rb, line 102
def resolve_api_body &block
  # execute before "in the wild"
  # model @api.pbject should be set here
  execute_callback :before_all

  instance_exec &block if block

  # if we have model defiend, we execute member otherwise collection
  type   = @api.id ? :member : :collection

  execute_callback 'before_%s' % type
  api_method = '_api_%s_%s' % [type, @api.action]
  raise Joshua::Error, "Api method #{type}:#{@api.action} not found" unless respond_to?(api_method)

  data = send api_method
  response.data data unless response.data?

  # after blocks
  execute_callback 'after_%s' % type
end
response(content_type=nil) { || ... } click to toggle source
# File lib/joshua/base_instance.rb, line 139
def response content_type=nil
  if block_given?
    @api.raw = yield

    api_host do
      response.header['Content-Type'] = content_type || (@api.raw[0] == '{' ? 'application/json' : 'text/plain')
    end
  elsif content_type
    response.data = content_type
  else
    @api.response
  end
end
super!(name=nil) click to toggle source
# File lib/joshua/base_instance.rb, line 179
def super! name=nil
  type   = @api.id ? :member : :collection
  name ||= caller[0].split('`')[1].sub("'", '')
  name   = "_api_#{type}_#{name}"
  self.class.superclass.instance_method(name).bind(self).call
end