haveapi 0.3.0
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 +7 -0
- data/lib/haveapi/action.rb +521 -0
- data/lib/haveapi/actions/default.rb +55 -0
- data/lib/haveapi/actions/paginable.rb +12 -0
- data/lib/haveapi/api.rb +66 -0
- data/lib/haveapi/authentication/base.rb +37 -0
- data/lib/haveapi/authentication/basic/provider.rb +37 -0
- data/lib/haveapi/authentication/chain.rb +110 -0
- data/lib/haveapi/authentication/token/provider.rb +166 -0
- data/lib/haveapi/authentication/token/resources.rb +107 -0
- data/lib/haveapi/authorization.rb +108 -0
- data/lib/haveapi/common.rb +38 -0
- data/lib/haveapi/context.rb +78 -0
- data/lib/haveapi/example.rb +36 -0
- data/lib/haveapi/extensions/action_exceptions.rb +25 -0
- data/lib/haveapi/extensions/base.rb +9 -0
- data/lib/haveapi/extensions/resource_prefetch.rb +7 -0
- data/lib/haveapi/hooks.rb +190 -0
- data/lib/haveapi/metadata.rb +56 -0
- data/lib/haveapi/model_adapter.rb +119 -0
- data/lib/haveapi/model_adapters/active_record.rb +352 -0
- data/lib/haveapi/model_adapters/hash.rb +27 -0
- data/lib/haveapi/output_formatter.rb +57 -0
- data/lib/haveapi/output_formatters/base.rb +29 -0
- data/lib/haveapi/output_formatters/json.rb +9 -0
- data/lib/haveapi/params/param.rb +114 -0
- data/lib/haveapi/params/resource.rb +109 -0
- data/lib/haveapi/params.rb +314 -0
- data/lib/haveapi/public/css/bootstrap-theme.min.css +7 -0
- data/lib/haveapi/public/css/bootstrap.min.css +7 -0
- data/lib/haveapi/public/js/bootstrap.min.js +6 -0
- data/lib/haveapi/public/js/jquery-1.11.1.min.js +4 -0
- data/lib/haveapi/resource.rb +120 -0
- data/lib/haveapi/route.rb +22 -0
- data/lib/haveapi/server.rb +440 -0
- data/lib/haveapi/spec/helpers.rb +103 -0
- data/lib/haveapi/types.rb +24 -0
- data/lib/haveapi/version.rb +3 -0
- data/lib/haveapi/views/doc_layout.erb +27 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +20 -0
- data/lib/haveapi/views/doc_sidebars/protocol.erb +42 -0
- data/lib/haveapi/views/index.erb +12 -0
- data/lib/haveapi/views/main_layout.erb +50 -0
- data/lib/haveapi/views/version_page.erb +195 -0
- data/lib/haveapi/views/version_sidebar.erb +42 -0
- data/lib/haveapi.rb +22 -0
- metadata +242 -0
@@ -0,0 +1,440 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
class Server
|
3
|
+
attr_reader :root, :routes, :module_name, :auth_chain, :versions, :default_version,
|
4
|
+
:extensions
|
5
|
+
|
6
|
+
include Hookable
|
7
|
+
|
8
|
+
# Called after the user was authenticated (or not). The block is passed
|
9
|
+
# current user object or nil as an argument.
|
10
|
+
has_hook :post_authenticated
|
11
|
+
|
12
|
+
module ServerHelpers
|
13
|
+
def authenticate!(v)
|
14
|
+
require_auth! unless authenticated?(v)
|
15
|
+
end
|
16
|
+
|
17
|
+
def authenticated?(v)
|
18
|
+
return @current_user if @current_user
|
19
|
+
|
20
|
+
@current_user = settings.api_server.send(:do_authenticate, v, request)
|
21
|
+
settings.api_server.call_hooks_for(:post_authenticated, args: @current_user)
|
22
|
+
@current_user
|
23
|
+
end
|
24
|
+
|
25
|
+
def access_control
|
26
|
+
if request.env['HTTP_ORIGIN'] && request.env['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
|
27
|
+
halt 200, {
|
28
|
+
'Access-Control-Allow-Origin' => '*',
|
29
|
+
'Access-Control-Allow-Methods' => 'GET,POST,OPTIONS,PATCH,PUT,DELETE',
|
30
|
+
'Access-Control-Allow-Credentials' => 'false',
|
31
|
+
'Access-Control-Allow-Headers' => settings.api_server.allowed_headers,
|
32
|
+
'Access-Control-Max-Age' => (60*60).to_s
|
33
|
+
}, ''
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_user
|
38
|
+
@current_user
|
39
|
+
end
|
40
|
+
|
41
|
+
def pretty_format(obj)
|
42
|
+
ret = ''
|
43
|
+
PP.pp(obj, ret)
|
44
|
+
end
|
45
|
+
|
46
|
+
def require_auth!
|
47
|
+
report_error(401, {'WWW-Authenticate' => 'Basic realm="Restricted Area"'},
|
48
|
+
'Action requires user to authenticate')
|
49
|
+
end
|
50
|
+
|
51
|
+
def report_error(code, headers, msg)
|
52
|
+
@halted = true
|
53
|
+
content_type @formatter.content_type, charset: 'utf-8'
|
54
|
+
halt code, headers, @formatter.format(false, nil, msg)
|
55
|
+
end
|
56
|
+
|
57
|
+
def root
|
58
|
+
settings.api_server.root
|
59
|
+
end
|
60
|
+
|
61
|
+
def logout_url
|
62
|
+
ret = url("#{root}_logout")
|
63
|
+
ret.insert(ret.index('//') + 2, '_log:out@')
|
64
|
+
end
|
65
|
+
|
66
|
+
def doc(file)
|
67
|
+
markdown :"../../../doc/#{file}"
|
68
|
+
end
|
69
|
+
|
70
|
+
def version
|
71
|
+
HaveAPI::VERSION
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def initialize(module_name = HaveAPI.module_name)
|
76
|
+
@module_name = module_name
|
77
|
+
@allowed_headers = ['Content-Type']
|
78
|
+
@auth_chain = HaveAPI::Authentication::Chain.new(self)
|
79
|
+
@extensions = []
|
80
|
+
end
|
81
|
+
|
82
|
+
# Include specific version +v+ of API.
|
83
|
+
# +v+ can be one of:
|
84
|
+
# [:all] use all available versions
|
85
|
+
# [Array] use all versions in +Array+
|
86
|
+
# [version] include only concrete version
|
87
|
+
# +default+ is set only when including concrete version. Use
|
88
|
+
# set_default_version otherwise.
|
89
|
+
def use_version(v, default: false)
|
90
|
+
@versions ||= []
|
91
|
+
|
92
|
+
if v == :all
|
93
|
+
@versions = HaveAPI.get_versions(@module_name)
|
94
|
+
elsif v.is_a?(Array)
|
95
|
+
@versions += v
|
96
|
+
@versions.uniq!
|
97
|
+
else
|
98
|
+
@versions << v
|
99
|
+
@default_version = v if default
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Set default version of API.
|
104
|
+
def set_default_version(v)
|
105
|
+
@default_version = v
|
106
|
+
end
|
107
|
+
|
108
|
+
# Load routes for all resource from included API versions.
|
109
|
+
# All routes are mounted under prefix +path+.
|
110
|
+
# If no default version is set, the last included version is used.
|
111
|
+
def mount(prefix='/')
|
112
|
+
@root = prefix
|
113
|
+
|
114
|
+
@sinatra = Sinatra.new do
|
115
|
+
set :views, settings.root + '/views'
|
116
|
+
set :public_folder, settings.root + '/public'
|
117
|
+
set :bind, '0.0.0.0'
|
118
|
+
|
119
|
+
helpers ServerHelpers
|
120
|
+
|
121
|
+
before do
|
122
|
+
@formatter = OutputFormatter.new
|
123
|
+
|
124
|
+
unless @formatter.supports?(request.accept)
|
125
|
+
@halted = true
|
126
|
+
halt 406, "Not Acceptable\n"
|
127
|
+
end
|
128
|
+
|
129
|
+
content_type @formatter.content_type, charset: 'utf-8'
|
130
|
+
|
131
|
+
if request.env['HTTP_ORIGIN']
|
132
|
+
headers 'Access-Control-Allow-Origin' => '*',
|
133
|
+
'Access-Control-Allow-Credentials' => 'false'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
not_found do
|
138
|
+
report_error(404, {}, 'Action not found') unless @halted
|
139
|
+
end
|
140
|
+
|
141
|
+
after do
|
142
|
+
ActiveRecord::Base.clear_active_connections!
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
@sinatra.set(:api_server, self)
|
147
|
+
|
148
|
+
@routes = {}
|
149
|
+
@default_version ||= @versions.last
|
150
|
+
|
151
|
+
# Mount root
|
152
|
+
@sinatra.get @root do
|
153
|
+
authenticated?(settings.api_server.default_version)
|
154
|
+
|
155
|
+
@api = settings.api_server.describe(Context.new(settings.api_server, user: current_user,
|
156
|
+
params: params))
|
157
|
+
|
158
|
+
content_type 'text/html'
|
159
|
+
erb :index, layout: :main_layout
|
160
|
+
end
|
161
|
+
|
162
|
+
@sinatra.options @root do
|
163
|
+
access_control
|
164
|
+
authenticated?(settings.api_server.default_version)
|
165
|
+
ret = nil
|
166
|
+
|
167
|
+
case params[:describe]
|
168
|
+
when 'versions'
|
169
|
+
ret = {versions: settings.api_server.versions,
|
170
|
+
default: settings.api_server.default_version}
|
171
|
+
|
172
|
+
when 'default'
|
173
|
+
ret = settings.api_server.describe_version(Context.new(settings.api_server, version: settings.api_server.default_version,
|
174
|
+
user: current_user, params: params))
|
175
|
+
|
176
|
+
else
|
177
|
+
ret = settings.api_server.describe(Context.new(settings.api_server, user: current_user,
|
178
|
+
params: params))
|
179
|
+
end
|
180
|
+
|
181
|
+
@formatter.format(true, ret)
|
182
|
+
end
|
183
|
+
|
184
|
+
# Doc
|
185
|
+
@sinatra.get "#{@root}doc" do
|
186
|
+
content_type 'text/html'
|
187
|
+
erb :main_layout do
|
188
|
+
doc(:index)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
@sinatra.get "#{@root}doc/readme" do
|
193
|
+
content_type 'text/html'
|
194
|
+
erb :main_layout do
|
195
|
+
GitHub::Markdown.render(File.new(settings.views + '/../../../README.md').read)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
@sinatra.get %r{#{@root}doc/([^\.]+)[\.md]?} do |f|
|
200
|
+
content_type 'text/html'
|
201
|
+
erb :doc_layout, layout: :main_layout do
|
202
|
+
begin
|
203
|
+
@content = doc(f)
|
204
|
+
|
205
|
+
rescue Errno::ENOENT
|
206
|
+
halt 404
|
207
|
+
end
|
208
|
+
|
209
|
+
@sidebar = erb :"doc_sidebars/#{f}"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Login/logout links
|
214
|
+
@sinatra.get "#{root}_login" do
|
215
|
+
if current_user
|
216
|
+
redirect back
|
217
|
+
else
|
218
|
+
authenticate!(settings.api_server.default_version) # FIXME
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
@sinatra.get "#{root}_logout" do
|
223
|
+
require_auth!
|
224
|
+
end
|
225
|
+
|
226
|
+
@auth_chain << HaveAPI.default_authenticate if @auth_chain.empty?
|
227
|
+
@auth_chain.setup(@versions)
|
228
|
+
|
229
|
+
@extensions.each { |e| e.enabled }
|
230
|
+
|
231
|
+
# Mount default version first
|
232
|
+
mount_version(@root, @default_version)
|
233
|
+
|
234
|
+
@versions.each do |v|
|
235
|
+
mount_version(version_prefix(v), v)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def mount_version(prefix, v)
|
240
|
+
@routes[v] ||= {}
|
241
|
+
@routes[v][:resources] = {}
|
242
|
+
|
243
|
+
@sinatra.get prefix do
|
244
|
+
authenticated?(v)
|
245
|
+
|
246
|
+
@v = v
|
247
|
+
@help = settings.api_server.describe_version(Context.new(settings.api_server, version: v,
|
248
|
+
user: current_user, params: params))
|
249
|
+
content_type 'text/html'
|
250
|
+
erb :doc_layout, layout: :main_layout do
|
251
|
+
@content = erb :version_page
|
252
|
+
@sidebar = erb :version_sidebar
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
@sinatra.options prefix do
|
257
|
+
access_control
|
258
|
+
authenticated?(v)
|
259
|
+
|
260
|
+
@formatter.format(true, settings.api_server.describe_version(Context.new(settings.api_server, version: v,
|
261
|
+
user: current_user, params: params)))
|
262
|
+
end
|
263
|
+
|
264
|
+
HaveAPI.get_version_resources(@module_name, v).each do |resource|
|
265
|
+
mount_resource(prefix, v, resource, @routes[v][:resources])
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def mount_resource(prefix, v, resource, hash)
|
270
|
+
hash[resource] = {resources: {}, actions: {}}
|
271
|
+
|
272
|
+
resource.routes(prefix).each do |route|
|
273
|
+
if route.is_a?(Hash)
|
274
|
+
hash[resource][:resources][route.keys.first] = mount_nested_resource(v, route.values.first)
|
275
|
+
|
276
|
+
else
|
277
|
+
hash[resource][:actions][route.action] = route.url
|
278
|
+
mount_action(v, route)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def mount_nested_resource(v, routes)
|
284
|
+
ret = {resources: {}, actions: {}}
|
285
|
+
|
286
|
+
routes.each do |route|
|
287
|
+
if route.is_a?(Hash)
|
288
|
+
ret[:resources][route.keys.first] = mount_nested_resource(v, route.values.first)
|
289
|
+
|
290
|
+
else
|
291
|
+
ret[:actions][route.action] = route.url
|
292
|
+
mount_action(v, route)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
ret
|
297
|
+
end
|
298
|
+
|
299
|
+
def mount_action(v, route)
|
300
|
+
@sinatra.method(route.http_method).call(route.url) do
|
301
|
+
authenticate!(v) if route.action.auth
|
302
|
+
|
303
|
+
request.body.rewind
|
304
|
+
|
305
|
+
begin
|
306
|
+
body = request.body.read
|
307
|
+
|
308
|
+
if body.empty?
|
309
|
+
body = nil
|
310
|
+
else
|
311
|
+
body = JSON.parse(body, symbolize_names: true)
|
312
|
+
end
|
313
|
+
|
314
|
+
rescue => e
|
315
|
+
report_error(400, {}, 'Bad JSON syntax')
|
316
|
+
end
|
317
|
+
|
318
|
+
action = route.action.new(request, v, params, body, Context.new(settings.api_server, version: v,
|
319
|
+
action: route.action, url: route.url,
|
320
|
+
params: params,
|
321
|
+
user: current_user, endpoint: true))
|
322
|
+
|
323
|
+
unless action.authorized?(current_user)
|
324
|
+
report_error(403, {}, 'Access denied. Insufficient permissions.')
|
325
|
+
end
|
326
|
+
|
327
|
+
status, reply, errors = action.safe_exec
|
328
|
+
|
329
|
+
@formatter.format(
|
330
|
+
status,
|
331
|
+
status ? reply : nil,
|
332
|
+
!status ? reply : nil,
|
333
|
+
errors
|
334
|
+
)
|
335
|
+
end
|
336
|
+
|
337
|
+
@sinatra.options route.url do |*args|
|
338
|
+
access_control
|
339
|
+
route_method = route.http_method.to_s.upcase
|
340
|
+
|
341
|
+
pass if params[:method] && params[:method] != route_method
|
342
|
+
|
343
|
+
authenticate!(v) if route.action.auth
|
344
|
+
|
345
|
+
begin
|
346
|
+
desc = route.action.describe(Context.new(settings.api_server, version: v,
|
347
|
+
action: route.action, url: route.url,
|
348
|
+
args: args, params: params,
|
349
|
+
user: current_user, endpoint: true))
|
350
|
+
|
351
|
+
unless desc
|
352
|
+
report_error(403, {}, 'Access denied. Insufficient permissions.')
|
353
|
+
end
|
354
|
+
|
355
|
+
rescue ActiveRecord::RecordNotFound
|
356
|
+
report_error(404, {}, 'Object not found')
|
357
|
+
end
|
358
|
+
|
359
|
+
@formatter.format(true, desc)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def describe(context)
|
364
|
+
context.version = @default_version
|
365
|
+
|
366
|
+
ret = {
|
367
|
+
default_version: @default_version,
|
368
|
+
versions: {default: describe_version(context)},
|
369
|
+
}
|
370
|
+
|
371
|
+
@versions.each do |v|
|
372
|
+
context.version = v
|
373
|
+
ret[:versions][v] = describe_version(context)
|
374
|
+
end
|
375
|
+
|
376
|
+
ret
|
377
|
+
end
|
378
|
+
|
379
|
+
def describe_version(context)
|
380
|
+
ret = {
|
381
|
+
authentication: @auth_chain.describe(context),
|
382
|
+
resources: {},
|
383
|
+
meta: Metadata.describe,
|
384
|
+
help: version_prefix(context.version)
|
385
|
+
}
|
386
|
+
|
387
|
+
#puts JSON.pretty_generate(@routes)
|
388
|
+
|
389
|
+
@routes[context.version][:resources].each do |resource, children|
|
390
|
+
r_name = resource.to_s.demodulize.underscore
|
391
|
+
r_desc = describe_resource(resource, children, context)
|
392
|
+
|
393
|
+
unless r_desc[:actions].empty? && r_desc[:resources].empty?
|
394
|
+
ret[:resources][r_name] = r_desc
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
ret
|
399
|
+
end
|
400
|
+
|
401
|
+
def describe_resource(r, hash, context)
|
402
|
+
r.describe(hash, context)
|
403
|
+
end
|
404
|
+
|
405
|
+
def version_prefix(v)
|
406
|
+
"#{@root}v#{v}/"
|
407
|
+
end
|
408
|
+
|
409
|
+
def add_auth_module(v, name, mod, prefix: '')
|
410
|
+
@routes[v] ||= {authentication: {name => {resources: {}}}}
|
411
|
+
|
412
|
+
HaveAPI.get_version_resources(mod, v).each do |r|
|
413
|
+
mount_resource("#{@root}_auth/#{prefix}/", v, r, @routes[v][:authentication][name][:resources])
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
def allow_header(name)
|
418
|
+
@allowed_headers << name unless @allowed_headers.include?(name)
|
419
|
+
@allowed_headers_str = nil
|
420
|
+
end
|
421
|
+
|
422
|
+
def allowed_headers
|
423
|
+
return @allowed_headers_str if @allowed_headers_str
|
424
|
+
@allowed_headers_str = @allowed_headers.join(',')
|
425
|
+
end
|
426
|
+
|
427
|
+
def app
|
428
|
+
@sinatra
|
429
|
+
end
|
430
|
+
|
431
|
+
def start!
|
432
|
+
@sinatra.run!
|
433
|
+
end
|
434
|
+
|
435
|
+
private
|
436
|
+
def do_authenticate(v, request)
|
437
|
+
@auth_chain.authenticate(v, request)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
|
3
|
+
module HaveAPI
|
4
|
+
# Contains methods for specification of API to be used in +description+ block.
|
5
|
+
module ApiBuilder
|
6
|
+
def auth_chain(chain)
|
7
|
+
@auth_chain = chain
|
8
|
+
end
|
9
|
+
|
10
|
+
def use_version(v)
|
11
|
+
before(:each) do
|
12
|
+
@versions = v
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_version(v)
|
17
|
+
@default_version = v
|
18
|
+
end
|
19
|
+
|
20
|
+
def mount_to(path)
|
21
|
+
@mount = path
|
22
|
+
end
|
23
|
+
|
24
|
+
def login(*credentials)
|
25
|
+
@username, @password = credentials
|
26
|
+
|
27
|
+
before(:each) do
|
28
|
+
basic_authorize(*credentials)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Helper methods for specs.
|
34
|
+
module SpecMethods
|
35
|
+
include Rack::Test::Methods
|
36
|
+
|
37
|
+
# This class wraps raw reply from the API and provides more friendly
|
38
|
+
# interface.
|
39
|
+
class ApiResponse
|
40
|
+
def initialize(body)
|
41
|
+
@data = JSON.parse(body, symbolize_names: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
def status
|
45
|
+
@data[:status]
|
46
|
+
end
|
47
|
+
|
48
|
+
def ok?
|
49
|
+
@data[:status]
|
50
|
+
end
|
51
|
+
|
52
|
+
def failed?
|
53
|
+
!ok?
|
54
|
+
end
|
55
|
+
|
56
|
+
def response
|
57
|
+
@data[:response]
|
58
|
+
end
|
59
|
+
|
60
|
+
def message
|
61
|
+
@data[:message]
|
62
|
+
end
|
63
|
+
|
64
|
+
def errors
|
65
|
+
@data[:errors]
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](k)
|
69
|
+
@data[:response][k]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def app
|
74
|
+
api = HaveAPI::Server.new
|
75
|
+
api.auth_chain << @auth_chain if @auth_chain
|
76
|
+
api.use_version(@versions || :all)
|
77
|
+
api.set_default_version(@default_version) if @default_version
|
78
|
+
api.mount(@mount || '/')
|
79
|
+
api.app
|
80
|
+
end
|
81
|
+
|
82
|
+
# Login with HTTP basic auth.
|
83
|
+
def login(*credentials)
|
84
|
+
basic_authorize(*credentials)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Make API request.
|
88
|
+
# This method is a wrapper for Rack::Test::Methods. Input parameters
|
89
|
+
# are encoded into JSON and sent with correct Content-Type.
|
90
|
+
def api(http_method, url, params={})
|
91
|
+
method(http_method).call(
|
92
|
+
url,
|
93
|
+
params.to_json,
|
94
|
+
{'Content-Type' => 'application/json'}
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Return parsed API response.
|
99
|
+
def api_response
|
100
|
+
@api_response ||= ApiResponse.new(last_response.body)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# Just to represent boolean type in self-description
|
2
|
+
module Boolean
|
3
|
+
def self.to_b(str)
|
4
|
+
return true if str === true
|
5
|
+
return true if str =~ /^(true|t|yes|y|1)$/i
|
6
|
+
|
7
|
+
return false if str === false
|
8
|
+
return false if str =~ /^(false|f|no|n|0)$/i
|
9
|
+
|
10
|
+
false
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Datetime
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
module Custom
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class Text < String
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<% yield %>
|
2
|
+
<div class="row-fluid">
|
3
|
+
<div class="col-sm-10">
|
4
|
+
<%= @content %>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<div class="col-sm-2 table-of-contents">
|
8
|
+
<div class="nav sidebar-nav-fixed" data-spy="affix" data-offset-top="100" data-offset-bottom="200">
|
9
|
+
<%= @sidebar %>
|
10
|
+
</div>
|
11
|
+
</div>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<script type="text/javascript">
|
15
|
+
$(document).ready(function(){
|
16
|
+
|
17
|
+
// Show/hide items in navigation
|
18
|
+
$('ul.top-level ul').css('display', 'none');
|
19
|
+
|
20
|
+
$('.table-of-contents').on('activate.bs.scrollspy', function(){
|
21
|
+
var j = $(this);
|
22
|
+
|
23
|
+
j.find('ul.top-level ul').css('display', 'none');
|
24
|
+
j.find('ul.top-level li.active > ul').css('display', 'block');
|
25
|
+
});
|
26
|
+
});
|
27
|
+
</script>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<h1>Contents</h1>
|
2
|
+
<ul class="top-level">
|
3
|
+
<li><a href="#client-definition">Client definition</a></li>
|
4
|
+
<li><a href="#design-rules">Design rules</a></li>
|
5
|
+
<li>
|
6
|
+
<a href="#necessary-features-to-implement">Necessary features to implement</a>
|
7
|
+
<ul>
|
8
|
+
<li><a href="#resource-tree">Resource tree</a></li>
|
9
|
+
<li><a href="#inputoutput-parameters">Input/output parameters</a></li>
|
10
|
+
<li><a href="#authentication">Authentication</a></li>
|
11
|
+
<li><a href="#object-like-access">Object-like access</a></li>
|
12
|
+
</ul>
|
13
|
+
</li>
|
14
|
+
<li>
|
15
|
+
<a href="#supplemental-features">Supplemental features</a>
|
16
|
+
<ul>
|
17
|
+
<li><a href="#client-sde-validations">Client-side validations</a></li>
|
18
|
+
</ul>
|
19
|
+
</li>
|
20
|
+
</ul>
|
@@ -0,0 +1,42 @@
|
|
1
|
+
<h1>Contents</h1>
|
2
|
+
<ul class="top-level">
|
3
|
+
<li><a href="#protocol-definition">Protocol definition</a></li>
|
4
|
+
<li><a href="#self-description">Self-description</a></li>
|
5
|
+
<li><a href="#envelope">Envelope</a></li>
|
6
|
+
<li>
|
7
|
+
<a href="#description-format">Description format</a>
|
8
|
+
<ul>
|
9
|
+
<li><a href="#version">Version</a></li>
|
10
|
+
<li>
|
11
|
+
<a href="#Authentication">Authentication</a>
|
12
|
+
<ul>
|
13
|
+
<li><a href="#http-basic-authentication">HTTP basic authentication</a></li>
|
14
|
+
<li><a href="#token-authentication">Token authentication</a></li>
|
15
|
+
</ul>
|
16
|
+
</li>
|
17
|
+
<li><a href="#resources">Resources</a></li>
|
18
|
+
<li>
|
19
|
+
<a href="#actions">Actions</a>
|
20
|
+
<ul>
|
21
|
+
<li><a href="#layouts">Layouts</a></li>
|
22
|
+
<li><a href="#namespace">Namespace</a></li>
|
23
|
+
</ul>
|
24
|
+
</li>
|
25
|
+
<li>
|
26
|
+
<a href="#parameters">Parameters</a>
|
27
|
+
<ul>
|
28
|
+
<li><a href="#data-types">Data types</a></li>
|
29
|
+
<li><a href="#resource-associations">Resource associations</a></li>
|
30
|
+
</ul>
|
31
|
+
</li>
|
32
|
+
<li><a href="#examples">Examples</a></li>
|
33
|
+
<li><a href="#list-api-versions">List API versions</a></li>
|
34
|
+
<li><a href="#describe-default-version">Describe default version</a></li>
|
35
|
+
<li><a href="#describe-the-whole-api">Describe the whole API</a></li>
|
36
|
+
</ul>
|
37
|
+
</li>
|
38
|
+
<li><a href="#authorization">Authorization</a></li>
|
39
|
+
<li><a href="#io">Input/output formats</a></li>
|
40
|
+
<li><a href="#request">Request</a></li>
|
41
|
+
<li><a href="#response">Response</a></li>
|
42
|
+
</ul>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<h1>API description</h1>
|
2
|
+
<h2>Available versions:</h2>
|
3
|
+
<ul>
|
4
|
+
<% @api[:versions].each do |v, info| %>
|
5
|
+
<% next if v == :default %>
|
6
|
+
<li>
|
7
|
+
<a href="<%= info[:help] %>">Version <%= v.to_s %></a>
|
8
|
+
<%= '(default)' if v == @api[:default_version] %>
|
9
|
+
</li>
|
10
|
+
<% end %>
|
11
|
+
</ul>
|
12
|
+
<h2><a href="/doc">Documentation</a></h2>
|