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.
@@ -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
+