Class: Joshua
- Inherits:
-
Object
- Object
- Joshua
- 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
Instance Attribute Summary collapse
-
#api ⇒ Object
readonly
Returns the value of attribute api.
Class Method Summary collapse
-
.after(&block) ⇒ Object
block execute after any public method or just some member or collection methods used to add meta tags to response.
-
.annotation(name, &block) ⇒ Object
define method annotations annotation :unsecure! do @is_unsecure = true end unsecure! def login …
- .api_path ⇒ Object
-
.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.
- .base(what) ⇒ Object
-
.before(&block) ⇒ Object
block execute before any public method or just some member or collection methods.
-
.call(env) ⇒ Object
perform auto_mount from a rake call.
-
.collection(&block) ⇒ Object
/api/companies/list?countrty_id=1.
-
.desc(data) ⇒ Object
api method description.
-
.detail(data) ⇒ Object
api method detailed description.
-
.documented ⇒ Object
if you want to make API DOC public use “documented”.
-
.error(text) ⇒ Object
show and render single error in class error format usually when API class not found.
- .error_print(error) ⇒ Object
- .get(*args) ⇒ Object
-
.gettable ⇒ Object
method in available for GET requests as well.
-
.icon(data) ⇒ Object
api method icon you can find great icons at boxicons.com/ - export to svg.
-
.member(&block) ⇒ Object
/api/companies/1/show.
-
.method_added(name) ⇒ Object
here we capture member & collection metods.
-
.opts ⇒ Object
dig all options for a current class.
-
.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.
-
.plugin(name, &block) ⇒ Object
simplified module include, masked as plugin Joshua.plugin :foo do …
- .render(action, opts = {}) ⇒ Object
-
.rescue_from(klass, desc = nil, &block) ⇒ Object
rescue_from CustomError do …
-
.unsafe ⇒ Object
allow methods without @api.bearer token set.
-
.unsecure ⇒ Object
all api methods are secure (require bearer token).
Instance Method Summary collapse
- #error(desc) ⇒ Object
- #execute_call ⇒ Object
-
#initialize(action, id: nil, bearer: nil, params: {}, opts: {}, request: nil, response: nil, development: false) ⇒ Joshua
constructor
A new instance of Joshua.
- #message(data) ⇒ Object
- #resolve_api_body(&block) ⇒ Object
- #to_h ⇒ Object
- #to_json ⇒ Object
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
#api ⇒ Object (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_path ⇒ Object
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 |
.documented ⇒ Object
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.] 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 |
.gettable ⇒ Object
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 |
.opts ⇒ Object
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
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 |
.unsafe ⇒ Object
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 |
.unsecure ⇒ Object
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
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_call ⇒ Object
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 data response. 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. 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. 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_h ⇒ Object
241 242 243 |
# File 'lib/joshua/base.rb', line 241 def to_h execute_call end |
#to_json ⇒ Object
237 238 239 |
# File 'lib/joshua/base.rb', line 237 def to_json execute_call.to_json end |