joshua 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3681a7c08c75032f3f6c574645e6a8375e5613d64c729a90119d3bc1c6d3e5be
4
+ data.tar.gz: 4cf6aeac3d58573b649f545ccdfae95f905c393ec476364989e8fb1fa0c38595
5
+ SHA512:
6
+ metadata.gz: 9791cbbf644061bf19821093e7f91367466df6e77f66b8a0ef414f2cd5b5b0b4fa9a0c36125a64b3c874cf55e791a920ce8381a55b6bbc58b6a38e0aed791a1b
7
+ data.tar.gz: 3cf4e524aa396ee9c017c953f6eea4efc478df31160e1f96e92602a73ef9710cc896a297a1e7a6c3b77cbe2979571d51040471eef4e2a64bef47c1c5fb05dbed
data/.version ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/.DS_Store ADDED
Binary file
data/lib/doc/doc.rb ADDED
@@ -0,0 +1,273 @@
1
+ class Joshua
2
+ module Doc
3
+ extend self
4
+
5
+ ICONS = {
6
+ github: {
7
+ url: 'https://github.com/dux/joshua',
8
+ image: '<path d="M11.999 1.271C5.925 1.271 1 6.196 1 12.273c0 4.859 3.152 8.982 7.523 10.437.55.1.751-.239.751-.53l-.015-1.872c-3.06.666-3.706-1.474-3.706-1.474-.5-1.271-1.221-1.609-1.221-1.609-.999-.683.075-.668.075-.668 1.105.077 1.685 1.133 1.685 1.133.981 1.681 2.575 1.196 3.202.914.1-.711.384-1.196.698-1.471-2.442-.277-5.011-1.221-5.011-5.436 0-1.201.429-2.183 1.133-2.952-.114-.278-.491-1.397.108-2.911 0 0 .923-.296 3.025 1.127A10.56 10.56 0 0 1 12 6.591c.935.004 1.876.127 2.754.37 2.1-1.423 3.022-1.127 3.022-1.127.6 1.514.223 2.633.11 2.911.705.769 1.131 1.751 1.131 2.952 0 4.225-2.573 5.155-5.023 5.427.395.34.747 1.011.747 2.038 0 1.471-.014 2.657-.014 3.018 0 .293.199.636.756.528C19.851 21.251 23 17.13 23 12.273c0-6.077-4.926-11.002-11.001-11.002z"></path>',
9
+ },
10
+ twitter: {
11
+ url: 'https://twitter.com/@dux',
12
+ image: '<path d="M22.208 3.871c-.757.252-1.824.496-2.834.748-.757-.883-1.892-1.388-3.154-1.388-2.902 0-4.92 2.649-4.289 5.425-3.659-.126-6.939-1.892-9.083-4.542-1.135 1.892-.505 4.542 1.388 5.803-.757 0-1.388-.126-2.019-.505 0 2.145 1.388 4.037 3.532 4.416-.631.252-1.388.252-2.019.126.505 1.766 2.145 3.028 4.037 3.028-1.892 1.388-4.289 2.019-6.56 1.766 1.892 1.262 4.163 2.019 6.686 2.019 8.2 0 12.742-6.813 12.49-12.994.753-1.089 1.49-2.201 1.824-3.902z"></path>',
13
+ },
14
+ email: {
15
+ url: 'mailto:reic.dino@gmail.com',
16
+ image: '<path d="M22.22 9.787c0-5.13-4.062-8.307-8.983-8.307-6.666 0-11.457 4.948-11.457 11.431 0 6.042 4.609 9.609 9.999 9.609 1.64 0 3.749-.391 5.234-1.12l.364-2.031c-1.484.729-3.645 1.224-5.442 1.224-4.661 0-7.968-2.968-7.968-7.682 0-5.025 3.619-9.478 9.14-9.478 3.854 0 7.004 2.318 7.004 6.354 0 1.562-.39 3.671-1.588 4.843-.521.521-1.068.885-1.849.885-.599 0-1.015-.312-1.015-1.015 0-.235.052-.495.104-.729l1.614-6.458h-1.745l-.65 1.094c-.521-.938-1.615-1.381-2.63-1.381-3.386 0-5.208 3.151-5.208 6.25 0 1.198.416 2.291 1.197 3.047.599.598 1.485 1.041 2.5 1.041 1.328 0 2.422-.443 3.307-1.458.209.729 1.042 1.458 2.292 1.458 1.64 0 2.578-.625 3.541-1.562 1.536-1.484 2.239-3.828 2.239-6.015zm-7.916 1.276c0 1.77-.755 4.426-2.916 4.426-1.458 0-2.057-1.067-2.057-2.395 0-1.094.365-2.474 1.172-3.385.442-.495 1.015-.886 1.718-.886 1.406 0 2.083.886 2.083 2.24z"></path>',
17
+ },
18
+ error: {
19
+ image: '<path d="M3,4v12c0,1.103,0.897,2,2,2h3.5l3.5,4l3.5-4H19c1.103,0,2-0.897,2-2V4c0-1.103-0.897-2-2-2H5C3.897,2,3,2.897,3,4z M11,5 h2v6h-2V5z M11,13h2v2h-2V13z" />'
20
+ }
21
+ }
22
+
23
+ def tag
24
+ HtmlTagBuilder
25
+ end
26
+
27
+ def misc_file name
28
+ File.read [__dir__, '../misc/%s' % name].join('/')
29
+ end
30
+
31
+ # render full page
32
+ def render mount_on: nil, request: nil, bearer: nil
33
+ mount_on ||= request.url.split('?').first+'/'
34
+ mount_on.sub! %r{//$}, '/'
35
+
36
+ tag.html do |n|
37
+ n.head do |n|
38
+ n.title 'Joshua Tester'
39
+ n.link({ href: "https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700,800,900&display=swap", rel:"stylesheet" })
40
+ n.link({ rel:"stylesheet", href:"https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" })
41
+ n.script({ src: 'https://cdnjs.cloudflare.com/ajax/libs/zepto/1.2.0/zepto.min.js' })
42
+ n.script %[window.api_opts = { mount_on: '#{mount_on}', bearer: '#{bearer}' }]
43
+ end
44
+ n.body do |n|
45
+ n.style { misc_file('doc.css') }
46
+ n.header({ style: 'border-bottom: 1px solid rgb(228, 228, 228);'}) do |n|
47
+ n._container do |n|
48
+ n.push top_icons
49
+ n.push %[<button id="bearer_button" onclick="AuthButton.set()" class="btn btn-sm btn-outline-primary" style="float: right; margin-top: 15px; margin-right: 20px;">-</button>]
50
+ n.h1({ class: :nav}) { %[<a href="#top">Joshua &nbsp; <gray>Docs</gray></a>] }
51
+ end
52
+ end
53
+
54
+ n.push modal_dialog
55
+
56
+ n._container do |n|
57
+ n._row do |n|
58
+ n._col_3 do |n|
59
+ n._sticky(style: 'padding-top: 30px;') do |n|
60
+ n.a({ class: :dark, href: '#top' }) { '<p><b>API OBJECTS</b></p>' }
61
+ n.push left_nav
62
+
63
+ n.br
64
+ n.br
65
+
66
+ n.p '<b>TOOLS</b>'
67
+ n.div do |n|
68
+ n.push %[<p><a class="badge badge-light" href="#api_errors">Named errors</a></p>]
69
+ n.push %[<p><a class="badge badge-light" href="#{mount_on}_/postman" target="capi_postman">Postman import URL</a></p>]
70
+ n.push %[<p><a class="badge badge-light" href="#{mount_on}_/raw" target="capi_raw">Raw doc data</a></p>]
71
+ end
72
+
73
+ n.br
74
+
75
+ n.p '<b>API LIBRARIES</b>'
76
+ n.div do |n|
77
+ n.push %[<a class="badge badge-light" href="https://github.com/dux/joshua/blob/master/lib/misc/ruby_client.rb" target="capi_ruby">Ruby</a>]
78
+ n.push %[<a class="badge badge-light" href="https://github.com/dux/joshua/blob/master/lib/misc/api_example.coffee" target="capi_js">Javascript</a>]
79
+ n.push %[<a class="badge badge-light" href="#">Python</a>]
80
+ n.push %[<a class="badge badge-light" href="#">C#</a>]
81
+ end
82
+
83
+ n.br
84
+
85
+ n.p '<b>RESOURCES</b>'
86
+ n.div do |n|
87
+ n.push %[<a class="badge badge-light" href="http://vmrcre.org/web/scribe/home/-/blogs/why-rest-sucks" target="capi_why">Why we only prefer POST?</a>]
88
+ end
89
+ end
90
+ end
91
+
92
+ n._col_9 do |n|
93
+ n.push index
94
+ n.push list_errors
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ # anchor link
103
+ def name_link name, top=nil
104
+ %[<a name="#{name}" class="anchor" style="top: #{top || -95}px;"></a>]
105
+ end
106
+
107
+ # render single icon
108
+ def icon data, size: 24, color: nil, style: nil
109
+ %[<svg style="width: #{size}px; height: #{size}px; #{style}" viewBox="0 0 24 24" fill="currentColor">#{data}</svg>]
110
+ end
111
+
112
+ # top side navigation icons
113
+ def top_icons
114
+ tag.div({ style: 'float: right; margin-top: 18px;' }) do |n|
115
+ for icon in ICONS.values
116
+ next unless icon[:url]
117
+ n.push %[<a target="_new" href="#{icon[:url]}">#{icon icon[:image]}</a>]
118
+ end
119
+ end
120
+ end
121
+
122
+ # left side navigation
123
+ def left_nav
124
+ tag.div do |n|
125
+ Joshua.documented.each do |name|
126
+ n.a({ class:'btn btn-outline-info btn-sm', style: '-font-size: 14px; margin-bottom: 10px;', href: '#%s' % name}) do |n|
127
+ icon = name.opts.dig(:opts, :icon)
128
+ n.push self.icon icon, size: 20 if icon
129
+ n.push name.to_s.sub(/Api$/, '')
130
+ end
131
+
132
+ n.br
133
+ end
134
+ end
135
+ end
136
+
137
+ # render doc for all documented classes
138
+ def index
139
+ tag.div do |n|
140
+ for @klass in Joshua.documented
141
+ @opts = @klass.opts
142
+ icon = @opts.dig(:opts, :icon)
143
+
144
+ n._sticky(style: 'background: #f7f7f7; padding-bottom: 5px; padding-top: 30px; margin-top: 2px;') do |n|
145
+ n.push name_link @klass, 40
146
+ n.push self.icon icon, style: 'position: absolute; margin-left: -40px; margin-top: 1px; fill: #777; background: #f7f7f7;' if icon
147
+ n.h4 { @klass.to_s.sub(/Api$/, '') }
148
+ end
149
+
150
+ if desc = @opts.dig(:opts, :desc)
151
+ n.p { desc }
152
+ end
153
+
154
+ if detail = @opts.dig(:opts, :detail)
155
+ n.p { detail }
156
+ end
157
+
158
+ n.push render_type :member
159
+ n.push render_type :collection
160
+
161
+ n.br
162
+ n.hr
163
+ n.br
164
+ end
165
+ end
166
+ end
167
+
168
+ # render members or collection
169
+ def render_type name
170
+ base = @opts[name] || return
171
+
172
+ tag.div do |n|
173
+ n.br
174
+ n.h5 '<gray>%s methods</gray>' % name
175
+
176
+ for m_name, member in base
177
+ n.div do |n|
178
+ n.push render_method name: name, m_name: m_name, opts: member
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ # render api method
185
+ def render_method name:, m_name:, opts:
186
+ tag._box do |n|
187
+ # n.push %[<button onclick="" class="btn btn-info btn-sm request">request</button>]
188
+ anchor = [@klass, m_name].join('-')
189
+
190
+ n.push name_link anchor
191
+ n.h5 do |n|
192
+ n.push "<a href='##{anchor}'>#{m_name}</a>"
193
+ n.push ' <gray>&nbsp; &mdash; &nbsp; %s</gray>' % opts[:desc] if opts[:desc]
194
+ end
195
+
196
+ n.p({style: 'margin: 20px 0 25px 0;'}) do |n|
197
+ path = @klass.api_path
198
+ path += '/:id' if name == :member
199
+ path += "/#{m_name}"
200
+ n.push %[<button href="#{path}" class="btn btn-outline-info btn-sm" onclick="ModalForm.render(api_opts.mount_on+this.innerHTML, #{(opts[:params] || {}).to_json.gsub('"', '&quot;')})">#{path}</button>]
201
+ end
202
+
203
+ if opts[:detail]
204
+ n.h6 'Details'
205
+ n.pre opts[:detail]
206
+ end
207
+
208
+ if mopts = opts[:params]
209
+ n.h6 'Params'
210
+ n.ul do |n|
211
+ for name, opt in mopts
212
+ n.li do |n|
213
+ n.push '<bold>%s</bold>: ' % name
214
+ n.push opt[:type]
215
+
216
+ data = []
217
+ data.push 'required' if opt[:required]
218
+ data.push 'default: %s' % opt[:default].to_s unless opt[:default].nil?
219
+ n.push ' &mdash; (%s)' % data.join(', ') if data.length > 0
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ def list_errors
228
+ tag.div do |n|
229
+ n.push name_link :api_errors
230
+ n.push icon ICONS[:error][:image], style: 'position: absolute; margin-left: -40px; margin-top: 1px; fill: #777;'
231
+ n.h4 { 'Named errors' }
232
+
233
+ n._box do |n|
234
+ if RESCUE_FROM.keys.length == 0
235
+ n.p 'No named errors defiend via'
236
+ n.code "rescue from :name, 'Error description'"
237
+ end
238
+
239
+ n._row({ style: 'margin-bottom: 30px;' }) do |n|
240
+ for key, desc in RESCUE_FROM
241
+ next if key == :all
242
+ next unless key.is_a?(Symbol) && desc.is_a?(String)
243
+
244
+ n._col_4 { "<code>#{key}</code>" }
245
+ n._col_8 { desc }
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ def modal_dialog
253
+ %[
254
+ <script>#{misc_file('doc.js')}</script>
255
+ <div id="modal" class="modal" tabindex="-1" role="dialog">
256
+ <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(99,99,99,0.3)"></div>
257
+ <div class="modal-dialog modal-lg" role="document">
258
+ <div class="modal-content">
259
+ <div class="modal-header">
260
+ <h5 class="modal-title"></h5>
261
+ <button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="Modal.close()">
262
+ <span aria-hidden="true">&times;</span>
263
+ </button>
264
+ </div>
265
+ <div class="modal-body">
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ ]
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,20 @@
1
+ # reponse from /api/_/foo
2
+
3
+ class Joshua
4
+ module DocSpecial
5
+ extend self
6
+
7
+ def postman
8
+ raw
9
+ end
10
+
11
+ def raw
12
+ unwanted = %w(all member collection)
13
+ {}.tap do |doc|
14
+ for el in Joshua.documented
15
+ doc[el.to_s.sub(/Api$/, '').tableize] = el.opts.filter { |k, _| !unwanted.include?(k.to_s.split('_')[1]) }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/joshua.rb ADDED
@@ -0,0 +1,39 @@
1
+ unless ''.respond_to?(:dasherize)
2
+ require 'dry/inflector'
3
+
4
+ class String
5
+ %w(
6
+ classify
7
+ constantize
8
+ dasherize
9
+ ordinalize
10
+ pluralize
11
+ singularize
12
+ tableize
13
+ underscore
14
+ ).each do |name|
15
+ define_method name do
16
+ Dry::Inflector.new.send(name, self)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ require 'json'
23
+ require 'html-tag'
24
+ require 'clean-hash'
25
+
26
+ require_relative './joshua/params/define'
27
+ require_relative './joshua/params/parse'
28
+ require_relative './joshua/params/types'
29
+ require_relative './joshua/params/types_errors'
30
+ require_relative './joshua/opts'
31
+ require_relative './joshua/base'
32
+ require_relative './joshua/error'
33
+ require_relative './joshua/response'
34
+
35
+ require_relative './doc/doc'
36
+ require_relative './doc/special'
37
+
38
+
39
+
@@ -0,0 +1,312 @@
1
+ class Joshua
2
+ INSTANCE ||= Struct.new 'JoshuaOpts',
3
+ :action,
4
+ :bearer,
5
+ :development,
6
+ :id,
7
+ :method_opts,
8
+ :opts,
9
+ :params,
10
+ :raw,
11
+ :rack_response,
12
+ :request,
13
+ :response,
14
+ :uid
15
+
16
+ attr_reader :api
17
+
18
+ class << self
19
+ # here we capture member & collection metods
20
+ def method_added name
21
+ return if name.to_s.start_with?('_api_')
22
+ return unless @method_type
23
+
24
+ set @method_type, name, PARAMS.fetch_and_clear_opts
25
+
26
+ alias_method "_api_#{@method_type}_#{name}", name
27
+ remove_method name
28
+ end
29
+
30
+ # perform auto_mount from a rake call
31
+ def call env
32
+ request = Rack::Request.new env
33
+
34
+ if request.path == '/favicon.ico'
35
+ [
36
+ 200,
37
+ { 'Cache-Control'=>'public; max-age=1000000' },
38
+ [Doc.misc_file('favicon.png')]
39
+ ]
40
+ else
41
+ data = auto_mount request: request, mount_on: '/', development: ENV['RACK_ENV'] == 'development'
42
+
43
+ if data.is_a?(Hash)
44
+ [
45
+ 200,
46
+ { 'Content-Type' => 'application/json', 'Cache-Control'=>'private; max-age=0' },
47
+ [data.to_json]
48
+ ]
49
+ else
50
+ data = data.to_s
51
+ [
52
+ 200,
53
+ { 'Content-Type' => 'text/html', 'Cache-Control'=>'public; max-age=3600' },
54
+ [data]
55
+ ]
56
+ end
57
+ end
58
+ end
59
+
60
+ # ApplicationApi.auto_mount request: request, response: response, mount_on: '/api', development: true
61
+ # auto mount to a root
62
+ # * display doc in a root
63
+ # * call methods if possible /api/v1.comapny/1/show
64
+ def auto_mount request:, response: nil, mount_on: nil, bearer: nil, development: false
65
+ mount_on = [request.base_url, mount_on].join('') unless mount_on.to_s.include?('//')
66
+
67
+ if request.url == mount_on && request.request_method == 'GET'
68
+ response.header['Content-Type'] = 'text/html' if response
69
+
70
+ Doc.render request: request, bearer: bearer
71
+ else
72
+ response.header['Content-Type'] = 'application/json' if response
73
+
74
+ body = request.body.read.to_s
75
+ body = body[0] == '{' ? JSON.parse(body) : nil
76
+
77
+ # class: klass, params: params, bearer: bearer, request: request, response: response, development: development
78
+ opts = {}
79
+ opts[:request] = request
80
+ opts[:response] = response
81
+ opts[:development] = development
82
+ opts[:bearer] = bearer
83
+
84
+ action =
85
+ if body
86
+ # {
87
+ # "id": 'foo', # unique ID that will be returned, as required by JSON RPC spec
88
+ # "class": 'v1/users', # v1/users => V1::UsersApi
89
+ # "action": 'index', # "index' or "6/info" or [6, "info"]
90
+ # "token": 'ab12ef', # api_token (bearer)
91
+ # "params": {} # methos params
92
+ # }
93
+ opts[:params] = body['params'] || {}
94
+ opts[:bearer] = body['token'] if body['token']
95
+ opts[:class] = body['class']
96
+
97
+ body['action']
98
+ else
99
+ opts[:params] = request.params || {}
100
+ opts[:bearer] = opts[:params][:api_token] if opts[:params][:api_token]
101
+
102
+ mount_on = mount_on+'/' unless mount_on.end_with?('/')
103
+ path = request.url.split(mount_on, 2).last.split('?').first.to_s
104
+ parts = path.split('/')
105
+
106
+ opts[:class] = parts.shift
107
+ parts
108
+ end
109
+
110
+ opts[:bearer] ||= request.env['HTTP_AUTHORIZATION'].to_s.split('Bearer ')[1]
111
+
112
+ api_response = render action, **opts
113
+
114
+ if api_response.is_a?(Hash)
115
+ response.status = api_response[:status] if response
116
+ api_response.to_h
117
+ else
118
+ api_response
119
+ end
120
+ end
121
+ end
122
+
123
+ def render action, opts={}
124
+ return error 'Action not defined' unless action[0]
125
+
126
+ api_class =
127
+ if klass = opts.delete(:class)
128
+ # /api/_/foo
129
+ if klass == '_'
130
+ if Joshua::DocSpecial.respond_to?(action.first)
131
+ return Joshua::DocSpecial.send action.first.to_sym
132
+ else
133
+ return error 'Action %s not defined' % action.first
134
+ end
135
+ end
136
+
137
+ klass = klass.split('/') if klass.is_a?(String)
138
+ klass[klass.length-1] += '_api'
139
+
140
+ begin
141
+ klass.join('/').classify.constantize
142
+ rescue NameError => e
143
+ return error 'API class "%s" not found' % klass
144
+ end
145
+ else
146
+ self
147
+ end
148
+
149
+ api = api_class.new action, **opts
150
+ api.execute_call
151
+ end
152
+
153
+ private
154
+
155
+ def only_in_api_methods!
156
+ raise ArgumentError, "Available only inside collection or member block for API methods." unless @method_type
157
+ end
158
+
159
+ def set_callback name, block
160
+ name = [name, @method_type || :all].join('_').to_sym
161
+ set name, []
162
+ OPTS[to_s][name].push block
163
+ end
164
+ end
165
+
166
+ ###
167
+
168
+ def initialize action, id: nil, bearer: nil, params: {}, opts: {}, request: nil, response: nil, development: false
169
+ @api = INSTANCE.new
170
+
171
+ if action.is_a?(Array)
172
+ # unpack id and action is action is given in path form # [123, :show]
173
+ @api.id, @api.action = action[1] ? action : [nil, action[0]]
174
+ else
175
+ @api.action = action
176
+ end
177
+
178
+ @api.bearer = bearer
179
+ @api.id ||= id
180
+ @api.action = @api.action.to_sym
181
+ @api.request = request
182
+ @api.method_opts = self.class.opts.dig(@api.id ? :member : :collection, @api.action) || {}
183
+ @api.development = !!development
184
+ @api.rack_response = response
185
+ @api.params = ::CleanHash::Indifferent.new params
186
+ @api.opts = ::CleanHash::Indifferent.new opts
187
+ @api.response = ::Joshua::Response.new @api
188
+ end
189
+
190
+ def message data
191
+ response.message data
192
+ end
193
+
194
+ def execute_call
195
+ if !@api.development && @api.request && @api.request_method == 'GET' && !@api.method_opts[:gettable]
196
+ response.error 'GET request is not allowed'
197
+ else
198
+ parse_api_params
199
+ parse_annotations unless response.error?
200
+ resolve_api_body unless response.error?
201
+ end
202
+
203
+ @api.raw || response.render
204
+ end
205
+
206
+ def resolve_api_body &block
207
+ begin
208
+ # execute before "in the wild"
209
+ # model @api.pbject should be set here
210
+ execute_callback :before_all
211
+
212
+ instance_exec &block if block
213
+
214
+ # if we have model defiend, we execute member otherwise collection
215
+ type = @api.id ? :member : :collection
216
+
217
+ execute_callback 'before_%s' % type
218
+ api_method = '_api_%s_%s' % [type, @api.action]
219
+ raise Joshua::Error, "Api method #{type}:#{@api.action} not found" unless respond_to?(api_method)
220
+ data = send api_method
221
+ response.data data unless response.data?
222
+
223
+ # after blocks
224
+ execute_callback 'after_%s' % type
225
+ rescue Joshua::Error => error
226
+ # controlled error raised via error "message", ignore
227
+ response.error error.message
228
+ rescue => error
229
+ Joshua.error_print error
230
+
231
+ block = RESCUE_FROM[error.class] || RESCUE_FROM[:all]
232
+
233
+ if block
234
+ instance_exec error, &block
235
+ else
236
+ # uncontrolled error, should be logged
237
+ # search to response[:code] 500 in after block
238
+ response.error error.message
239
+ response.error :class, error.class.to_s
240
+ response.error :code, 500
241
+ end
242
+ end
243
+
244
+ # we execute generic after block in case of error or no
245
+ execute_callback :after_all
246
+ end
247
+
248
+ def to_json
249
+ execute_call.to_json
250
+ end
251
+
252
+ def to_h
253
+ execute_call
254
+ end
255
+
256
+ private
257
+
258
+ def parse_api_params
259
+ return unless @api.method_opts[:params]
260
+
261
+ parse = Joshua::Params::Parse.new
262
+
263
+ for name, opts in @api.method_opts[:params]
264
+ # enforce required
265
+ if opts[:required] && @api.params[name].to_s == ''
266
+ response.error_detail name, 'Argument missing'
267
+ next
268
+ end
269
+
270
+ begin
271
+ # check and coerce value
272
+ @api.params[name] = parse.check opts[:type], @api.params[name], opts
273
+ rescue Joshua::Error => error
274
+ # add to details if error found
275
+ response.error_detail name, error.message
276
+ end
277
+ end
278
+ end
279
+
280
+ def parse_annotations
281
+ for key, opts in (@api.method_opts[:annotations] || {})
282
+ instance_exec *opts, &ANNOTATIONS[key]
283
+ end
284
+ end
285
+
286
+ def execute_callback name
287
+ self.class.ancestors.reverse.map(&:to_s).each do |klass|
288
+ if before_list = (OPTS.dig(klass, name.to_sym) || [])
289
+ for before in before_list
290
+ instance_exec &before
291
+ end
292
+ end
293
+ end
294
+ end
295
+
296
+ def response content_type=nil
297
+ if block_given?
298
+ @api.raw = yield
299
+
300
+ if @api.rack_response
301
+ @api.rack_response.header['Content-Type'] = content_type || (@api.raw[0] == '{' ? 'application/json' : 'text/plain')
302
+ end
303
+ else
304
+ @api.response
305
+ end
306
+ end
307
+
308
+ def params
309
+ @api.params
310
+ end
311
+
312
+ end