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 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