Class: Joshua

Inherits:
Object
  • Object
show all
Defined in:
lib/doc/special.rb,
lib/doc/doc.rb,
lib/joshua/base.rb,
lib/joshua/opts.rb,
lib/joshua/error.rb,
lib/joshua/response.rb,
lib/joshua/params/parse.rb,
lib/joshua/params/types.rb,
lib/joshua/params/define.rb,
lib/joshua/params/types_errors.rb

Overview

Api response is constructed from this object

Defined Under Namespace

Modules: Doc, DocSpecial, Params Classes: Error, Response

Constant Summary collapse

OPTS =
{}
PLUGINS =
{}
DOCUMENTED =
[]
RESCUE_FROM =
{}
@@params =
Params::Define.new

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(action, id: nil, bearer: nil, params: {}, opts: {}, request: nil, response: nil, development: false) ⇒ Joshua

Returns a new instance of Joshua.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/joshua/base.rb', line 157

def initialize action, id: nil, bearer: nil, params: {}, opts: {}, request: nil, response: nil, development: false
  @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       = request
  @api.method_opts   = self.class.opts.dig(@api.id ? :member : :collection, @api.action) || {}
  @api.development   = !!development
  @api.rack_response = response
  @api.params        = ::CleanHash::Indifferent.new params
  @api.opts          = ::CleanHash::Indifferent.new opts
  @api.response      = ::Joshua::Response.new @api
end

Instance Attribute Details

#apiObject (readonly)

Returns the value of attribute api.



16
17
18
# File 'lib/joshua/base.rb', line 16

def api
  @api
end

Class Method Details

.after(&block) ⇒ Object

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



143
144
145
# File 'lib/joshua/opts.rb', line 143

def after &block
  set_callback :after, block
end

.annotation(name, &block) ⇒ Object

define method annotations annotation :unsecure! do

@is_unsecure = true

end unsecure! def login

...


34
35
36
37
38
39
# File 'lib/joshua/opts.rb', line 34

def annotation name, &block
  ANNOTATIONS[name] = block
  self.define_singleton_method name do |*args|
    @@params.add_annotation name, args
  end
end

.api_pathObject



23
24
25
# File 'lib/joshua/opts.rb', line 23

def api_path
  to_s.underscore.sub(/_api$/, '')
end

.auto_mount(request:, response: nil, mount_on: nil, bearer: nil, development: false) ⇒ Object

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



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/joshua/base.rb', line 53

def auto_mount request:, response: nil, mount_on: nil, bearer: nil, development: false
  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[:request]     = request
    opts[:response]    = response
    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('/')

      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

.base(what) ⇒ Object



10
11
12
# File 'lib/joshua/opts.rb', line 10

def base what
  set :opts, :base, what
end

.before(&block) ⇒ Object

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



137
138
139
# File 'lib/joshua/opts.rb', line 137

def before &block
  set_callback :before, block
end

.call(env) ⇒ Object

perform auto_mount from a rake call



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/joshua/base.rb', line 20

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, mount_on: '/', development: ENV['RACK_ENV'] == 'development'

    if data.is_a?(Hash)
      [
        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) ⇒ Object

/api/companies/list?countrty_id=1



49
50
51
52
53
# File 'lib/joshua/opts.rb', line 49

def collection &block
  @method_type = :collection
  class_exec &block
  @method_type = nil
end

.desc(data) ⇒ Object

api method description



94
95
96
97
98
99
100
# File 'lib/joshua/opts.rb', line 94

def desc data
  if @method_type
    @@params.add_generic :desc, data
  else
    set :opts, :desc, data
  end
end

.detail(data) ⇒ Object

api method detailed description



103
104
105
106
107
108
109
110
111
# File 'lib/joshua/opts.rb', line 103

def detail data
  return if data.to_s == ''

  if @method_type
    @@params.add_generic :detail, data
  else
    set :opts, :detail, data
  end
end

.documentedObject

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



15
16
17
18
19
20
21
# File 'lib/joshua/opts.rb', line 15

def documented
  if self == Joshua
    DOCUMENTED.map(&:to_s).sort.map(&:constantize)
  else
    DOCUMENTED.push self unless DOCUMENTED.include?(self)
  end
end

.error(text) ⇒ Object

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



25
26
27
28
29
# File 'lib/joshua/error.rb', line 25

def error text
  out = Response.new nil
  out.error text
  out.render
end

.error_print(error) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
# File 'lib/joshua/error.rb', line 31

def error_print error
  return if ENV['RACK_ENV'] == 'test'

  puts
  puts 'Joshua error dump'.red
  puts '---'
  puts '%s: %s' % [error.class, error.message]
  puts '---'
  puts error.backtrace
  puts '---'
end

.get(*args) ⇒ Object



162
163
164
# File 'lib/joshua/opts.rb', line 162

def get *args
  opts.dig *args
end

.gettableObject

method in available for GET requests as well



114
115
116
117
118
119
120
# File 'lib/joshua/opts.rb', line 114

def gettable
  if @method_type
    @@params.add_generic :gettable
  else
    raise ArgumentError.new('gettable can only be set on methods')
  end
end

.icon(data) ⇒ Object

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



85
86
87
88
89
90
91
# File 'lib/joshua/opts.rb', line 85

def icon data
  if @method_type
    raise ArgumentError.new('Icons cant be added on methods')
  else
    set :opts, :icon, data
  end
end

.member(&block) ⇒ Object

/api/companies/1/show



42
43
44
45
46
# File 'lib/joshua/opts.rb', line 42

def member &block
  @method_type = :member
  class_exec &block
  @method_type = nil
end

.method_added(name) ⇒ Object

here we capture member & collection metods



188
189
190
191
192
193
194
195
196
# File 'lib/joshua/opts.rb', line 188

def method_added name
  return if name.to_s.start_with?('_api_')
  return unless @method_type

  set @method_type, name, @@params.fetch_and_clear_opts

  alias_method "_api_#{@method_type}_#{name}", name
  remove_method name
end

.optsObject

dig all options for a current class



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/joshua/opts.rb', line 167

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(*args, &block) ⇒ Object

There are multiple ways to create params params :name, String, req: true params.name!, String params do

name String, required: true
name! String

end params :label do |value, opts|

# validate is value a label, return coarced label
# or raise error with error

end



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/joshua/opts.rb', line 66

def params *args, &block
  if name = args.first
    if block
      # if argument is provided we create a validator
      Params::Parse.define name, &block
    else
      only_in_api_methods!
      @@params.send *args
    end
  elsif block
    @@params.instance_eval &block
  else
    only_in_api_methods!
    @@params
  end
end

.plugin(name, &block) ⇒ Object

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



150
151
152
153
154
155
156
157
158
159
160
# File 'lib/joshua/opts.rb', line 150

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, opts = {}) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/joshua/base.rb', line 112

def render action, opts={}
  return error 'Action not defined' unless action[0]

  api_class =
  if klass = opts.delete(:class)
    # /api/_/foo
    if klass == '_'
      if Joshua::DocSpecial.respond_to?(action.first)
        return Joshua::DocSpecial.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
end

.rescue_from(klass, desc = nil, &block) ⇒ Object

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



19
20
21
# File 'lib/joshua/error.rb', line 19

def rescue_from klass, desc=nil, &block
  RESCUE_FROM[klass] = desc || block
end

.unsafeObject

allow methods without @api.bearer token set



123
124
125
126
127
128
129
# File 'lib/joshua/opts.rb', line 123

def unsafe
  if @method_type
    @@params.add_generic :unsafe
  else
    raise ArgumentError.new('Only api methods can be unsafe')
  end
end

.unsecureObject

all api methods are secure (require bearer token)



132
133
134
# File 'lib/joshua/opts.rb', line 132

def unsecure

end

Instance Method Details

#error(desc) ⇒ Object

Raises:



46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/joshua/error.rb', line 46

def error desc
  if err = RESCUE_FROM[desc]
    if err.is_a?(Proc)
      err.call
    else
      response.error desc, err
      desc = err
    end

    return
  end

  raise Joshua::Error, desc
end

#execute_callObject



183
184
185
186
187
188
189
190
191
192
193
# File 'lib/joshua/base.rb', line 183

def execute_call
  if !@api.development && @api.request && @api.request_method == 'GET' && !@api.method_opts[:gettable]
    response.error 'GET request is not allowed'
  else
    parse_api_params
    parse_annotations unless response.error?
    resolve_api_body unless response.error?
  end

  @api.raw || response.render
end

#message(data) ⇒ Object



179
180
181
# File 'lib/joshua/base.rb', line 179

def message data
  response.message data
end

#resolve_api_body(&block) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/joshua/base.rb', line 195

def resolve_api_body &block
  begin
    # 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
  rescue Joshua::Error => error
    # controlled error raised via error "message", ignore
    response.error error.message
  rescue => error
    Joshua.error_print error

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

    if block
      instance_exec error, &block
    else
      # uncontrolled error, should be logged
      # search to response[:code] 500 in after block
      response.error error.message
      response.error :class, error.class.to_s
      response.error :code, 500
    end
  end

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

#to_hObject



241
242
243
# File 'lib/joshua/base.rb', line 241

def to_h
  execute_call
end

#to_jsonObject



237
238
239
# File 'lib/joshua/base.rb', line 237

def to_json
  execute_call.to_json
end