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
@@ -0,0 +1,195 @@
|
|
1
|
+
class Joshua
|
2
|
+
class Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
ANNOTATIONS ||= {}
|
6
|
+
RESCUE_FROM ||= {}
|
7
|
+
OPTS ||= { api: {} }
|
8
|
+
PLUGINS ||= {}
|
9
|
+
MODELS ||= {}
|
10
|
+
DOCUMENTED ||= []
|
11
|
+
INSTANCE ||= Struct.new 'JoshuaOpts',
|
12
|
+
:action,
|
13
|
+
:bearer,
|
14
|
+
:development,
|
15
|
+
:id,
|
16
|
+
:method_opts,
|
17
|
+
:opts,
|
18
|
+
:params,
|
19
|
+
:raw,
|
20
|
+
:api_host,
|
21
|
+
:request,
|
22
|
+
:response,
|
23
|
+
:uid
|
24
|
+
|
25
|
+
attr_reader :api
|
26
|
+
|
27
|
+
def initialize action, params: {}, opts: {}, development: false, id: nil, bearer: nil, api_host: nil
|
28
|
+
@api = INSTANCE.new
|
29
|
+
|
30
|
+
if action.is_a?(Array)
|
31
|
+
# unpack id and action is action is given in path form # [123, :show]
|
32
|
+
@api.id, @api.action = action[1] ? action : [nil, action[0]]
|
33
|
+
else
|
34
|
+
@api.action = action
|
35
|
+
end
|
36
|
+
|
37
|
+
@api.bearer = bearer
|
38
|
+
@api.id ||= id
|
39
|
+
@api.action = @api.action.to_sym
|
40
|
+
@api.request = api_host ? api_host.request : nil
|
41
|
+
@api.method_opts = self.class.opts.dig(@api.id ? :member : :collection, @api.action) || {}
|
42
|
+
@api.development = !!development
|
43
|
+
@api.params = HashWia.new params
|
44
|
+
@api.opts = HashWia.new opts
|
45
|
+
@api.api_host = api_host
|
46
|
+
@api.response = ::Joshua::Response.new @api
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute_call
|
50
|
+
if !@api.development && @api.request && @api.request.request_method == 'GET' && !@api.method_opts[:gettable]
|
51
|
+
response.error 'GET request is not allowed'
|
52
|
+
else
|
53
|
+
begin
|
54
|
+
parse_api_params
|
55
|
+
parse_annotations unless response.error?
|
56
|
+
resolve_api_body unless response.error?
|
57
|
+
rescue Joshua::Error => error
|
58
|
+
# controlled error raised via error "message", ignore
|
59
|
+
response.error error.message
|
60
|
+
rescue => error
|
61
|
+
# uncontrolled error, should be logged
|
62
|
+
Joshua.error_print error if @api.development
|
63
|
+
|
64
|
+
block = RESCUE_FROM[error.class] || RESCUE_FROM[:all]
|
65
|
+
|
66
|
+
if block
|
67
|
+
instance_exec error, &block
|
68
|
+
else
|
69
|
+
response.error error.message, status: 500
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# we execute generic after block in case of error or no
|
74
|
+
execute_callback :after_all
|
75
|
+
end
|
76
|
+
|
77
|
+
@api.raw || response.render
|
78
|
+
end
|
79
|
+
|
80
|
+
def to_json
|
81
|
+
execute_call.to_json
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_h
|
85
|
+
execute_call
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def parse_api_params
|
91
|
+
params = @api.method_opts[:params]
|
92
|
+
typero = @api.method_opts[:_typero]
|
93
|
+
|
94
|
+
if params && typero
|
95
|
+
# add validation errors
|
96
|
+
typero.validate @api.params do |name, error|
|
97
|
+
response.error_detail name, error
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def resolve_api_body &block
|
103
|
+
# execute before "in the wild"
|
104
|
+
# model @api.pbject should be set here
|
105
|
+
execute_callback :before_all
|
106
|
+
|
107
|
+
instance_exec &block if block
|
108
|
+
|
109
|
+
# if we have model defiend, we execute member otherwise collection
|
110
|
+
type = @api.id ? :member : :collection
|
111
|
+
|
112
|
+
execute_callback 'before_%s' % type
|
113
|
+
api_method = '_api_%s_%s' % [type, @api.action]
|
114
|
+
raise Joshua::Error, "Api method #{type}:#{@api.action} not found" unless respond_to?(api_method)
|
115
|
+
|
116
|
+
data = send api_method
|
117
|
+
response.data data unless response.data?
|
118
|
+
|
119
|
+
# after blocks
|
120
|
+
execute_callback 'after_%s' % type
|
121
|
+
end
|
122
|
+
|
123
|
+
def parse_annotations
|
124
|
+
for key, opts in (@api.method_opts[:annotations] || {})
|
125
|
+
instance_exec *opts, &ANNOTATIONS[key]
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def execute_callback name
|
130
|
+
self.class.ancestors.reverse.map(&:to_s).each do |klass|
|
131
|
+
if before_list = (OPTS.dig(klass, name.to_sym) || [])
|
132
|
+
for before in before_list
|
133
|
+
instance_exec response.data, &before
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def response content_type=nil
|
140
|
+
if block_given?
|
141
|
+
@api.raw = yield
|
142
|
+
|
143
|
+
api_host do
|
144
|
+
response.header['Content-Type'] = content_type || (@api.raw[0] == '{' ? 'application/json' : 'text/plain')
|
145
|
+
end
|
146
|
+
elsif content_type
|
147
|
+
response.data = content_type
|
148
|
+
else
|
149
|
+
@api.response
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def params
|
154
|
+
@api.params
|
155
|
+
end
|
156
|
+
|
157
|
+
# inline error raise
|
158
|
+
def error text, args={}
|
159
|
+
puts 'JOSHUA API Error: %s (%s)' % [text, caller[0]] if @api.development
|
160
|
+
|
161
|
+
if err = RESCUE_FROM[text]
|
162
|
+
if err.is_a?(Proc)
|
163
|
+
err.call
|
164
|
+
return
|
165
|
+
else
|
166
|
+
response.error err, args
|
167
|
+
end
|
168
|
+
else
|
169
|
+
response.error text, args
|
170
|
+
end
|
171
|
+
|
172
|
+
raise Joshua::Error, text
|
173
|
+
end
|
174
|
+
|
175
|
+
def message data
|
176
|
+
response.message data
|
177
|
+
end
|
178
|
+
|
179
|
+
def super! name=nil
|
180
|
+
type = @api.id ? :member : :collection
|
181
|
+
name ||= caller[0].split('`')[1].sub("'", '')
|
182
|
+
name = "_api_#{type}_#{name}"
|
183
|
+
self.class.superclass.instance_method(name).bind(self).call
|
184
|
+
end
|
185
|
+
|
186
|
+
# execute actions on api host
|
187
|
+
def api_host &block
|
188
|
+
if block_given? && @api.api_host
|
189
|
+
@api.api_host.instance_exec self, &block
|
190
|
+
end
|
191
|
+
|
192
|
+
@api.api_host
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative '../../client/ruby/client'
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Proxy class for simplified more user friendly render
|
2
|
+
#
|
3
|
+
# UserApi.render.login(123, foo: 'bar') -> UserApi.render :login, id: 133, params: { foo: 'bar' }
|
4
|
+
#
|
5
|
+
# spec/tests/proxy_spec.rb
|
6
|
+
# UserApi.render.login(user: 'foo', pass: 'bar')
|
7
|
+
# CompanyApi.render.show(1)
|
8
|
+
|
9
|
+
class Joshua
|
10
|
+
class RenderProxy
|
11
|
+
def initialize api
|
12
|
+
@api = api
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing method_name, *args
|
16
|
+
# if first param present, it must be resource ID
|
17
|
+
api_id = args.shift unless args.first.is_a?(Hash)
|
18
|
+
|
19
|
+
# convinience, second param is params hash, options follw
|
20
|
+
params, opts = [args[0], args[1] || {}]
|
21
|
+
|
22
|
+
# merge id and params to options
|
23
|
+
opts.merge! params: params, id: api_id
|
24
|
+
|
25
|
+
@api.render method_name, opts
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/joshua/response.rb
CHANGED
@@ -2,6 +2,16 @@
|
|
2
2
|
|
3
3
|
class Joshua
|
4
4
|
class Response
|
5
|
+
attr_reader :errors
|
6
|
+
|
7
|
+
def self.auto_format error
|
8
|
+
response = new nil
|
9
|
+
response.error error.message, code: error.is_a?(Joshua::Error) ? 400 : 500
|
10
|
+
response.render
|
11
|
+
end
|
12
|
+
|
13
|
+
###
|
14
|
+
|
5
15
|
def initialize api
|
6
16
|
@api = api
|
7
17
|
@out = {}
|
@@ -23,8 +33,12 @@ class Joshua
|
|
23
33
|
end
|
24
34
|
|
25
35
|
# human readable response message
|
26
|
-
def message value
|
27
|
-
|
36
|
+
def message value, force=false
|
37
|
+
if force
|
38
|
+
@message = value
|
39
|
+
else
|
40
|
+
@message ||= value
|
41
|
+
end
|
28
42
|
end
|
29
43
|
|
30
44
|
# api meta response, any data is allowed
|
@@ -37,14 +51,19 @@ class Joshua
|
|
37
51
|
end
|
38
52
|
|
39
53
|
# add api response error
|
40
|
-
def error
|
41
|
-
|
54
|
+
def error text, args={}
|
55
|
+
code = args.delete(:code)
|
56
|
+
status = args.delete(:status)
|
57
|
+
|
58
|
+
raise 'Key %s is not supported' % args.keys.first if args.keys.first
|
59
|
+
|
60
|
+
@status ||= status if status
|
42
61
|
|
43
|
-
|
62
|
+
text = text.to_s
|
44
63
|
|
45
|
-
@errors[:code]
|
64
|
+
@errors[:code] ||= code if code
|
46
65
|
@errors[:messages] ||= []
|
47
|
-
@errors[:messages].push
|
66
|
+
@errors[:messages].push text unless @errors[:messages].include?(text)
|
48
67
|
end
|
49
68
|
|
50
69
|
def error?
|
@@ -58,9 +77,14 @@ class Joshua
|
|
58
77
|
@errors[:details][name] = desc
|
59
78
|
end
|
60
79
|
|
61
|
-
def data value
|
62
|
-
|
80
|
+
def data value=:_undefind
|
81
|
+
if value == :_undefind
|
82
|
+
@data
|
83
|
+
else
|
84
|
+
@data = value
|
85
|
+
end
|
63
86
|
end
|
87
|
+
alias :data= :data
|
64
88
|
|
65
89
|
def data?
|
66
90
|
!@data.nil?
|
@@ -79,7 +103,7 @@ class Joshua
|
|
79
103
|
out[:meta] = @meta
|
80
104
|
out[:message] = @message if @message
|
81
105
|
out[:data] = @data unless @data.nil?
|
82
|
-
out[:status] = error? ? 400 : 200
|
106
|
+
out[:status] = @status || (error? ? 400 : 200)
|
83
107
|
end
|
84
108
|
end
|
85
109
|
end
|
data/lib/misc/doc.js
CHANGED
@@ -44,7 +44,7 @@ window.ModalForm = {
|
|
44
44
|
let data = []
|
45
45
|
|
46
46
|
data.push(`<form onsubmit="TabResponse.render('${title}', this); return false;">`)
|
47
|
-
data.push(` <table>`)
|
47
|
+
data.push(` <table class="table">`)
|
48
48
|
|
49
49
|
if (title.includes('/:id/')) {
|
50
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>`)
|
@@ -55,7 +55,7 @@ window.ModalForm = {
|
|
55
55
|
data.push(` <td><label>${name}</label></td><td>`)
|
56
56
|
|
57
57
|
if (vals.type == 'boolean') {
|
58
|
-
data.push(` <input type="checkbox" class="form-control" name="${name}" />`)
|
58
|
+
data.push(` <input type="checkbox" class="form-control" name="${name}" style="width: 20px; height: 20px;" />`)
|
59
59
|
} else {
|
60
60
|
data.push(` <input type="text" class="form-control" name="${name}" value="" autocomplete="off" />`)
|
61
61
|
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: joshua
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dino Reic
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-inflector
|
@@ -53,7 +53,21 @@ dependencies:
|
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
56
|
+
name: hash_wia
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: typero
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
58
72
|
requirements:
|
59
73
|
- - ">="
|
@@ -80,6 +94,20 @@ dependencies:
|
|
80
94
|
- - ">="
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: http
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
83
111
|
description: Ruby language based, framework agnostic API request/response lib
|
84
112
|
email: rejotl@gmail.com
|
85
113
|
executables: []
|
@@ -91,24 +119,19 @@ files:
|
|
91
119
|
- "./lib/doc/doc.rb"
|
92
120
|
- "./lib/doc/special.rb"
|
93
121
|
- "./lib/joshua.rb"
|
94
|
-
- "./lib/joshua/
|
95
|
-
- "./lib/joshua/
|
96
|
-
- "./lib/joshua/
|
97
|
-
- "./lib/joshua/
|
98
|
-
- "./lib/joshua/params/parse.rb"
|
99
|
-
- "./lib/joshua/params/types.rb"
|
100
|
-
- "./lib/joshua/params/types_errors.rb"
|
122
|
+
- "./lib/joshua/base_class.rb"
|
123
|
+
- "./lib/joshua/base_instance.rb"
|
124
|
+
- "./lib/joshua/client.rb"
|
125
|
+
- "./lib/joshua/render_proxy.rb"
|
101
126
|
- "./lib/joshua/response.rb"
|
102
|
-
- "./lib/misc/api_example.coffee"
|
103
127
|
- "./lib/misc/doc.css"
|
104
128
|
- "./lib/misc/doc.js"
|
105
129
|
- "./lib/misc/favicon.png"
|
106
|
-
- "./lib/misc/ruby_client.rb"
|
107
130
|
homepage: http://github.com/dux/joshua
|
108
131
|
licenses:
|
109
132
|
- MIT
|
110
133
|
metadata: {}
|
111
|
-
post_install_message:
|
134
|
+
post_install_message:
|
112
135
|
rdoc_options: []
|
113
136
|
require_paths:
|
114
137
|
- lib
|
@@ -124,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
147
|
version: '0'
|
125
148
|
requirements: []
|
126
149
|
rubygems_version: 3.0.6
|
127
|
-
signing_key:
|
150
|
+
signing_key:
|
128
151
|
specification_version: 4
|
129
152
|
summary: Joshua
|
130
153
|
test_files: []
|
data/lib/joshua/base.rb
DELETED
@@ -1,301 +0,0 @@
|
|
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
|
-
# perform auto_mount from a rake call
|
20
|
-
def call env
|
21
|
-
request = Rack::Request.new env
|
22
|
-
|
23
|
-
if request.path == '/favicon.ico'
|
24
|
-
[
|
25
|
-
200,
|
26
|
-
{ 'Cache-Control'=>'public; max-age=1000000' },
|
27
|
-
[Doc.misc_file('favicon.png')]
|
28
|
-
]
|
29
|
-
else
|
30
|
-
data = auto_mount request: request, mount_on: '/', development: ENV['RACK_ENV'] == 'development'
|
31
|
-
|
32
|
-
if data.is_a?(Hash)
|
33
|
-
[
|
34
|
-
200,
|
35
|
-
{ 'Content-Type' => 'application/json', 'Cache-Control'=>'private; max-age=0' },
|
36
|
-
[data.to_json]
|
37
|
-
]
|
38
|
-
else
|
39
|
-
data = data.to_s
|
40
|
-
[
|
41
|
-
200,
|
42
|
-
{ 'Content-Type' => 'text/html', 'Cache-Control'=>'public; max-age=3600' },
|
43
|
-
[data]
|
44
|
-
]
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
# ApplicationApi.auto_mount request: request, response: response, mount_on: '/api', development: true
|
50
|
-
# auto mount to a root
|
51
|
-
# * display doc in a root
|
52
|
-
# * call methods if possible /api/v1.comapny/1/show
|
53
|
-
def auto_mount request:, response: nil, mount_on: nil, bearer: nil, development: false
|
54
|
-
mount_on = [request.base_url, mount_on].join('') unless mount_on.to_s.include?('//')
|
55
|
-
|
56
|
-
if request.url == mount_on && request.request_method == 'GET'
|
57
|
-
response.header['Content-Type'] = 'text/html' if response
|
58
|
-
|
59
|
-
Doc.render request: request, bearer: bearer
|
60
|
-
else
|
61
|
-
response.header['Content-Type'] = 'application/json' if response
|
62
|
-
|
63
|
-
body = request.body.read.to_s
|
64
|
-
body = body[0] == '{' ? JSON.parse(body) : nil
|
65
|
-
|
66
|
-
# class: klass, params: params, bearer: bearer, request: request, response: response, development: development
|
67
|
-
opts = {}
|
68
|
-
opts[:request] = request
|
69
|
-
opts[:response] = response
|
70
|
-
opts[:development] = development
|
71
|
-
opts[:bearer] = bearer
|
72
|
-
|
73
|
-
action =
|
74
|
-
if body
|
75
|
-
# {
|
76
|
-
# "id": 'foo', # unique ID that will be returned, as required by JSON RPC spec
|
77
|
-
# "class": 'v1/users', # v1/users => V1::UsersApi
|
78
|
-
# "action": 'index', # "index' or "6/info" or [6, "info"]
|
79
|
-
# "token": 'ab12ef', # api_token (bearer)
|
80
|
-
# "params": {} # methos params
|
81
|
-
# }
|
82
|
-
opts[:params] = body['params'] || {}
|
83
|
-
opts[:bearer] = body['token'] if body['token']
|
84
|
-
opts[:class] = body['class']
|
85
|
-
|
86
|
-
body['action']
|
87
|
-
else
|
88
|
-
opts[:params] = request.params || {}
|
89
|
-
opts[:bearer] = opts[:params][:api_token] if opts[:params][:api_token]
|
90
|
-
|
91
|
-
mount_on = mount_on+'/' unless mount_on.end_with?('/')
|
92
|
-
path = request.url.split(mount_on, 2).last.split('?').first.to_s
|
93
|
-
parts = path.split('/')
|
94
|
-
|
95
|
-
opts[:class] = parts.shift
|
96
|
-
parts
|
97
|
-
end
|
98
|
-
|
99
|
-
opts[:bearer] ||= request.env['HTTP_AUTHORIZATION'].to_s.split('Bearer ')[1]
|
100
|
-
|
101
|
-
api_response = render action, **opts
|
102
|
-
|
103
|
-
if api_response.is_a?(Hash)
|
104
|
-
response.status = api_response[:status] if response
|
105
|
-
api_response.to_h
|
106
|
-
else
|
107
|
-
api_response
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def render action, opts={}
|
113
|
-
return error 'Action not defined' unless action[0]
|
114
|
-
|
115
|
-
api_class =
|
116
|
-
if klass = opts.delete(:class)
|
117
|
-
# /api/_/foo
|
118
|
-
if klass == '_'
|
119
|
-
if Joshua::DocSpecial.respond_to?(action.first)
|
120
|
-
return Joshua::DocSpecial.send action.first.to_sym
|
121
|
-
else
|
122
|
-
return error 'Action %s not defined' % action.first
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
klass = klass.split('/') if klass.is_a?(String)
|
127
|
-
klass[klass.length-1] += '_api'
|
128
|
-
|
129
|
-
begin
|
130
|
-
klass.join('/').classify.constantize
|
131
|
-
rescue NameError => e
|
132
|
-
return error 'API class "%s" not found' % klass
|
133
|
-
end
|
134
|
-
else
|
135
|
-
self
|
136
|
-
end
|
137
|
-
|
138
|
-
api = api_class.new action, **opts
|
139
|
-
api.execute_call
|
140
|
-
end
|
141
|
-
|
142
|
-
private
|
143
|
-
|
144
|
-
def only_in_api_methods!
|
145
|
-
raise ArgumentError, "Available only inside collection or member block for API methods." unless @method_type
|
146
|
-
end
|
147
|
-
|
148
|
-
def set_callback name, block
|
149
|
-
name = [name, @method_type || :all].join('_').to_sym
|
150
|
-
set name, []
|
151
|
-
OPTS[to_s][name].push block
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
###
|
156
|
-
|
157
|
-
def initialize action, id: nil, bearer: nil, params: {}, opts: {}, request: nil, response: nil, development: false
|
158
|
-
@api = INSTANCE.new
|
159
|
-
|
160
|
-
if action.is_a?(Array)
|
161
|
-
# unpack id and action is action is given in path form # [123, :show]
|
162
|
-
@api.id, @api.action = action[1] ? action : [nil, action[0]]
|
163
|
-
else
|
164
|
-
@api.action = action
|
165
|
-
end
|
166
|
-
|
167
|
-
@api.bearer = bearer
|
168
|
-
@api.id ||= id
|
169
|
-
@api.action = @api.action.to_sym
|
170
|
-
@api.request = request
|
171
|
-
@api.method_opts = self.class.opts.dig(@api.id ? :member : :collection, @api.action) || {}
|
172
|
-
@api.development = !!development
|
173
|
-
@api.rack_response = response
|
174
|
-
@api.params = ::CleanHash::Indifferent.new params
|
175
|
-
@api.opts = ::CleanHash::Indifferent.new opts
|
176
|
-
@api.response = ::Joshua::Response.new @api
|
177
|
-
end
|
178
|
-
|
179
|
-
def message data
|
180
|
-
response.message data
|
181
|
-
end
|
182
|
-
|
183
|
-
def execute_call
|
184
|
-
if !@api.development && @api.request && @api.request_method == 'GET' && !@api.method_opts[:gettable]
|
185
|
-
response.error 'GET request is not allowed'
|
186
|
-
else
|
187
|
-
parse_api_params
|
188
|
-
parse_annotations unless response.error?
|
189
|
-
resolve_api_body unless response.error?
|
190
|
-
end
|
191
|
-
|
192
|
-
@api.raw || response.render
|
193
|
-
end
|
194
|
-
|
195
|
-
def resolve_api_body &block
|
196
|
-
begin
|
197
|
-
# execute before "in the wild"
|
198
|
-
# model @api.pbject should be set here
|
199
|
-
execute_callback :before_all
|
200
|
-
|
201
|
-
instance_exec &block if block
|
202
|
-
|
203
|
-
# if we have model defiend, we execute member otherwise collection
|
204
|
-
type = @api.id ? :member : :collection
|
205
|
-
|
206
|
-
execute_callback 'before_%s' % type
|
207
|
-
api_method = '_api_%s_%s' % [type, @api.action]
|
208
|
-
raise Joshua::Error, "Api method #{type}:#{@api.action} not found" unless respond_to?(api_method)
|
209
|
-
data = send api_method
|
210
|
-
response.data data unless response.data?
|
211
|
-
|
212
|
-
# after blocks
|
213
|
-
execute_callback 'after_%s' % type
|
214
|
-
rescue Joshua::Error => error
|
215
|
-
# controlled error raised via error "message", ignore
|
216
|
-
response.error error.message
|
217
|
-
rescue => error
|
218
|
-
Joshua.error_print error
|
219
|
-
|
220
|
-
block = RESCUE_FROM[error.class] || RESCUE_FROM[:all]
|
221
|
-
|
222
|
-
if block
|
223
|
-
instance_exec error, &block
|
224
|
-
else
|
225
|
-
# uncontrolled error, should be logged
|
226
|
-
# search to response[:code] 500 in after block
|
227
|
-
response.error error.message
|
228
|
-
response.error :class, error.class.to_s
|
229
|
-
response.error :code, 500
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
# we execute generic after block in case of error or no
|
234
|
-
execute_callback :after_all
|
235
|
-
end
|
236
|
-
|
237
|
-
def to_json
|
238
|
-
execute_call.to_json
|
239
|
-
end
|
240
|
-
|
241
|
-
def to_h
|
242
|
-
execute_call
|
243
|
-
end
|
244
|
-
|
245
|
-
private
|
246
|
-
|
247
|
-
def parse_api_params
|
248
|
-
return unless @api.method_opts[:params]
|
249
|
-
|
250
|
-
parse = Joshua::Params::Parse.new
|
251
|
-
|
252
|
-
for name, opts in @api.method_opts[:params]
|
253
|
-
# enforce required
|
254
|
-
if opts[:required] && @api.params[name].to_s == ''
|
255
|
-
response.error_detail name, 'Argument missing'
|
256
|
-
next
|
257
|
-
end
|
258
|
-
|
259
|
-
begin
|
260
|
-
# check and coerce value
|
261
|
-
@api.params[name] = parse.check opts[:type], @api.params[name], opts
|
262
|
-
rescue Joshua::Error => error
|
263
|
-
# add to details if error found
|
264
|
-
response.error_detail name, error.message
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
def parse_annotations
|
270
|
-
for key, opts in (@api.method_opts[:annotations] || {})
|
271
|
-
instance_exec *opts, &ANNOTATIONS[key]
|
272
|
-
end
|
273
|
-
end
|
274
|
-
|
275
|
-
def execute_callback name
|
276
|
-
self.class.ancestors.reverse.map(&:to_s).each do |klass|
|
277
|
-
if before_list = (OPTS.dig(klass, name.to_sym) || [])
|
278
|
-
for before in before_list
|
279
|
-
instance_exec &before
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
def response content_type=nil
|
286
|
-
if block_given?
|
287
|
-
@api.raw = yield
|
288
|
-
|
289
|
-
if @api.rack_response
|
290
|
-
@api.rack_response.header['Content-Type'] = content_type || (@api.raw[0] == '{' ? 'application/json' : 'text/plain')
|
291
|
-
end
|
292
|
-
else
|
293
|
-
@api.response
|
294
|
-
end
|
295
|
-
end
|
296
|
-
|
297
|
-
def params
|
298
|
-
@api.params
|
299
|
-
end
|
300
|
-
|
301
|
-
end
|