joshua 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.version +1 -0
- data/lib/.DS_Store +0 -0
- data/lib/doc/doc.rb +273 -0
- data/lib/doc/special.rb +20 -0
- data/lib/joshua.rb +39 -0
- data/lib/joshua/base.rb +312 -0
- data/lib/joshua/error.rb +61 -0
- data/lib/joshua/opts.rb +204 -0
- data/lib/joshua/params/define.rb +53 -0
- data/lib/joshua/params/parse.rb +56 -0
- data/lib/joshua/params/types.rb +152 -0
- data/lib/joshua/params/types_errors.rb +33 -0
- data/lib/joshua/response.rb +86 -0
- data/lib/misc/api_example.coffee +75 -0
- data/lib/misc/doc.css +29 -0
- data/lib/misc/doc.js +279 -0
- data/lib/misc/favicon.png +0 -0
- data/lib/misc/ruby_client.rb +52 -0
- metadata +88 -0
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 <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> — %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('"', '"')})">#{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 ' — (%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">×</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
|
data/lib/doc/special.rb
ADDED
@@ -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
|
+
|
data/lib/joshua/base.rb
ADDED
@@ -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
|