haveapi 0.31.1 → 0.32.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 +4 -4
- data/.gitignore +2 -0
- data/benchmarks/documentation.rb +29 -6
- data/benchmarks/endpoints.rb +153 -24
- data/benchmarks/spec/client_examples_spec.rb +109 -37
- data/doc/client-examples.md +218 -45
- data/doc/documentation-oauth.md +157 -0
- data/doc/documentation-ui.md +192 -0
- data/doc/index.md +3 -6
- data/doc/performance.md +39 -12
- data/haveapi.gemspec +1 -1
- data/lib/haveapi/authentication/chain.rb +19 -0
- data/lib/haveapi/authentication/oauth2/provider.rb +7 -1
- data/lib/haveapi/client_example.rb +143 -15
- data/lib/haveapi/client_examples/curl.rb +30 -20
- data/lib/haveapi/client_examples/dynamic_helpers.rb +1 -1
- data/lib/haveapi/client_examples/go_client.rb +9 -2
- data/lib/haveapi/client_examples/http.rb +9 -4
- data/lib/haveapi/client_examples/js_async_client.rb +7 -0
- data/lib/haveapi/client_examples/js_client.rb +47 -36
- data/lib/haveapi/client_examples/php_client.rb +161 -75
- data/lib/haveapi/client_examples/python_async_client.rb +3 -0
- data/lib/haveapi/client_examples/python_client.rb +61 -47
- data/lib/haveapi/client_examples/ruby_cli.rb +27 -18
- data/lib/haveapi/client_examples/ruby_client.rb +80 -31
- data/lib/haveapi/client_examples/rust_client.rb +20 -9
- data/lib/haveapi/client_examples/ts_async_client.rb +7 -0
- data/lib/haveapi/client_examples/ts_client.rb +3 -0
- data/lib/haveapi/client_examples/ts_generated_client.rb +10 -2
- data/lib/haveapi/documentation_context.rb +7 -2
- data/lib/haveapi/documentation_helpers.rb +223 -0
- data/lib/haveapi/documentation_oauth.rb +399 -0
- data/lib/haveapi/documentation_search.rb +225 -0
- data/lib/haveapi/documentation_search_index.rb +79 -0
- data/lib/haveapi/documentation_validators.rb +179 -0
- data/lib/haveapi/public/css/documentation.css +343 -0
- data/lib/haveapi/public/js/documentation.js +992 -0
- data/lib/haveapi/server.rb +144 -86
- data/lib/haveapi/shared_documentation_helpers.rb +54 -0
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +16 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +3 -3
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +1 -1
- data/lib/haveapi/views/doc_sidebars/protocol.erb +5 -5
- data/lib/haveapi/views/documentation/search_form.erb +10 -0
- data/lib/haveapi/views/documentation/search_index.erb +1 -0
- data/lib/haveapi/views/documentation/search_results.erb +20 -0
- data/lib/haveapi/views/index.erb +2 -4
- data/lib/haveapi/views/main_layout.erb +12 -6
- data/lib/haveapi/views/shared_documentation.erb +12 -0
- data/lib/haveapi/views/shared_sidebar.erb +25 -0
- data/lib/haveapi/views/version_page/auth_body.erb +12 -33
- data/lib/haveapi/views/version_page/client_overview.erb +40 -0
- data/lib/haveapi/views/version_page/example_panels.erb +18 -0
- data/lib/haveapi/views/version_page/resource_body.erb +91 -132
- data/lib/haveapi/views/version_page/resource_directory.erb +14 -0
- data/lib/haveapi/views/version_page/usage.erb +7 -0
- data/lib/haveapi/views/version_page.erb +36 -117
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +12 -16
- data/lib/haveapi/views/version_sidebar/auth_resource_nav.erb +19 -0
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +26 -0
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +3 -0
- data/lib/haveapi/views/version_sidebar/brand.erb +31 -0
- data/lib/haveapi/views/version_sidebar/resource_nav.erb +11 -20
- data/lib/haveapi/views/version_sidebar.erb +59 -48
- data/spec/authentication/basic_spec.rb +10 -10
- data/spec/authentication/oauth2_chain_spec.rb +72 -0
- data/spec/authentication/oauth2_token_validation_spec.rb +38 -0
- data/spec/client_examples/assets/documentation_test.cjs +817 -0
- data/spec/client_examples/context_performance_spec.rb +5 -4
- data/spec/client_examples/context_spec.rb +23 -2
- data/spec/client_examples/detail_spec.rb +175 -0
- data/spec/client_examples/dynamic_acceptance.md +59 -26
- data/spec/client_examples/dynamic_acceptance.rb +164 -44
- data/spec/client_examples/dynamic_acceptance_spec.rb +140 -23
- data/spec/client_examples/dynamic_fixture.rb +4 -3
- data/spec/client_examples/dynamic_js_spec.rb +20 -11
- data/spec/client_examples/dynamic_metadata_spec.rb +69 -0
- data/spec/client_examples/dynamic_mounted_paths_spec.rb +9 -7
- data/spec/client_examples/dynamic_python_spec.rb +62 -11
- data/spec/client_examples/go_spec.rb +14 -1
- data/spec/client_examples/presentation_spec.rb +247 -0
- data/spec/client_examples/profile_mount_spec.rb +40 -13
- data/spec/client_examples/rust_client_spec.rb +35 -1
- data/spec/client_examples/shell_spec.rb +268 -0
- data/spec/client_examples/syntax_spec.rb +192 -0
- data/spec/client_examples/ts_generated_client_spec.rb +15 -2
- data/spec/client_examples/typescript_profiles_spec.rb +1 -1
- data/spec/documentation/auth_filtering_spec.rb +9 -4
- data/spec/documentation/canonical_routes_spec.rb +89 -0
- data/spec/documentation/client_examples_spec.rb +39 -5
- data/spec/documentation/client_overview_spec.rb +145 -0
- data/spec/documentation/current_user_html_escaping_spec.rb +7 -2
- data/spec/documentation/example_variants_spec.rb +93 -0
- data/spec/documentation/fixtures/search_index.json +476 -0
- data/spec/documentation/identity_and_guides_spec.rb +130 -0
- data/spec/documentation/identity_context_spec.rb +180 -0
- data/spec/documentation/navigation_spec.rb +296 -0
- data/spec/documentation/oauth_fixture_spec.rb +39 -0
- data/spec/documentation/oauth_spec.rb +912 -0
- data/spec/documentation/oauth_transport_spec.rb +45 -0
- data/spec/documentation/performance_spec.rb +47 -6
- data/spec/documentation/reference_headings_spec.rb +190 -0
- data/spec/documentation/search_index_routes_spec.rb +202 -0
- data/spec/documentation/search_index_spec.rb +85 -0
- data/spec/documentation/search_routes_spec.rb +199 -0
- data/spec/documentation/search_spec.rb +134 -0
- data/spec/documentation/shared_pages_spec.rb +240 -0
- data/spec/documentation/validators_spec.rb +123 -0
- data/spec/documentation_spec.rb +3 -3
- data/spec/packaging_spec.rb +21 -2
- data/spec/performance/documentation_identity_fixture_spec.rb +47 -0
- data/spec/performance/documentation_shapes_spec.rb +97 -0
- data/spec/performance/json_activation_spec.rb +78 -1
- data/spec/performance/release_lineage_spec.rb +4 -3
- data/spec/performance/runner_spec.rb +1 -0
- data/test_support/all_client_performance_fixture.rb +6 -10
- data/test_support/documentation_oauth_fixture.rb +54 -0
- data/test_support/documentation_performance_fixture.rb +19 -1
- data/test_support/dynamic_documentation_api.rb +35 -10
- data/test_support/dynamic_python_login_spec.rb +3 -11
- metadata +52 -3
data/lib/haveapi/server.rb
CHANGED
|
@@ -4,13 +4,18 @@ require 'tilt'
|
|
|
4
4
|
require 'cgi'
|
|
5
5
|
require 'haveapi/hooks'
|
|
6
6
|
require 'haveapi/documentation_clients'
|
|
7
|
+
require 'haveapi/documentation_helpers'
|
|
8
|
+
require 'haveapi/documentation_validators'
|
|
9
|
+
require 'haveapi/documentation_search'
|
|
10
|
+
require 'haveapi/shared_documentation_helpers'
|
|
11
|
+
require 'haveapi/documentation_oauth'
|
|
7
12
|
|
|
8
13
|
module HaveAPI
|
|
9
14
|
class Server
|
|
10
15
|
attr_accessor :default_version, :action_state, :validation_error_http_status,
|
|
11
16
|
:default_locale, :parameter_i18n_scope
|
|
12
17
|
attr_reader :root, :routes, :module_name, :auth_chain, :versions, :extensions,
|
|
13
|
-
:action_state_auth, :locale_header, :available_locales
|
|
18
|
+
:action_state_auth, :locale_header, :available_locales, :documentation_name
|
|
14
19
|
|
|
15
20
|
include Hookable
|
|
16
21
|
|
|
@@ -231,13 +236,8 @@ module HaveAPI
|
|
|
231
236
|
settings.api_server.root
|
|
232
237
|
end
|
|
233
238
|
|
|
234
|
-
def logout_url
|
|
235
|
-
ret = url("#{root}_logout")
|
|
236
|
-
ret.insert(ret.index('//') + 2, '_log:out@')
|
|
237
|
-
end
|
|
238
|
-
|
|
239
239
|
def doc(file)
|
|
240
|
-
markdown :"../../../doc/#{file}"
|
|
240
|
+
markdown :"../../../doc/#{file}", with_toc_data: true, tables: true, fenced_code_blocks: true, lax_spacing: true
|
|
241
241
|
end
|
|
242
242
|
|
|
243
243
|
def base_url
|
|
@@ -260,7 +260,17 @@ module HaveAPI
|
|
|
260
260
|
profile = if client
|
|
261
261
|
settings.api_server.documentation_client_profiles(@v)[client.code&.to_sym]
|
|
262
262
|
end
|
|
263
|
-
@documentation_context
|
|
263
|
+
parent = @documentation_context
|
|
264
|
+
# Snapshot each selected resource once in this request, before client
|
|
265
|
+
# profile/detail derivation, and share it across that resource's panels.
|
|
266
|
+
if changes[:resource] && changes[:resource_path]
|
|
267
|
+
resource = changes.delete(:resource)
|
|
268
|
+
path = changes.delete(:resource_path).map { |part| part.to_s.dup.freeze }.freeze
|
|
269
|
+
@documentation_resource_contexts ||= {}
|
|
270
|
+
key = [resource.__id__, path]
|
|
271
|
+
parent = @documentation_resource_contexts[key] ||= parent.with(resource_path: path, resource:)
|
|
272
|
+
end
|
|
273
|
+
parent.with(profile:, **changes)
|
|
264
274
|
end
|
|
265
275
|
|
|
266
276
|
def documentation_clients
|
|
@@ -274,6 +284,27 @@ module HaveAPI
|
|
|
274
284
|
end
|
|
275
285
|
end
|
|
276
286
|
|
|
287
|
+
def documentation_example_variants(kind, auth_method: nil, auth_description: nil, **changes)
|
|
288
|
+
documentation_clients.flat_map do |client|
|
|
289
|
+
context = documentation_context(client, **changes, detail: :simple)
|
|
290
|
+
simple = client.render(kind, context:, auth_method:, auth_description:)
|
|
291
|
+
next [] unless simple
|
|
292
|
+
|
|
293
|
+
levels = client.detail_levels_for(kind, context:, result: simple, auth_method:, auth_description:)
|
|
294
|
+
unless [[:simple], %i[simple thorough]].include?(levels)
|
|
295
|
+
raise ArgumentError, 'Invalid documentation example detail levels'
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
variants = levels.filter_map do |detail|
|
|
299
|
+
result = detail == :simple ? simple : client.render(kind, context: context.with(detail:), auth_method:, auth_description:)
|
|
300
|
+
[client, detail, result] if result
|
|
301
|
+
end
|
|
302
|
+
@documentation_page_details ||= {}
|
|
303
|
+
@documentation_page_details[client.id] = (@documentation_page_details.fetch(client.id, []) | variants.map { |_client, detail, _result| detail })
|
|
304
|
+
variants
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
277
308
|
def host
|
|
278
309
|
request.env['HTTP_HOST'].split(':').first
|
|
279
310
|
end
|
|
@@ -299,27 +330,15 @@ module HaveAPI
|
|
|
299
330
|
def format_param_type(param)
|
|
300
331
|
return param[:type] if param[:type] != 'Resource'
|
|
301
332
|
|
|
302
|
-
|
|
303
|
-
|
|
333
|
+
entry = documentation_resources.find { |resource| resource[:path] == param[:resource].map(&:to_s) }
|
|
334
|
+
return param[:type] unless entry
|
|
304
335
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
validators.each do |name, opts|
|
|
310
|
-
ret += "<h5>#{escape_html(name.to_s.capitalize)}</h5>"
|
|
311
|
-
ret += '<dl>'
|
|
312
|
-
if opts.respond_to?(:each_pair)
|
|
313
|
-
opts.each_pair do |k, v|
|
|
314
|
-
ret += "<dt>#{escape_html(k)}</dt><dd>#{escape_html(v.to_s)}</dd>"
|
|
315
|
-
end
|
|
316
|
-
else
|
|
317
|
-
ret += "<dt>description</dt><dd>#{escape_html(opts.to_s)}</dd>"
|
|
318
|
-
end
|
|
319
|
-
ret += '</dl>'
|
|
320
|
-
end
|
|
336
|
+
anchor = entry[:info][:actions].has_key?('show') || entry[:info][:actions].has_key?(:show) ? "#{entry[:id]}-show" : entry[:id]
|
|
337
|
+
"<a href=\"#{escape_html(documentation_url(resource: entry[:key], anchor:))}\">#{param[:type]}</a>"
|
|
338
|
+
end
|
|
321
339
|
|
|
322
|
-
|
|
340
|
+
def format_validators(validators, type: nil)
|
|
341
|
+
DocumentationValidators.render(validators, type:)
|
|
323
342
|
end
|
|
324
343
|
|
|
325
344
|
def escape_html(v)
|
|
@@ -342,6 +361,38 @@ module HaveAPI
|
|
|
342
361
|
self.locale_header = 'Accept-Language'
|
|
343
362
|
end
|
|
344
363
|
|
|
364
|
+
def documentation_name=(value)
|
|
365
|
+
raise ArgumentError, 'Documentation name must be configured before mount' if @sinatra
|
|
366
|
+
|
|
367
|
+
if value.nil?
|
|
368
|
+
@documentation_name = nil
|
|
369
|
+
return
|
|
370
|
+
end
|
|
371
|
+
unless value.is_a?(String)
|
|
372
|
+
raise ArgumentError, 'Documentation name must be a UTF-8 string'
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
name = value.dup.force_encoding(Encoding::UTF_8)
|
|
376
|
+
unless name.valid_encoding? && !name.match?(/[[:cntrl:]]/)
|
|
377
|
+
raise ArgumentError, 'Documentation name must be a UTF-8 string without control characters'
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
name = name.strip
|
|
381
|
+
unless name.bytesize.between?(1, 256)
|
|
382
|
+
raise ArgumentError, 'Documentation name must contain 1 to 256 bytes'
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
@documentation_name = name.freeze
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def documentation_oauth(**options)
|
|
389
|
+
return @documentation_oauth if options.empty?
|
|
390
|
+
raise ArgumentError, 'Documentation OAuth is already configured' if @documentation_oauth
|
|
391
|
+
raise ArgumentError, 'Documentation OAuth must be configured before mount' if @sinatra
|
|
392
|
+
|
|
393
|
+
@documentation_oauth = DocumentationOAuth.new(**options)
|
|
394
|
+
end
|
|
395
|
+
|
|
345
396
|
def documentation_clients(version, **profiles)
|
|
346
397
|
@documentation_clients.register(version, **profiles)
|
|
347
398
|
end
|
|
@@ -461,6 +512,10 @@ module HaveAPI
|
|
|
461
512
|
helpers Sinatra::Cookies
|
|
462
513
|
helpers ServerHelpers
|
|
463
514
|
helpers DocHelpers
|
|
515
|
+
helpers DocumentationHelpers
|
|
516
|
+
helpers DocumentationSearch::Helpers
|
|
517
|
+
helpers SharedDocumentationHelpers
|
|
518
|
+
helpers DocumentationOAuth::Helpers
|
|
464
519
|
|
|
465
520
|
before do
|
|
466
521
|
setup_request_locale
|
|
@@ -496,7 +551,15 @@ module HaveAPI
|
|
|
496
551
|
|
|
497
552
|
# Mount root
|
|
498
553
|
@sinatra.get @root do
|
|
499
|
-
|
|
554
|
+
if documentation_selection?
|
|
555
|
+
next render_version_documentation(settings.api_server.default_version)
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
headers 'Cache-Control' => 'private, no-store'
|
|
559
|
+
single_version = settings.api_server.versions.length == 1
|
|
560
|
+
auth_users_by_version = settings.api_server.versions.to_h do |version|
|
|
561
|
+
[version, documentation_authenticated?(version)]
|
|
562
|
+
end
|
|
500
563
|
|
|
501
564
|
@api = settings.api_server.describe(Context.new(
|
|
502
565
|
settings.api_server,
|
|
@@ -505,8 +568,16 @@ module HaveAPI
|
|
|
505
568
|
auth_users_by_version:
|
|
506
569
|
))
|
|
507
570
|
|
|
571
|
+
if single_version
|
|
572
|
+
version = settings.api_server.default_version
|
|
573
|
+
@documentation_index_account = {
|
|
574
|
+
version:, user: auth_users_by_version[version],
|
|
575
|
+
authentication: @api[:versions].fetch(version).fetch(:authentication)
|
|
576
|
+
}
|
|
577
|
+
end
|
|
578
|
+
|
|
508
579
|
content_type 'text/html'
|
|
509
|
-
|
|
580
|
+
render_documentation_index
|
|
510
581
|
end
|
|
511
582
|
|
|
512
583
|
@sinatra.options @root do
|
|
@@ -548,67 +619,51 @@ module HaveAPI
|
|
|
548
619
|
@formatter.format(true, ret)
|
|
549
620
|
end
|
|
550
621
|
|
|
551
|
-
#
|
|
622
|
+
# Shared documentation keeps the existing content and URL aliases.
|
|
552
623
|
@sinatra.get "#{@root}doc" do
|
|
553
|
-
|
|
554
|
-
erb :main_layout do
|
|
555
|
-
doc(:index)
|
|
556
|
-
end
|
|
624
|
+
render_shared_readme
|
|
557
625
|
end
|
|
558
626
|
|
|
559
627
|
@sinatra.get "#{@root}doc/readme" do
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
erb :main_layout do
|
|
563
|
-
markdown File.new("#{settings.views}/../../../README.md").read
|
|
564
|
-
end
|
|
628
|
+
render_shared_readme
|
|
565
629
|
end
|
|
566
630
|
|
|
567
631
|
@sinatra.get "#{@root}doc/json-schema" do
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
rescue Errno::ENOENT
|
|
573
|
-
halt 404
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
@sidebar = erb :'doc_sidebars/json-schema'
|
|
632
|
+
begin
|
|
633
|
+
content = File.read(File.join(settings.root, '../../doc/json-schema.html'))
|
|
634
|
+
rescue Errno::ENOENT
|
|
635
|
+
halt 404
|
|
577
636
|
end
|
|
637
|
+
render_shared_documentation(content:, title: 'JSON schema', sidebar: erb(:'doc_sidebars/json-schema'))
|
|
578
638
|
end
|
|
579
639
|
|
|
580
|
-
@sinatra.get %r{#{@root}doc/([^.]+)(\.md)?} do |
|
|
581
|
-
|
|
582
|
-
erb :doc_layout, layout: :main_layout do
|
|
583
|
-
begin
|
|
584
|
-
@content = doc(f)
|
|
585
|
-
rescue Errno::ENOENT
|
|
586
|
-
halt 404
|
|
587
|
-
end
|
|
640
|
+
@sinatra.get %r{#{@root}doc/([^.]+)(\.md)?} do |file, _|
|
|
641
|
+
next render_shared_readme if file == 'index'
|
|
588
642
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
end
|
|
643
|
+
begin
|
|
644
|
+
content = doc(file)
|
|
645
|
+
rescue Errno::ENOENT
|
|
646
|
+
halt 404
|
|
594
647
|
end
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if current_user
|
|
600
|
-
redirect back
|
|
601
|
-
else
|
|
602
|
-
authenticate!(settings.api_server.default_version) # FIXME
|
|
648
|
+
sidebar = begin
|
|
649
|
+
erb :"doc_sidebars/#{file}"
|
|
650
|
+
rescue Errno::ENOENT
|
|
651
|
+
nil
|
|
603
652
|
end
|
|
653
|
+
render_shared_documentation(content:, title: file.humanize, sidebar:)
|
|
604
654
|
end
|
|
605
655
|
|
|
606
|
-
|
|
607
|
-
|
|
656
|
+
# Retain old documentation links without engaging browser credentials.
|
|
657
|
+
%w[_login _logout].each do |path|
|
|
658
|
+
@sinatra.get "#{root}#{path}" do
|
|
659
|
+
headers 'Cache-Control' => 'private, no-store'
|
|
660
|
+
halt 303, { 'Location' => url(settings.api_server.version_prefix(settings.api_server.default_version), false) }, ''
|
|
661
|
+
end
|
|
608
662
|
end
|
|
609
663
|
|
|
610
664
|
@auth_chain << HaveAPI.default_authenticate if @auth_chain.empty?
|
|
611
665
|
@auth_chain.setup(@versions)
|
|
666
|
+
documentation_oauth&.mount(self, @sinatra)
|
|
612
667
|
|
|
613
668
|
@extensions.each { |e| e.enabled(self) }
|
|
614
669
|
|
|
@@ -618,6 +673,7 @@ module HaveAPI
|
|
|
618
673
|
mount_version(@root, @default_version)
|
|
619
674
|
|
|
620
675
|
@versions.each do |v|
|
|
676
|
+
mount_documentation_redirect(v)
|
|
621
677
|
mount_version(version_prefix(v), v)
|
|
622
678
|
end
|
|
623
679
|
|
|
@@ -629,26 +685,28 @@ module HaveAPI
|
|
|
629
685
|
raise
|
|
630
686
|
end
|
|
631
687
|
|
|
688
|
+
def mount_documentation_redirect(version)
|
|
689
|
+
prefix = version_prefix(version)
|
|
690
|
+
slashless = prefix.chomp('/')
|
|
691
|
+
@sinatra.get slashless do
|
|
692
|
+
pass unless request.path_info == slashless
|
|
693
|
+
|
|
694
|
+
headers 'Cache-Control' => 'private, no-store'
|
|
695
|
+
query = request.query_string
|
|
696
|
+
halt 400, 'Invalid documentation query' if query.bytes.any? { |byte| byte < 32 || byte == 127 }
|
|
697
|
+
destination = url(prefix, false)
|
|
698
|
+
destination += "?#{query}" unless query.empty?
|
|
699
|
+
halt 308, { 'Location' => destination }, ''
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
private :mount_documentation_redirect
|
|
703
|
+
|
|
632
704
|
def mount_version(prefix, v)
|
|
633
705
|
@routes[v] ||= {}
|
|
634
706
|
@routes[v][:resources] = {}
|
|
635
707
|
|
|
636
708
|
@sinatra.get prefix do
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
@v = v
|
|
640
|
-
@help = settings.api_server.describe_version(Context.new(
|
|
641
|
-
settings.api_server,
|
|
642
|
-
version: v,
|
|
643
|
-
user: current_user,
|
|
644
|
-
params:
|
|
645
|
-
))
|
|
646
|
-
|
|
647
|
-
content_type 'text/html'
|
|
648
|
-
erb :doc_layout, layout: :main_layout do
|
|
649
|
-
@content = erb :version_page
|
|
650
|
-
@sidebar = erb :version_sidebar
|
|
651
|
-
end
|
|
709
|
+
render_version_documentation(v)
|
|
652
710
|
end
|
|
653
711
|
|
|
654
712
|
@sinatra.options prefix do
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module HaveAPI
|
|
2
|
+
module SharedDocumentationHelpers
|
|
3
|
+
def render_documentation_index
|
|
4
|
+
render_neutral_documentation(content: erb(:index), title: 'API versions', page: 'versions')
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def render_shared_documentation(content:, title:, sidebar: nil)
|
|
8
|
+
render_neutral_documentation(content:, title:, sidebar:, page: 'guide')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render_shared_readme
|
|
12
|
+
content = markdown(File.read("#{settings.views}/../../../README.md"),
|
|
13
|
+
with_toc_data: true, tables: true, fenced_code_blocks: true, lax_spacing: true)
|
|
14
|
+
render_shared_documentation(content:, title: 'HaveAPI internals')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def shared_documentation_url(path = '')
|
|
18
|
+
url("#{root}#{path}", false)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def shared_documentation_reference_url(page: nil)
|
|
22
|
+
version = settings.api_server.default_version
|
|
23
|
+
return if version.nil?
|
|
24
|
+
|
|
25
|
+
path = settings.api_server.version_prefix(version)
|
|
26
|
+
path += "?page=#{CGI.escape(page)}" if page
|
|
27
|
+
url(path, false)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def render_neutral_documentation(content:, title:, page:, sidebar: nil)
|
|
33
|
+
headers 'Cache-Control' => 'private, no-store'
|
|
34
|
+
@documentation_ui = true
|
|
35
|
+
@documentation_page = page
|
|
36
|
+
@documentation_title = title
|
|
37
|
+
@documentation_resource = nil
|
|
38
|
+
@v = nil
|
|
39
|
+
@documentation_toc = sidebar
|
|
40
|
+
content_type 'text/html'
|
|
41
|
+
erb :doc_layout, layout: :main_layout do
|
|
42
|
+
@content = erb :shared_documentation, locals: { content: shared_documentation_tables(content), title: }
|
|
43
|
+
@sidebar = erb :shared_sidebar
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def shared_documentation_tables(content)
|
|
48
|
+
# Trusted repository HTML is rendered before this presentation-only pass.
|
|
49
|
+
content.gsub(%r{<table\b[^>]*>.*?</table>}m) do |table|
|
|
50
|
+
%(<div class="docs-table-scroll" role="region" aria-label="Documentation table" tabindex="0">#{table}</div>)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/haveapi/version.rb
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
<% yield %>
|
|
2
|
+
<% if @documentation_ui %>
|
|
3
|
+
<div data-docs data-page="<%= @documentation_page %>" data-resource="<%= escape_html(@documentation_resource && @documentation_resource[:key]) %>">
|
|
4
|
+
<a class="docs-skip-link" href="#docs-main">Skip to content</a>
|
|
5
|
+
<div class="docs-mobile-toolbar">
|
|
6
|
+
<button type="button" data-nav-toggle aria-controls="docs-navigation" aria-expanded="false">Navigation</button>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="docs-layout">
|
|
9
|
+
<aside class="docs-sidebar" id="docs-navigation" data-docs-sidebar><%= @sidebar %></aside>
|
|
10
|
+
<main class="docs-main" id="docs-main" tabindex="-1"><%= @content %></main>
|
|
11
|
+
</div>
|
|
12
|
+
<script type="application/json" data-documentation-clients><%= documentation_json(@help ? documentation_client_metadata : []) %></script>
|
|
13
|
+
<script type="application/json" data-legacy-anchors><%= documentation_json(@documentation_page == 'overview' ? documentation_legacy_anchors : {}) %></script>
|
|
14
|
+
</div>
|
|
15
|
+
<% else %>
|
|
2
16
|
<div class="row-fluid">
|
|
3
17
|
<div class="col-sm-10">
|
|
4
18
|
<%= @content %>
|
|
@@ -25,3 +39,5 @@
|
|
|
25
39
|
});
|
|
26
40
|
});
|
|
27
41
|
</script>
|
|
42
|
+
|
|
43
|
+
<% end %>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<
|
|
1
|
+
<h2>Contents</h2>
|
|
2
2
|
<ul class="top-level">
|
|
3
3
|
<li><a href="#client-definition">Client definition</a></li>
|
|
4
4
|
<li><a href="#design-rules">Design rules</a></li>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<a href="#necessary-features-to-implement">Necessary features to implement</a>
|
|
7
7
|
<ul>
|
|
8
8
|
<li><a href="#resource-tree">Resource tree</a></li>
|
|
9
|
-
<li><a href="#
|
|
9
|
+
<li><a href="#input-output-parameters">Input/output parameters</a></li>
|
|
10
10
|
<li><a href="#authentication">Authentication</a></li>
|
|
11
11
|
<li><a href="#object-like-access">Object-like access</a></li>
|
|
12
12
|
</ul>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<li>
|
|
15
15
|
<a href="#supplemental-features">Supplemental features</a>
|
|
16
16
|
<ul>
|
|
17
|
-
<li><a href="#client-
|
|
17
|
+
<li><a href="#client-side-validations">Client-side validations</a></li>
|
|
18
18
|
</ul>
|
|
19
19
|
</li>
|
|
20
20
|
</ul>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<
|
|
1
|
+
<h2>Contents</h2>
|
|
2
2
|
<ul class="top-level">
|
|
3
3
|
<li><a href="#protocol-definition">Protocol definition</a></li>
|
|
4
4
|
<li><a href="#self-description">Self-description</a></li>
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<li>
|
|
7
7
|
<a href="#description-format">Description format</a>
|
|
8
8
|
<ul>
|
|
9
|
-
<li><a href="#version">Version</a></li>
|
|
9
|
+
<li><a href="#api-version">Version</a></li>
|
|
10
10
|
<li>
|
|
11
|
-
<a href="#
|
|
11
|
+
<a href="#authentication">Authentication</a>
|
|
12
12
|
<ul>
|
|
13
13
|
<li><a href="#http-basic-authentication">HTTP basic authentication</a></li>
|
|
14
14
|
<li><a href="#token-authentication">Token authentication</a></li>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<a href="#parameters">Parameters</a>
|
|
27
27
|
<ul>
|
|
28
28
|
<li><a href="#data-types">Data types</a></li>
|
|
29
|
-
<li><a href="#resource-
|
|
29
|
+
<li><a href="#resource-association">Resource associations</a></li>
|
|
30
30
|
</ul>
|
|
31
31
|
</li>
|
|
32
32
|
<li><a href="#examples">Examples</a></li>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
</ul>
|
|
37
37
|
</li>
|
|
38
38
|
<li><a href="#authorization">Authorization</a></li>
|
|
39
|
-
<li><a href="#
|
|
39
|
+
<li><a href="#input-output-formats">Input/output formats</a></li>
|
|
40
40
|
<li><a href="#request">Request</a></li>
|
|
41
41
|
<li><a href="#response">Response</a></li>
|
|
42
42
|
</ul>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<form data-documentation-search method="get" action="<%= escape_html(action) %>" data-search-version="<%= escape_html(version) %>" data-search-index-url="<%= escape_html(index_url) %>" data-search-view-key="<%= escape_html(view_key) %>">
|
|
2
|
+
<input type="hidden" name="page" value="resources">
|
|
3
|
+
<label for="<%= escape_html(id) %>-query">Search API v<%= escape_html(version) %></label>
|
|
4
|
+
<input type="search" name="q" data-search-query id="<%= escape_html(id) %>-query" value="<%= escape_html(query) %>" placeholder="Resource..." maxlength="256">
|
|
5
|
+
<button type="submit">Search</button>
|
|
6
|
+
<div data-search-suggestions role="region" aria-label="Search suggestions" hidden>
|
|
7
|
+
<p data-search-status role="status"></p>
|
|
8
|
+
<div data-search-preview></div>
|
|
9
|
+
</div>
|
|
10
|
+
</form>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<script type="application/json" data-documentation-search-index><%= documentation_json(key: search_index.key, index: search_index.payload) %></script>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<section data-search-results aria-label="Search results">
|
|
2
|
+
<h2>Search results for <%= escape_html(search[:query]) %></h2>
|
|
3
|
+
<p data-search-status role="status">
|
|
4
|
+
<% if search[:results].empty? %>
|
|
5
|
+
No matches. Try another resource, action or parameter name.
|
|
6
|
+
<% elsif search[:more] %>
|
|
7
|
+
Showing <%= search[:results].length %> results. More matches are available; refine your search.
|
|
8
|
+
<% else %>
|
|
9
|
+
<%= search[:results].length %> <%= search[:results].length == 1 ? 'result' : 'results' %>.
|
|
10
|
+
<% end %>
|
|
11
|
+
</p>
|
|
12
|
+
<ol>
|
|
13
|
+
<% search[:results].each do |result| %>
|
|
14
|
+
<li>
|
|
15
|
+
<a href="<%= escape_html(documentation_search_result_url(result)) %>"><%= escape_html(result[:kind].to_s.capitalize) %>: <code><%= escape_html(documentation_search_result_title(result)) %></code></a>
|
|
16
|
+
<% documentation_search_result_context(result).each do |context| %><span><%= escape_html(context) %></span><% end %>
|
|
17
|
+
</li>
|
|
18
|
+
<% end %>
|
|
19
|
+
</ol>
|
|
20
|
+
</section>
|
data/lib/haveapi/views/index.erb
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
<h1>API
|
|
2
|
-
<h2>Available versions:</h2>
|
|
1
|
+
<h1>API versions</h1>
|
|
3
2
|
<ul>
|
|
4
3
|
<% @api[:versions].each do |v, info| %>
|
|
5
4
|
<% next if v == :default %>
|
|
6
5
|
<li>
|
|
7
|
-
<a href="<%= info[:help] %>">Version <%= v
|
|
6
|
+
<a href="<%= escape_html(url(info[:help], false)) %>">Version <%= escape_html(v) %></a>
|
|
8
7
|
<%= '(default)' if v == @api[:default_version] %>
|
|
9
8
|
</li>
|
|
10
9
|
<% end %>
|
|
11
10
|
</ul>
|
|
12
|
-
<h2><a href="/doc">Documentation</a></h2>
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
|
-
<
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title><%= escape_html(documentation_document_title) %></title>
|
|
7
|
+
<% if @documentation_ui %>
|
|
8
|
+
<script src="<%= escape_html(url('/js/documentation.js', false)) %>" data-highlighter-url="<%= escape_html(url('/js/highlighter.js', false)) %>"></script>
|
|
9
|
+
<link rel="stylesheet" href="<%= escape_html(url('/css/documentation.css', false)) %>">
|
|
10
|
+
<% else %>
|
|
6
11
|
|
|
7
12
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
|
8
13
|
<script src="/js/jquery-1.11.1.min.js"></script>
|
|
@@ -73,16 +78,17 @@
|
|
|
73
78
|
white-space: pre;
|
|
74
79
|
}
|
|
75
80
|
</style>
|
|
81
|
+
<% end %>
|
|
76
82
|
</head>
|
|
77
|
-
<body data-spy="scroll" data-target=".table-of-contents"
|
|
78
|
-
<div class="container-fluid">
|
|
83
|
+
<body<% unless @documentation_ui %> data-spy="scroll" data-target=".table-of-contents"<% end %>>
|
|
84
|
+
<div class="<%= @documentation_ui ? 'docs-container' : 'container-fluid' %>">
|
|
79
85
|
<%= yield %>
|
|
80
86
|
|
|
81
|
-
<
|
|
82
|
-
<div
|
|
87
|
+
<footer class="docs-footer">
|
|
88
|
+
<div>
|
|
83
89
|
<p class="text-right">Generated by <a href="https://github.com/vpsfreecz/haveapi">HaveAPI</a> v<%= version %></p>
|
|
84
90
|
</div>
|
|
85
|
-
</
|
|
91
|
+
</footer>
|
|
86
92
|
</div>
|
|
87
93
|
</body>
|
|
88
94
|
</html>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<% if @documentation_page == 'guide' %>
|
|
2
|
+
<nav aria-label="Breadcrumb">
|
|
3
|
+
<ol>
|
|
4
|
+
<li><a href="<%= escape_html(shared_documentation_url) %>">API versions</a></li>
|
|
5
|
+
<% unless title == 'HaveAPI internals' %>
|
|
6
|
+
<li><a href="<%= escape_html(shared_documentation_url('doc')) %>">HaveAPI internals</a></li>
|
|
7
|
+
<% end %>
|
|
8
|
+
<li aria-current="page"><%= escape_html(title) %></li>
|
|
9
|
+
</ol>
|
|
10
|
+
</nav>
|
|
11
|
+
<% end %>
|
|
12
|
+
<%= content %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%= erb :'version_sidebar/brand', locals: {account: documentation_account_context} %>
|
|
2
|
+
<% if (search_options = documentation_search_form_options(id: 'docs-search-sidebar')) %>
|
|
3
|
+
<%= erb :'documentation/search_form', locals: search_options %>
|
|
4
|
+
<% end %>
|
|
5
|
+
<nav aria-label="Documentation pages">
|
|
6
|
+
<ul>
|
|
7
|
+
<li><a href="<%= escape_html(shared_documentation_url) %>"<% if @documentation_page == 'versions' %> aria-current="page"<% end %>>API versions</a></li>
|
|
8
|
+
<% if (reference = shared_documentation_reference_url) %>
|
|
9
|
+
<li><a href="<%= escape_html(reference) %>">API reference</a></li>
|
|
10
|
+
<li><a href="<%= escape_html(shared_documentation_reference_url(page: 'resources')) %>">Resources</a></li>
|
|
11
|
+
<% end %>
|
|
12
|
+
</ul>
|
|
13
|
+
</nav>
|
|
14
|
+
<% if @documentation_toc && !@documentation_toc.empty? %>
|
|
15
|
+
<nav aria-label="Contents" data-action-nav><%= @documentation_toc %></nav>
|
|
16
|
+
<% end %>
|
|
17
|
+
<nav aria-label="Shared documentation">
|
|
18
|
+
<h2><a href="<%= escape_html(shared_documentation_url('doc')) %>">HaveAPI internals</a></h2>
|
|
19
|
+
<ul>
|
|
20
|
+
<li><a href="<%= escape_html(shared_documentation_url('doc/protocol')) %>">Protocol</a></li>
|
|
21
|
+
<li><a href="<%= escape_html(shared_documentation_url('doc/json-schema')) %>">JSON schema</a></li>
|
|
22
|
+
<li><a href="<%= escape_html(shared_documentation_url('doc/create-client')) %>">Create a client</a></li>
|
|
23
|
+
<li><a href="<%= escape_html(shared_documentation_url('doc/client-examples')) %>">Client examples</a></li>
|
|
24
|
+
</ul>
|
|
25
|
+
</nav>
|