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.
@@ -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
@@ -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
- @message = value
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 *args
41
- return @errors unless args[0]
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
- desc, code = args.reverse
62
+ text = text.to_s
44
63
 
45
- @errors[:code] = code if code
64
+ @errors[:code] ||= code if code
46
65
  @errors[:messages] ||= []
47
- @errors[:messages].push desc unless @errors[:messages].include?(desc)
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
- @data ||= value
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.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: 2020-05-02 00:00:00.000000000 Z
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: clean-hash
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/base.rb"
95
- - "./lib/joshua/error.rb"
96
- - "./lib/joshua/opts.rb"
97
- - "./lib/joshua/params/define.rb"
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