joshua 0.2.2 → 0.2.4
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 +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
|