joshua 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,33 @@
1
+ class Joshua
2
+ module Params
3
+ class Parse
4
+ ERRORS = {
5
+ en:{
6
+ bad_format: 'Bad value format',
7
+ not_integer: 'Not an integer',
8
+ min_value: 'Minimal allowed value is: %s',
9
+ max_value: 'Maximal allowed value is: %s',
10
+ email_min: 'Email requireds min of 8 characters',
11
+ email_missing: 'Email is missing @',
12
+ url_start: 'URL is not starting with http or https',
13
+ point_format: 'Point should be in format 1.2345678,1.2345678',
14
+ min_date: 'Minimal allow date is %s',
15
+ max_date: 'Maximal allow date is %s'
16
+ },
17
+
18
+ hr: {
19
+ bad_format: 'Format vrijednosti ne zadovoljava',
20
+ not_integer: 'Nije cijeli broj',
21
+ min_value: 'Minimalna dozvoljena vrijednost je: %s',
22
+ max_value: 'Maksimalna dozvoljena vrijednost je: %s',
23
+ email_min: 'Email zatjeva minimalno 8 znakova',
24
+ email_missing: 'U email-u nedostaje @',
25
+ url_start: 'URL ne započinje sa http ili https',
26
+ point_format: 'Geo točka bi trebala biti u formatu 1.2345678,1.2345678',
27
+ min_date: 'Minimalni dozvoljeni datum je %s',
28
+ max_date: 'Maksimalni dozvoljeni datum je %s'
29
+ }
30
+ }
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,86 @@
1
+ # Api response is constructed from this object
2
+
3
+ class Joshua
4
+ class Response
5
+ def initialize api
6
+ @api = api
7
+ @out = {}
8
+ @meta = {}
9
+ @errors = {}
10
+ end
11
+
12
+ def []= key, value
13
+ meta key, value
14
+ end
15
+
16
+ # forward header to rack_response.header
17
+ def header *args
18
+ if args.first
19
+ @api.rack_response.header[args.first] = args[1] if @api.rack_response
20
+ else
21
+ @api.rack_response.header
22
+ end
23
+ end
24
+
25
+ # human readable response message
26
+ def message value
27
+ @message = value
28
+ end
29
+
30
+ # api meta response, any data is allowed
31
+ def meta key, value = nil
32
+ if value
33
+ @meta[key] = value
34
+ else
35
+ @meta[key]
36
+ end
37
+ end
38
+
39
+ # add api response error
40
+ def error *args
41
+ return @errors unless args[0]
42
+
43
+ desc, code = args.reverse
44
+
45
+ @errors[:code] = code if code
46
+ @errors[:messages] ||= []
47
+ @errors[:messages].push desc unless @errors[:messages].include?(desc)
48
+ end
49
+
50
+ def error?
51
+ !!(@errors[:messages] || @errors[:details])
52
+ end
53
+
54
+ def error_detail name, desc
55
+ error '%s (%s)' % [desc, name]
56
+
57
+ @errors[:details] ||= {}
58
+ @errors[:details][name] = desc
59
+ end
60
+
61
+ def data value
62
+ @data ||= value
63
+ end
64
+
65
+ def data?
66
+ !@data.nil?
67
+ end
68
+
69
+ # render full api response
70
+ def render
71
+ {}.tap do |out|
72
+ if @errors.keys.empty?
73
+ out[:success] = true
74
+ else
75
+ out[:success] = false
76
+ out[:error] = @errors
77
+ end
78
+
79
+ out[:meta] = @meta
80
+ out[:message] = @message if @message
81
+ out[:data] = @data unless @data.nil?
82
+ out[:status] = error? ? 400 : 200
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,75 @@
1
+ # Coffee code example for chainable CleaApi access that is easy to read
2
+ # fell free to compile to JS here https://coffeescript.org/#try
3
+ #
4
+ # just execute action and trigger Info.api() buy default
5
+ # Api(path, opts)
6
+ #
7
+ # silent execute
8
+ # Api(path, opts).silent()
9
+ #
10
+ # trigger info and custom function on success
11
+ # Api(path, opts).done(function() { ... }) - info + done
12
+ #
13
+ # silent and redirect on sucess
14
+ # Api(path, opts).silent().redirect(path) - info and redirect on success
15
+ #
16
+ # silent on success, custom error func
17
+ # Api(path, opts).silent().error(error_func)
18
+ #
19
+ # real life example: update product, on success to not show info, just redirect to product page
20
+ # Api('product/123/update', {name: 'Foo', description: 'Bar', value: 5}).silent().follow()
21
+
22
+ window.Api = (path, opts={}) ->
23
+ if typeof(path) != 'string'
24
+ form = $ path
25
+ path = form.attr 'action'
26
+ opts = form.serializeHash()
27
+
28
+ path = "/api/#{path}" unless path.indexOf('/api/') == 0
29
+ request = new RequestBase()
30
+
31
+ $.ajax
32
+ type: 'POST'
33
+ url: path,
34
+ data: opts,
35
+ complete: request.complete
36
+
37
+ request
38
+
39
+ class RequestBase
40
+ refresh: (node) => @do_refresh = node || true; @
41
+ reload: => @do_reload = true; @
42
+ follow: (func) => @do_follow = true; @
43
+ error: (func) => @error_func = func; @
44
+ done: (func) => @done_func = func; @
45
+ redirect: (path) => @do_redirect = path; @
46
+ silent: (info) => @is_silent = [info]; @
47
+ complete: (data, http_status) =>
48
+ response = JSON.parse(data.responseText)
49
+
50
+ if response.error
51
+ if request.error_func
52
+ request.error_func(response.error)
53
+ else
54
+ Info.api(response)
55
+
56
+ if http_status == 'success'
57
+ Info.api(response) unless @is_silent
58
+ @done_func(response) if @done_func
59
+ Pjax.load(@do_redirect) if @do_redirect
60
+ Pjax.reload() if @do_reload
61
+
62
+ if node = @do_refresh
63
+ if typeof(node) == 'object'
64
+ Svelte('ajax', node).reload()
65
+ else
66
+ Pjax.refresh()
67
+
68
+ if @do_follow
69
+ location = data.getResponseHeader('location') || response.meta.path
70
+
71
+ if location
72
+ Pjax.load(location)
73
+ else
74
+ Info.error 'Follow URL not found'
75
+
data/lib/misc/doc.css ADDED
@@ -0,0 +1,29 @@
1
+ * { font-family: 'Inter', sans-serif; }
2
+ .gutter { margin-left: 20px; }
3
+ .sticky { position: -webkit-sticky; position: sticky; top: 0; }
4
+ .btn-outline-info svg { fill: #17a2b8; }
5
+ .btn-outline-info:hover svg { fill: #eee; }
6
+ button.request { float: right; display: none; }
7
+ .box { border-top: 1px solid #ccc; padding: 20px 20px 10px 20px; border-top-color: #ccc; background-color: #fff; margin-bottom: 20px; }
8
+ .box:hover button.request { display: inline; }
9
+ .anchor { display: block; position: relative; top: -95px; visibility: hidden; }
10
+ .btn { background-color: #fff; }
11
+ body { background-color: #f7f7f7; margin-bottom: 200px; }
12
+ header { background-color: #fff; box-shadow:0 1px 4px rgba(0,0,0,.15); }
13
+ pre { padding-bottom: 10px; font-family: inherit;}
14
+ h1.nav a { font-size: 22px; color: #383838; padding: 17px 0 18px 0; }
15
+ h5 { font-size: 18px; font-weight: 600; }
16
+ h5 a { color: #333 !important; }
17
+ h6 { color: #888; margin-bottom: 10px; -font-size: 13px; }
18
+ gray { color: #707070; }
19
+ b { color: #666975; }
20
+ svg { margin: 0 5px; }
21
+ bold { font-weight: 600; }
22
+ ul { margin-left: -10px; margin-top: -5px;}
23
+ form label { font-weight: bold; display: block; }
24
+ code { background: '#f8f8f8'; color: #e01e5a; border: 1px solid rgb(221, 221, 221); padding: 2px 7px; border-radius: 4px; font-size: 14px; font-family: Monospace; }
25
+
26
+ pre.code { font-family: monospace; font-size: 13px; background:#f7f7f7; padding: 10px 10px; margin-top: 15px; }
27
+
28
+ #modal table td { padding-bottom: 15px; vertical-align: top; }
29
+ #modal table td:first-child { padding: 10px 30px 0 0; }
data/lib/misc/doc.js ADDED
@@ -0,0 +1,279 @@
1
+ // global state
2
+ window.State = {
3
+ active_tab: 'response',
4
+ request: {},
5
+ full_url: '',
6
+
7
+ set_request: (full_url) => {
8
+ State.full_url = full_url
9
+
10
+ parts = full_url.split('?')
11
+ parts[1] = parts[1] || ''
12
+
13
+ State.request = {
14
+ host: api_opts.mount_on,
15
+ path: parts[0].replace(api_opts.mount_on, ''),
16
+ params: parts[1],
17
+ object: {}
18
+ }
19
+
20
+ for (el of parts[1].split('&')) {
21
+ let [key, value] = el.split('=', 2)
22
+ State.request.object[key] = value
23
+ }
24
+
25
+ return State.request
26
+ },
27
+ }
28
+
29
+ // generic modal interface
30
+ window.Modal = {
31
+ render: (title, data) => {
32
+ $('#modal .modal-title').html(title)
33
+ $('#modal .modal-body').html(data)
34
+ $('#modal').show()
35
+ },
36
+ close: () => {
37
+ $('#modal').hide()
38
+ }
39
+ }
40
+
41
+ // form in modal box
42
+ window.ModalForm = {
43
+ render: (title, params) => {
44
+ let data = []
45
+
46
+ data.push(`<form onsubmit="TabResponse.render('${title}', this); return false;">`)
47
+ data.push(` <table>`)
48
+
49
+ if (title.includes('/:id/')) {
50
+ data.push(` <tr><td><label>ID</label></td><td><input id="api_id_value" type="text" class="form-control" value="" autocomplete="off" /></td></tr>`)
51
+ }
52
+
53
+ for (let [name, vals] of Object.entries(params)) {
54
+ data.push(` <tr>`)
55
+ data.push(` <td><label>${name}</label></td><td>`)
56
+
57
+ if (vals.type == 'boolean') {
58
+ data.push(` <input type="checkbox" class="form-control" name="${name}" />`)
59
+ } else {
60
+ data.push(` <input type="text" class="form-control" name="${name}" value="" autocomplete="off" />`)
61
+ }
62
+
63
+ if (vals.default) {
64
+ data.push(`<small class="form-text text-muted">default: ${vals.default}</small>`)
65
+ }
66
+
67
+ data.push(` </td></tr>`)
68
+ }
69
+
70
+ data.push(` <tr><td></td><td><button class="btn btn-outline-primary">Execute API request</button></td></tr>`)
71
+ data.push(` </table>`)
72
+ data.push(`</form>`)
73
+
74
+ data = `${data.join("\n")}<div id="api_result"></div>`
75
+
76
+ Modal.render(title, data);
77
+ }
78
+ }
79
+
80
+ // backend api call
81
+ window.TabResponse = {
82
+ render: (url, form) => {
83
+ let post = $(form).serialize()
84
+ let id_val = $('#api_id_value').val()
85
+
86
+ if (id_val) {
87
+ url = url.replace('/:id/', () => '/'+id_val+'/' )
88
+ }
89
+
90
+ full_url = url
91
+ if (post) full_url += `?${post}`
92
+
93
+ if (url.includes('/:id/')) {
94
+ alert('ID is not defined')
95
+ return
96
+ }
97
+
98
+ State.set_request(full_url)
99
+
100
+ TabResponse.render_tab_data()
101
+
102
+ let bearer = AuthButton.get()
103
+ if (bearer) {
104
+ post += post ? '&' : ''
105
+ post += 'api_token=' + bearer
106
+ }
107
+
108
+ $.post(url, post, (data) => {
109
+ State.response = data;
110
+ TabResponse.render_tab_data()
111
+ })
112
+ },
113
+
114
+ setActiveTab: (node, name) => {
115
+ let el = $(node)
116
+ // debugger
117
+ el.parents('ul').find('a').removeClass('active')
118
+ el.addClass('active')
119
+
120
+ State.active_tab = name
121
+ TabResponse.render_tab_data()
122
+ },
123
+
124
+ render_tab_data: () => {
125
+ let tabs = ['response', 'curl', 'javascript', 'ruby']
126
+
127
+ let out = []
128
+ out.push(`<div id="api_response"><br /><button class="btn btn-sm btn-outline-info" style="float: right; margin-top: -4px; margin-bottom: -30px;" onclick="$('#api_response').remove()">close</button>`)
129
+
130
+ out.push(`<ul class="nav nav-tabs" style="margin-left:0; margin-bottom: 15px;">`)
131
+
132
+ for (name of tabs) {
133
+ let is_active = State.active_tab == name ? ' active' : ''
134
+ out.push(`<li class="nav-item"><a class="nav-link ${is_active}" href="#" onclick="TabResponse.setActiveTab(this, '${name}'); return false;">${name}</a></li>`)
135
+ }
136
+
137
+ out.push(`</ul>`)
138
+
139
+ out.push(TabResponse.format[State.active_tab]())
140
+ $('#api_result').html(out.join("\n\n"))
141
+ },
142
+
143
+ format: {
144
+ response: () => {
145
+ let data = JSON.stringify(State.response || {}, null, 2)
146
+ let url = `<a href="${State.full_url}">${State.full_url}</a>`
147
+
148
+ return `<p>${url}</p><pre class="code">${data}</pre></div>`
149
+ },
150
+
151
+ curl: () => {
152
+ out = []
153
+
154
+ out.push(`# PS: you can send post data as JSON as well`)
155
+ out.push(`# JSON export is a default, "Accept: application/json" header is not needed`)
156
+ out.push(``)
157
+
158
+ out.push(`curl -s -X POST\\`)
159
+
160
+ let token = AuthButton.get()
161
+ if (token) {
162
+ out.push(` -H "Authorization: Bearer ${token}"\\`)
163
+ }
164
+
165
+ url = State.request
166
+
167
+ if (url.params) out.push(` --data '${url.params}'\\`)
168
+
169
+ out.push(` ${url.host}${url.path}`)
170
+
171
+ let parts = url.path.split('/')
172
+
173
+ let opts = {
174
+ id: 'foo-rand',
175
+ class: parts.shift(),
176
+ action: parts,
177
+ params: State.request.object,
178
+ token: token
179
+ }
180
+
181
+ out.push(``)
182
+ out.push(`# or json rpc style`)
183
+ out.push(`curl -s -X POST\\`)
184
+ out.push(` --data '${JSON.stringify(opts)}'\\`)
185
+ out.push(` ${url.host.replace(/\/$/, '')}`)
186
+
187
+ return `<pre class="code">${out.join("\n")}</pre></div>`
188
+ },
189
+
190
+ ruby: () => {
191
+ out = []
192
+
193
+ let params = JSON.stringify(State.request.object)
194
+ let parts = State.request.path.split('/')
195
+
196
+ out.push `# gem install 'joshua'`
197
+ out.push `require 'joshua/remote'\n`
198
+
199
+ out.push(`api = JoshuaRemote.new '${State.request.host.replace(/\/$/, '')}'`)
200
+
201
+ let token = AuthButton.get()
202
+ if (token) {
203
+ out.push(`api.auth_token = '${token}'`)
204
+ }
205
+
206
+ if (parts[2]) {
207
+ out.push(`api.${parts[0]}(${parts[1]}).${parts[2]}(${params})`)
208
+ out.push(`# or -> api.call('${parts[0]}/${parts[1]}/${parts[2]}', ${params})`)
209
+ out.push(`# or -> api.call(:${parts[0]}, ${parts[1]}, :${parts[2]}, ${params})`)
210
+ } else {
211
+ out.push(`api.${parts[0]}.${parts[1]}(${params})`)
212
+ out.push(`# or -> api.call('${parts[0]}/${parts[1]}', ${params})`)
213
+ out.push(`# or -> api.call(:${parts[0]}, :${parts[1]}, ${params})`)
214
+ }
215
+
216
+ out.push(`api.success?`)
217
+ out.push(`api.response`)
218
+ return `<pre class="code">${out.join("\n")}</pre></div>`
219
+ },
220
+
221
+ javascript: () => {
222
+ out = []
223
+ out.push(`const axios = require('axios').default;`)
224
+ out.push(``)
225
+ out.push(`axios.post(`)
226
+ out.push(` '${State.full_url.split('?')[0]}',`)
227
+ out.push(` ${JSON.stringify(State.request.object)},`)
228
+
229
+ let token = AuthButton.get()
230
+ if (token) {
231
+ out.push(` { headers: { Authorization: 'Bearer ${token}' } }`)
232
+ }
233
+
234
+ out.push(`).then((response) => { });`)
235
+ return `<pre class="code">${out.join("\n")}</pre></div>`
236
+ }
237
+ }
238
+ }
239
+
240
+ // auth botton
241
+ window.AuthButton = {
242
+ set: () => {
243
+ if (api_opts.bearer) {
244
+ Modal.render('Bearer token', api_opts.bearer)
245
+ } else {
246
+ let token = prompt('Bearer token?', AuthButton.get() || '')
247
+
248
+ if (token != null) {
249
+ localStorage.setItem('auth_token', token)
250
+ }
251
+ }
252
+
253
+ AuthButton.draw()
254
+ },
255
+
256
+ get: () => {
257
+ return api_opts.bearer || localStorage.getItem('auth_token')
258
+ },
259
+
260
+ draw: () => {
261
+ let value = AuthButton.get()
262
+ let text = value ? `Yes` : 'n/a'
263
+
264
+ $('#bearer_button').html(`Bearer Auth: <bold>${text}</bold>`)
265
+ }
266
+ }
267
+
268
+ AuthButton.draw()
269
+
270
+
271
+ // close dialog on escape
272
+ document.onkeydown = (evt) => {
273
+ evt = evt || window.event;
274
+ if (evt.keyCode == 27) {
275
+ Modal.close();
276
+ }
277
+ };
278
+
279
+