joshua 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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