joshua 0.2.2 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.version +1 -1
- data/lib/doc/doc.rb +8 -5
- data/lib/doc/special.rb +112 -4
- data/lib/joshua.rb +5 -8
- data/lib/joshua/base_class.rb +420 -0
- data/lib/joshua/base_instance.rb +195 -0
- data/lib/joshua/client.rb +1 -0
- data/lib/joshua/render_proxy.rb +28 -0
- data/lib/joshua/response.rb +34 -10
- data/lib/misc/doc.js +2 -2
- metadata +38 -15
- data/lib/joshua/base.rb +0 -301
- data/lib/joshua/error.rb +0 -61
- data/lib/joshua/opts.rb +0 -216
- data/lib/joshua/params/define.rb +0 -53
- data/lib/joshua/params/parse.rb +0 -56
- data/lib/joshua/params/types.rb +0 -152
- data/lib/joshua/params/types_errors.rb +0 -33
- data/lib/misc/api_example.coffee +0 -75
- data/lib/misc/ruby_client.rb +0 -52
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 890a0ef798280cde7effdabc9a385aa7adc07b52579bea110af04ab714c98e46
|
4
|
+
data.tar.gz: 749d50df471c4ee8d2badfe598b174a3eb3bf4190f88b70cc13da320b5a2386b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4fb42a8f389bea6fe2be92224ede26f06f3b8328d22c06c2c744a2cac4345ca819fc0c9d1a64af92d899de3f4a94b2d3e0cc96a031fcbec91a877dc808a519c5
|
7
|
+
data.tar.gz: aef74afb03695d3be2b5d7bfa15b94b0c5497080c314c135f7c075b07c4a019d03bd88edaa42b5650861758990e001d1dd0aaf94db88131bdeb37485977115d8
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.4
|
data/lib/doc/doc.rb
CHANGED
@@ -8,14 +8,15 @@ class Joshua
|
|
8
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
9
|
},
|
10
10
|
twitter: {
|
11
|
-
url:
|
11
|
+
url: nil,
|
12
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
13
|
},
|
14
14
|
email: {
|
15
|
-
url:
|
15
|
+
url: nil,
|
16
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
17
|
},
|
18
18
|
error: {
|
19
|
+
url: 'https://github.com/dux/joshua/issues',
|
19
20
|
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
|
}
|
21
22
|
}
|
@@ -37,7 +38,7 @@ class Joshua
|
|
37
38
|
n.head do |n|
|
38
39
|
n.title 'Joshua Tester'
|
39
40
|
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://
|
41
|
+
n.link({ rel:"stylesheet", href:"https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css" })
|
41
42
|
n.script({ src: 'https://cdnjs.cloudflare.com/ajax/libs/zepto/1.2.0/zepto.min.js' })
|
42
43
|
n.script %[window.api_opts = { mount_on: '#{mount_on}', bearer: '#{bearer}' }]
|
43
44
|
end
|
@@ -51,6 +52,8 @@ class Joshua
|
|
51
52
|
end
|
52
53
|
end
|
53
54
|
|
55
|
+
n.img src:"https://i.imgur.com/HWoUz5k.png", style: 'width: 40px; z-index: 1; position: absolute; top: 10px; left: 50%;', onclick: "window.open('https://github.com/dux/joshua')"
|
56
|
+
|
54
57
|
n.push modal_dialog
|
55
58
|
|
56
59
|
n._container do |n|
|
@@ -66,7 +69,7 @@ class Joshua
|
|
66
69
|
n.p '<b>TOOLS</b>'
|
67
70
|
n.div do |n|
|
68
71
|
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>]
|
72
|
+
n.push %[<p><a class="badge badge-light" href="#{mount_on}_/postman" target="capi_postman">Postman/Insomnija import URL</a></p>]
|
70
73
|
n.push %[<p><a class="badge badge-light" href="#{mount_on}_/raw" target="capi_raw">Raw doc data</a></p>]
|
71
74
|
end
|
72
75
|
|
@@ -74,7 +77,7 @@ class Joshua
|
|
74
77
|
|
75
78
|
n.p '<b>API LIBRARIES</b>'
|
76
79
|
n.div do |n|
|
77
|
-
n.push %[<a class="badge badge-light" href="https://github.com/dux/joshua/blob/master/lib/
|
80
|
+
n.push %[<a class="badge badge-light" href="https://github.com/dux/joshua/blob/master/lib/client/ruby/client" target="capi_ruby">Ruby</a>]
|
78
81
|
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
82
|
n.push %[<a class="badge badge-light" href="#">Python</a>]
|
80
83
|
n.push %[<a class="badge badge-light" href="#">C#</a>]
|
data/lib/doc/special.rb
CHANGED
@@ -1,20 +1,128 @@
|
|
1
1
|
# reponse from /api/_/foo
|
2
2
|
|
3
3
|
class Joshua
|
4
|
-
|
5
|
-
|
4
|
+
class DocSpecial
|
5
|
+
def initialize api
|
6
|
+
@api = api
|
7
|
+
end
|
6
8
|
|
7
9
|
def postman
|
8
|
-
|
10
|
+
out = {
|
11
|
+
info: {
|
12
|
+
_postman_id: request.url,
|
13
|
+
_bearer_token: @api[:bearer],
|
14
|
+
name: request.host,
|
15
|
+
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
16
|
+
},
|
17
|
+
item: []
|
18
|
+
}
|
19
|
+
|
20
|
+
for table, data in raw
|
21
|
+
raw_data = raw[table.to_s]
|
22
|
+
hash = {}
|
23
|
+
hash[:name] = table
|
24
|
+
hash[:item] = []
|
25
|
+
|
26
|
+
for type in [:collection, :member]
|
27
|
+
next unless raw_data[type]
|
28
|
+
|
29
|
+
if raw_data[type]
|
30
|
+
items = []
|
31
|
+
|
32
|
+
for key, value in raw_data[type]
|
33
|
+
items.push postman_add_method(type, table, key, value)
|
34
|
+
end
|
35
|
+
|
36
|
+
hash[:item].push *items
|
37
|
+
# hash[:item].push({
|
38
|
+
# name: type,
|
39
|
+
# item: items
|
40
|
+
# })
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
out[:item].push hash
|
45
|
+
end
|
46
|
+
|
47
|
+
@api[:development] ? JSON.pretty_generate(out) : out.to_json
|
9
48
|
end
|
10
49
|
|
11
50
|
def raw
|
12
51
|
unwanted = %w(all member collection)
|
13
52
|
{}.tap do |doc|
|
14
53
|
for el in Joshua.documented
|
15
|
-
doc[el.to_s.sub(/Api$/, '').
|
54
|
+
doc[el.to_s.sub(/Api$/, '').underscore] = el.opts.filter do |k, v|
|
55
|
+
for k1, v1 in v
|
56
|
+
if v1.is_a?(Hash)
|
57
|
+
for k2 in v1.keys
|
58
|
+
# remove Typero
|
59
|
+
v1.delete(k2) if k2.to_s.start_with?('_')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
!unwanted.include?(k.to_s.split('_')[1])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def postman_add_method type, table, name, item
|
73
|
+
path = []
|
74
|
+
|
75
|
+
base = request.url.split('/_/').first
|
76
|
+
base = base.split('/')
|
77
|
+
|
78
|
+
path.push base.pop
|
79
|
+
base = base.join('/')
|
80
|
+
|
81
|
+
path.push table
|
82
|
+
path.push ':id' if type == :member
|
83
|
+
path.push name
|
84
|
+
|
85
|
+
name = '%s*' % name if type == :collection
|
86
|
+
|
87
|
+
out = {
|
88
|
+
name: name,
|
89
|
+
request: {
|
90
|
+
method: 'POST',
|
91
|
+
header: [],
|
92
|
+
url: {
|
93
|
+
raw: ([base] + path).join('/'),
|
94
|
+
protocol: base.split(':').first,
|
95
|
+
host: request.host.split('.'),
|
96
|
+
port: request.port,
|
97
|
+
path: path
|
98
|
+
},
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
for key, value in (item[:params] || {})
|
103
|
+
out[:request][:body] ||= { mode: 'formdata', formdata: [] }
|
104
|
+
|
105
|
+
formdata_custom = 'formdata_%s' % value[:type]
|
106
|
+
|
107
|
+
# if value[:type] == 'model' and key == 'user' you can define "formdata_model"
|
108
|
+
# that returns list of fields for defined model
|
109
|
+
formdata_value =
|
110
|
+
if respond_to?(formdata_custom)
|
111
|
+
opts = { key: key, value: value, name: name, type: type, group: table }
|
112
|
+
[send(formdata_custom, opts.to_hwia)].flatten
|
113
|
+
else
|
114
|
+
{ key: key, description: value[:type] }
|
16
115
|
end
|
116
|
+
|
117
|
+
formdata_value = [formdata_value] unless formdata_value.is_a?(Array)
|
118
|
+
out[:request][:body][:formdata].push *formdata_value
|
17
119
|
end
|
120
|
+
|
121
|
+
out
|
122
|
+
end
|
123
|
+
|
124
|
+
def request
|
125
|
+
@api[:api_host].request
|
18
126
|
end
|
19
127
|
end
|
20
128
|
end
|
data/lib/joshua.rb
CHANGED
@@ -20,17 +20,14 @@ unless ''.respond_to?(:dasherize)
|
|
20
20
|
end
|
21
21
|
|
22
22
|
require 'json'
|
23
|
+
require 'typero'
|
23
24
|
require 'html-tag'
|
24
|
-
require '
|
25
|
+
require 'hash_wia'
|
25
26
|
|
26
|
-
require_relative './joshua/
|
27
|
-
require_relative './joshua/
|
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'
|
27
|
+
require_relative './joshua/base_instance'
|
28
|
+
require_relative './joshua/base_class'
|
33
29
|
require_relative './joshua/response'
|
30
|
+
require_relative './joshua/render_proxy'
|
34
31
|
|
35
32
|
require_relative './doc/doc'
|
36
33
|
require_relative './doc/special'
|
@@ -0,0 +1,420 @@
|
|
1
|
+
class Joshua
|
2
|
+
@@after_auto_mount = nil
|
3
|
+
@@opts = {}
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# perform auto_mount from a rake call
|
7
|
+
def call env
|
8
|
+
request = Rack::Request.new env
|
9
|
+
|
10
|
+
if request.path == '/favicon.ico'
|
11
|
+
[
|
12
|
+
200,
|
13
|
+
{ 'Cache-Control'=>'public; max-age=1000000' },
|
14
|
+
[Doc.misc_file('favicon.png')]
|
15
|
+
]
|
16
|
+
else
|
17
|
+
data = auto_mount request: request, development: ENV['RACK_ENV'] == 'development'
|
18
|
+
|
19
|
+
if data.is_a?(Hash)
|
20
|
+
[
|
21
|
+
data[:status] || 200,
|
22
|
+
{ 'Content-Type' => 'application/json', 'Cache-Control'=>'private; max-age=0' },
|
23
|
+
[data.to_json]
|
24
|
+
]
|
25
|
+
else
|
26
|
+
data = data.to_s
|
27
|
+
[
|
28
|
+
200,
|
29
|
+
{ 'Content-Type' => 'text/html', 'Cache-Control'=>'public; max-age=3600' },
|
30
|
+
[data]
|
31
|
+
]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# ApplicationApi.auto_mount request: request, response: response, mount_on: '/api', development: true
|
37
|
+
# auto mount to a root
|
38
|
+
# * display doc in a root
|
39
|
+
# * call methods if possible /api/v1.comapny/1/show
|
40
|
+
def auto_mount api_host:, mount_on: nil, bearer: nil, development: false
|
41
|
+
request = api_host.request
|
42
|
+
response = api_host.response
|
43
|
+
|
44
|
+
mount_on ||= OPTS[:api][:mount_on] || '/'
|
45
|
+
mount_on = [request.base_url, mount_on].join('') unless mount_on.to_s.include?('//')
|
46
|
+
|
47
|
+
if request.url == mount_on && request.request_method == 'GET'
|
48
|
+
response.header['Content-Type'] = 'text/html' if response
|
49
|
+
|
50
|
+
Doc.render request: request, bearer: bearer
|
51
|
+
else
|
52
|
+
response.header['Content-Type'] = 'application/json' if response
|
53
|
+
|
54
|
+
body = request.body.read.to_s
|
55
|
+
body = body[0] == '{' ? JSON.parse(body) : nil
|
56
|
+
|
57
|
+
# class: klass, params: params, bearer: bearer, request: request, response: response, development: development
|
58
|
+
opts = {}
|
59
|
+
opts[:api_host] = api_host
|
60
|
+
opts[:development] = development
|
61
|
+
opts[:bearer] = bearer
|
62
|
+
|
63
|
+
action =
|
64
|
+
if body
|
65
|
+
# {
|
66
|
+
# "id": 'foo', # unique ID that will be returned, as required by JSON RPC spec
|
67
|
+
# "class": 'v1/users', # v1/users => V1::UsersApi
|
68
|
+
# "action": 'index', # "index' or "6/info" or [6, "info"]
|
69
|
+
# "token": 'ab12ef', # api_token (bearer)
|
70
|
+
# "params": {} # methos params
|
71
|
+
# }
|
72
|
+
opts[:params] = body['params'] || {}
|
73
|
+
opts[:bearer] = body['token'] if body['token']
|
74
|
+
opts[:class] = body['class']
|
75
|
+
|
76
|
+
body['action']
|
77
|
+
else
|
78
|
+
opts[:params] = request.params || {}
|
79
|
+
opts[:bearer] = opts[:params][:api_token] if opts[:params][:api_token]
|
80
|
+
|
81
|
+
mount_on = mount_on+'/' unless mount_on.end_with?('/')
|
82
|
+
path = request.url.split(mount_on, 2).last.split('?').first.to_s
|
83
|
+
parts = path.split('/')
|
84
|
+
|
85
|
+
@@after_auto_mount.call parts, opts if @@after_auto_mount
|
86
|
+
|
87
|
+
opts[:class] = parts.shift
|
88
|
+
parts
|
89
|
+
end
|
90
|
+
|
91
|
+
opts[:bearer] ||= request.env['HTTP_AUTHORIZATION'].to_s.split('Bearer ')[1]
|
92
|
+
|
93
|
+
api_response = render action, **opts
|
94
|
+
|
95
|
+
if api_response.is_a?(Hash)
|
96
|
+
response.status = api_response[:status] if response
|
97
|
+
api_response.to_h
|
98
|
+
else
|
99
|
+
api_response
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# renders api doc or calls api class + action
|
105
|
+
def render action=nil, opts={}
|
106
|
+
if action
|
107
|
+
return error 'Action not defined' unless action[0]
|
108
|
+
else
|
109
|
+
return RenderProxy.new self
|
110
|
+
end
|
111
|
+
|
112
|
+
api_class =
|
113
|
+
if klass = opts.delete(:class)
|
114
|
+
# /api/_/foo
|
115
|
+
if klass == '_'
|
116
|
+
klass = Joshua::DocSpecial.new(opts)
|
117
|
+
|
118
|
+
if klass.respond_to?(action.first)
|
119
|
+
return klass.send action.first.to_sym
|
120
|
+
else
|
121
|
+
return error 'Action %s not defined' % action.first
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
klass = klass.split('/') if klass.is_a?(String)
|
126
|
+
klass[klass.length-1] += '_api'
|
127
|
+
|
128
|
+
begin
|
129
|
+
klass.join('/').classify.constantize
|
130
|
+
rescue NameError => e
|
131
|
+
return error 'API class "%s" not found' % klass
|
132
|
+
end
|
133
|
+
else
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
api = api_class.new action, **opts
|
138
|
+
api.execute_call
|
139
|
+
rescue => error
|
140
|
+
error_print error if opts[:development]
|
141
|
+
Response.auto_format error
|
142
|
+
end
|
143
|
+
|
144
|
+
# rescue_from CustomError do ...
|
145
|
+
# for unhandled
|
146
|
+
# rescue_from :all do
|
147
|
+
# api.error 500, 'Error happens'
|
148
|
+
# end
|
149
|
+
# define handled error code and description
|
150
|
+
# error :not_found, 'Document not found'
|
151
|
+
# error 404, 'Document not found'
|
152
|
+
# in api methods
|
153
|
+
# error 404
|
154
|
+
# error :not_found
|
155
|
+
def rescue_from klass, desc=nil, &block
|
156
|
+
RESCUE_FROM[klass] = desc || block
|
157
|
+
end
|
158
|
+
|
159
|
+
def after_auto_mount &blok
|
160
|
+
@@after_auto_mount = blok
|
161
|
+
end
|
162
|
+
|
163
|
+
# show and render single error in class error format
|
164
|
+
# usually when API class not found
|
165
|
+
def response_error text
|
166
|
+
out = Response.new nil
|
167
|
+
out.error text
|
168
|
+
out.render
|
169
|
+
end
|
170
|
+
|
171
|
+
# class errors, raised by params validation
|
172
|
+
def error desc
|
173
|
+
raise Joshua::Error, desc
|
174
|
+
end
|
175
|
+
|
176
|
+
def error_print error
|
177
|
+
puts
|
178
|
+
puts 'Joshua error dump'.red
|
179
|
+
puts '---'
|
180
|
+
puts '%s: %s' % [error.class, error.message]
|
181
|
+
puts '---'
|
182
|
+
puts error.backtrace
|
183
|
+
puts '---'
|
184
|
+
end
|
185
|
+
|
186
|
+
# sets api mount point
|
187
|
+
# mount_on '/api'
|
188
|
+
def mount_on what
|
189
|
+
OPTS[:api][:mount_on] = what
|
190
|
+
end
|
191
|
+
|
192
|
+
# if you want to make API DOC public use "documented"
|
193
|
+
def documented
|
194
|
+
if self == Joshua
|
195
|
+
DOCUMENTED.map(&:to_s).sort.map(&:constantize)
|
196
|
+
else
|
197
|
+
DOCUMENTED.push self unless DOCUMENTED.include?(self)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def api_path
|
202
|
+
to_s.underscore.sub(/_api$/, '')
|
203
|
+
end
|
204
|
+
|
205
|
+
# define method annotations
|
206
|
+
# annotation :unsecure! do
|
207
|
+
# @is_unsecure = true
|
208
|
+
# end
|
209
|
+
# unsecure!
|
210
|
+
# def login
|
211
|
+
# ...
|
212
|
+
def annotation name, &block
|
213
|
+
ANNOTATIONS[name] = block
|
214
|
+
self.define_singleton_method name do |*args|
|
215
|
+
unless @method_type
|
216
|
+
error 'Annotation "%s" defined outside the API method blocks (member & collections)' % name
|
217
|
+
end
|
218
|
+
|
219
|
+
@@opts[:annotations] ||= {}
|
220
|
+
@@opts[:annotations][name] = args
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
# aleternative way to define a api function
|
225
|
+
# members do
|
226
|
+
# define :foo do
|
227
|
+
# params {}
|
228
|
+
# proc {}
|
229
|
+
# end
|
230
|
+
# end
|
231
|
+
def define name, &block
|
232
|
+
func = class_exec &block
|
233
|
+
|
234
|
+
if func.is_a?(Proc)
|
235
|
+
self.define_method(name, func)
|
236
|
+
else
|
237
|
+
raise 'Member block has to return a Func object'
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# /api/companies/1/show
|
242
|
+
def member &block
|
243
|
+
@method_type = :member
|
244
|
+
func = class_exec &block
|
245
|
+
@method_type = nil
|
246
|
+
end
|
247
|
+
alias :members :member
|
248
|
+
|
249
|
+
# /api/companies/list?countrty_id=1
|
250
|
+
def collection &block
|
251
|
+
@method_type = :collection
|
252
|
+
class_exec &block
|
253
|
+
@method_type = nil
|
254
|
+
end
|
255
|
+
alias :collections :collection
|
256
|
+
|
257
|
+
# params do
|
258
|
+
# name? String
|
259
|
+
# email :email
|
260
|
+
# end
|
261
|
+
def params &block
|
262
|
+
raise ArgumentError.new('Block not given for Joshua API method params') unless block_given?
|
263
|
+
|
264
|
+
@@opts[:_typero] = Typero.schema &block
|
265
|
+
@@opts[:params] = @@opts[:_typero].to_h
|
266
|
+
end
|
267
|
+
|
268
|
+
# api method icon
|
269
|
+
# you can find great icons at https://boxicons.com/ - export to svg
|
270
|
+
def icon data
|
271
|
+
if @method_type
|
272
|
+
raise ArgumentError.new('Icons cant be added on methods')
|
273
|
+
else
|
274
|
+
set :opts, :icon, data
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# api method description
|
279
|
+
def desc data
|
280
|
+
if @method_type
|
281
|
+
@@opts[:desc] = data
|
282
|
+
else
|
283
|
+
set :opts, :desc, data
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# api method detailed description
|
288
|
+
def detail data
|
289
|
+
return if data.to_s == ''
|
290
|
+
|
291
|
+
if @method_type
|
292
|
+
@@opts[:detail] = data
|
293
|
+
else
|
294
|
+
set :opts, :detail, data
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def allow type
|
299
|
+
if @method_type
|
300
|
+
@@opts[:allow] = type
|
301
|
+
else
|
302
|
+
raise ArgumentError.new('allow can only be set on methods')
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
# method in available for GET requests as well
|
307
|
+
def gettable
|
308
|
+
if @method_type
|
309
|
+
@@opts[:gettable] = true
|
310
|
+
else
|
311
|
+
raise ArgumentError.new('gettable can only be set on methods')
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
# allow methods without @api.bearer token set
|
316
|
+
def unsafe
|
317
|
+
if @method_type
|
318
|
+
@@opts[:unsafe] = true
|
319
|
+
else
|
320
|
+
raise ArgumentError.new('Only api methods can be unsafe')
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# block execute before any public method or just some member or collection methods
|
325
|
+
def before &block
|
326
|
+
set_callback :before, block
|
327
|
+
end
|
328
|
+
|
329
|
+
# block execute after any public method or just some member or collection methods
|
330
|
+
# used to add meta tags to response
|
331
|
+
def after &block
|
332
|
+
set_callback :after, block
|
333
|
+
end
|
334
|
+
|
335
|
+
# simplified module include, masked as plugin
|
336
|
+
# Joshua.plugin :foo do ...
|
337
|
+
# Joshua.plugin :foo
|
338
|
+
def plugin name, &block
|
339
|
+
if block_given?
|
340
|
+
# if block given, define a plugin
|
341
|
+
PLUGINS[name] = block
|
342
|
+
else
|
343
|
+
# without a block execute it
|
344
|
+
blk = PLUGINS[name]
|
345
|
+
raise ArgumentError.new('Plugin :%s not defined' % name) unless blk
|
346
|
+
instance_exec &blk
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def get *args
|
351
|
+
opts.dig *args
|
352
|
+
end
|
353
|
+
|
354
|
+
# dig all options for a current class
|
355
|
+
def opts
|
356
|
+
out = {}
|
357
|
+
|
358
|
+
# dig down the ancestors tree till Object class
|
359
|
+
ancestors.each do |klass|
|
360
|
+
break if klass == Object
|
361
|
+
|
362
|
+
# copy all member and collection method options
|
363
|
+
keys = (OPTS[klass.to_s] || {}).keys
|
364
|
+
keys.each do |type|
|
365
|
+
for k, v in (OPTS.dig(klass.to_s, type) || {})
|
366
|
+
out[type] ||= {}
|
367
|
+
out[type][k] ||= v
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
out
|
373
|
+
end
|
374
|
+
|
375
|
+
# propagate to typero
|
376
|
+
def model name, &block
|
377
|
+
Typero.schema name, &block
|
378
|
+
end
|
379
|
+
|
380
|
+
# here we capture member & collection metods
|
381
|
+
def method_added name
|
382
|
+
return if name.to_s.start_with?('_api_')
|
383
|
+
return unless @method_type
|
384
|
+
|
385
|
+
set @method_type, name, @@opts
|
386
|
+
|
387
|
+
@@opts = {}
|
388
|
+
|
389
|
+
alias_method "_api_#{@method_type}_#{name}", name
|
390
|
+
remove_method name
|
391
|
+
end
|
392
|
+
|
393
|
+
private
|
394
|
+
|
395
|
+
def only_in_api_methods!
|
396
|
+
raise ArgumentError, "Available only inside collection or member block for API methods." unless @method_type
|
397
|
+
end
|
398
|
+
|
399
|
+
def set_callback name, block
|
400
|
+
name = [name, @method_type || :all].join('_').to_sym
|
401
|
+
set name, []
|
402
|
+
OPTS[to_s][name].push block
|
403
|
+
end
|
404
|
+
|
405
|
+
# generic opts set
|
406
|
+
# set :user_name, :email, :baz
|
407
|
+
def set *args
|
408
|
+
name, value = args.pop(2)
|
409
|
+
args.unshift to_s
|
410
|
+
pointer = OPTS
|
411
|
+
|
412
|
+
for el in args
|
413
|
+
pointer[el] ||= {}
|
414
|
+
pointer = pointer[el]
|
415
|
+
end
|
416
|
+
|
417
|
+
pointer[name] = value
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|