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
Public Class Methods
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
# File lib/joshua/base_class.rb, line 159 def after_auto_mount &blok @@after_auto_mount = blok end
# 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
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
# File lib/joshua/base_class.rb, line 201 def api_path to_s.underscore.sub(/_api$/, '') end
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
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
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
/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
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
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
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
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
class errors, raised by params validation
# File lib/joshua/base_class.rb, line 172 def error desc raise Joshua::Error, desc end
# 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
# File lib/joshua/base_class.rb, line 350 def get *args opts.dig *args end
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
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
/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
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
propagate to typero
# File lib/joshua/base_class.rb, line 376 def model name, &block Typero.schema name, &block end
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
# 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
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 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
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
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
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
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
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
# 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
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
# 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
# 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
# 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
# File lib/doc/doc.rb, line 255 def modal_dialog %[ <script>#{misc_file('doc.js')}</script> <div id="modal" class="modal" tabindex="-1" role="dialog"> <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(99,99,99,0.3)"></div> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title"></h5> <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="Modal.close()"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> </div> </div> </div> </div> ] end
# File lib/joshua/base_instance.rb, line 84 def to_h execute_call end
# File lib/joshua/base_instance.rb, line 80 def to_json execute_call.to_json end
Private Instance Methods
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
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
# 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
# File lib/joshua/base_instance.rb, line 175 def message data response.message data end
# File lib/joshua/base_instance.rb, line 153 def params @api.params end
# 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
# 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
# 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
# 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
# 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