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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97f67b2151729e09380d17e95912b296ff0c331350fae452e9f76dca7eb261ff
4
- data.tar.gz: a357cae187aa7fe62a156fe072483ee2beabc551f8d0f4a3f7ef4cbddf54fbb1
3
+ metadata.gz: 890a0ef798280cde7effdabc9a385aa7adc07b52579bea110af04ab714c98e46
4
+ data.tar.gz: 749d50df471c4ee8d2badfe598b174a3eb3bf4190f88b70cc13da320b5a2386b
5
5
  SHA512:
6
- metadata.gz: d436bb1c456d35c94eb58652b8f8039e82e73f9843d27b260228ee5138f9e97e3f5f50f3e198071a1bbb31687a0a52b0b71b4e6d6cd63e9b545c3b4f5cf1d893
7
- data.tar.gz: 384f3639558cc7faa06bbf23d6bcd19ed92248b174eb58da4c812509d404717469bb1906e2dae363c9a182ca9ed444bd15a2a6fe123df7ff97824c2fbc157668
6
+ metadata.gz: 4fb42a8f389bea6fe2be92224ede26f06f3b8328d22c06c2c744a2cac4345ca819fc0c9d1a64af92d899de3f4a94b2d3e0cc96a031fcbec91a877dc808a519c5
7
+ data.tar.gz: aef74afb03695d3be2b5d7bfa15b94b0c5497080c314c135f7c075b07c4a019d03bd88edaa42b5650861758990e001d1dd0aaf94db88131bdeb37485977115d8
data/.version CHANGED
@@ -1 +1 @@
1
- 0.2.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: 'https://twitter.com/@dux',
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: 'mailto:reic.dino@gmail.com',
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://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" })
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/misc/ruby_client.rb" target="capi_ruby">Ruby</a>]
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
- module DocSpecial
5
- extend self
4
+ class DocSpecial
5
+ def initialize api
6
+ @api = api
7
+ end
6
8
 
7
9
  def postman
8
- raw
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$/, '').tableize] = el.opts.filter { |k, _| !unwanted.include?(k.to_s.split('_')[1]) }
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 'clean-hash'
25
+ require 'hash_wia'
25
26
 
26
- require_relative './joshua/params/define'
27
- require_relative './joshua/params/parse'
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