haveapi 0.32.1 → 0.32.2
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 +3 -8
- data/README.md +3 -422
- data/Rakefile +17 -7
- data/benchmarks/performance_compare.rb +27 -6
- data/doc/cs/client-examples.md +535 -0
- data/doc/cs/create-client.md +118 -0
- data/doc/cs/json-schema.html +1182 -0
- data/doc/cs/protocol.md +595 -0
- data/doc/cs/readme.md +432 -0
- data/doc/cs/typed-input-validation.md +122 -0
- data/doc/{client-examples.md → en/client-examples.md} +20 -10
- data/doc/{documentation-ui.md → en/documentation-ui.md} +42 -6
- data/doc/{performance.md → en/performance.md} +11 -0
- data/doc/{protocol.md → en/protocol.md} +2 -2
- data/doc/en/readme.md +423 -0
- data/haveapi.gemspec +2 -2
- data/lib/haveapi/client_example.rb +18 -5
- data/lib/haveapi/client_examples/curl.rb +5 -5
- data/lib/haveapi/client_examples/dynamic_helpers.rb +10 -10
- data/lib/haveapi/client_examples/go_client.rb +1 -1
- data/lib/haveapi/client_examples/http.rb +4 -4
- data/lib/haveapi/client_examples/js_client.rb +10 -10
- data/lib/haveapi/client_examples/php_client.rb +24 -25
- data/lib/haveapi/client_examples/python_client.rb +11 -11
- data/lib/haveapi/client_examples/ruby_cli.rb +6 -9
- data/lib/haveapi/client_examples/ruby_client.rb +24 -16
- data/lib/haveapi/client_examples/rust_client.rb +1 -1
- data/lib/haveapi/client_examples/ts_generated_client.rb +1 -1
- data/lib/haveapi/documentation_context.rb +14 -2
- data/lib/haveapi/documentation_helpers.rb +92 -15
- data/lib/haveapi/documentation_locale.rb +65 -0
- data/lib/haveapi/documentation_oauth.rb +12 -12
- data/lib/haveapi/documentation_search.rb +5 -5
- data/lib/haveapi/documentation_validators.rb +43 -33
- data/lib/haveapi/locales/cs.yml +468 -0
- data/lib/haveapi/locales/en.yml +467 -0
- data/lib/haveapi/public/css/documentation.css +8 -4
- data/lib/haveapi/public/js/documentation.js +73 -20
- data/lib/haveapi/server.rb +32 -47
- data/lib/haveapi/shared_documentation_helpers.rb +53 -5
- data/lib/haveapi/version.rb +1 -1
- data/lib/haveapi/views/doc_layout.erb +3 -2
- data/lib/haveapi/views/doc_sidebars/create-client.erb +10 -10
- data/lib/haveapi/views/doc_sidebars/json-schema.erb +2 -2
- data/lib/haveapi/views/doc_sidebars/protocol.erb +24 -24
- data/lib/haveapi/views/documentation/search_form.erb +5 -4
- data/lib/haveapi/views/documentation/search_results.erb +6 -6
- data/lib/haveapi/views/index.erb +3 -3
- data/lib/haveapi/views/main_layout.erb +4 -4
- data/lib/haveapi/views/shared_documentation.erb +8 -5
- data/lib/haveapi/views/shared_sidebar.erb +11 -11
- data/lib/haveapi/views/version_page/auth_body.erb +7 -7
- data/lib/haveapi/views/version_page/client_auth.erb +1 -1
- data/lib/haveapi/views/version_page/client_example.erb +1 -1
- data/lib/haveapi/views/version_page/client_init.erb +1 -1
- data/lib/haveapi/views/version_page/client_overview.erb +7 -5
- data/lib/haveapi/views/version_page/example_panels.erb +5 -2
- data/lib/haveapi/views/version_page/resource_body.erb +24 -24
- data/lib/haveapi/views/version_page/resource_directory.erb +2 -2
- data/lib/haveapi/views/version_page/usage.erb +5 -5
- data/lib/haveapi/views/version_page.erb +11 -11
- data/lib/haveapi/views/version_sidebar/auth_nav.erb +2 -2
- data/lib/haveapi/views/version_sidebar/authentication_control.erb +8 -8
- data/lib/haveapi/views/version_sidebar/authentication_status.erb +1 -1
- data/lib/haveapi/views/version_sidebar/brand.erb +16 -5
- data/lib/haveapi/views/version_sidebar.erb +33 -18
- data/spec/client_examples/assets/documentation_test.cjs +107 -9
- data/spec/client_examples/comment_capture.rb +72 -0
- data/spec/client_examples/comments_spec.rb +162 -0
- data/spec/client_examples/context_spec.rb +18 -2
- data/spec/client_examples/detail_spec.rb +68 -0
- data/spec/documentation/client_overview_spec.rb +60 -0
- data/spec/documentation/example_selection_spec.rb +192 -0
- data/spec/documentation/language_settings_spec.rb +132 -0
- data/spec/documentation/locale_spec.rb +367 -0
- data/spec/documentation/navigation_spec.rb +61 -3
- data/spec/documentation/oauth_spec.rb +42 -0
- data/spec/documentation/search_index_routes_spec.rb +8 -4
- data/spec/documentation/search_spec.rb +1 -1
- data/spec/documentation/shared_pages_spec.rb +63 -2
- data/spec/documentation/validators_spec.rb +25 -0
- data/spec/extensions/exception_mailer_spec.rb +2 -2
- data/spec/packaging_spec.rb +68 -9
- data/spec/performance/event_baseline_spec.rb +1 -1
- data/spec/performance/source_versions_spec.rb +111 -2
- metadata +24 -11
- /data/doc/{create-client.md → en/create-client.md} +0 -0
- /data/doc/{documentation-oauth.md → en/documentation-oauth.md} +0 -0
- /data/doc/{json-schema.html → en/json-schema.html} +0 -0
- /data/doc/{typed-input-validation.md → en/typed-input-validation.md} +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
require 'json'
|
|
2
2
|
require 'uri'
|
|
3
|
+
require 'digest/sha2'
|
|
3
4
|
|
|
4
5
|
module HaveAPI
|
|
5
6
|
# HTML navigation is derived only from this request's authorized description.
|
|
6
7
|
module DocumentationHelpers
|
|
8
|
+
ASSET_DIGESTS = %w[js/documentation.js css/documentation.css].to_h do |asset|
|
|
9
|
+
[asset, Digest::SHA256.file(File.join(__dir__, 'public', asset)).hexdigest.freeze]
|
|
10
|
+
end.freeze
|
|
11
|
+
|
|
7
12
|
def documentation_identity_name
|
|
8
13
|
settings.api_server.documentation_name || request.host_with_port
|
|
9
14
|
end
|
|
@@ -11,13 +16,13 @@ module HaveAPI
|
|
|
11
16
|
def documentation_document_title
|
|
12
17
|
page = @documentation_title || if @v
|
|
13
18
|
case @documentation_page
|
|
14
|
-
when 'authentication' then '
|
|
15
|
-
when 'resources' then '
|
|
19
|
+
when 'authentication' then documentation_t('labels.authentication')
|
|
20
|
+
when 'resources' then documentation_t('labels.resources')
|
|
16
21
|
when 'resource' then @documentation_resource[:key]
|
|
17
|
-
else '
|
|
22
|
+
else documentation_t('labels.overview')
|
|
18
23
|
end
|
|
19
24
|
else
|
|
20
|
-
'
|
|
25
|
+
documentation_t('labels.api_versions')
|
|
21
26
|
end
|
|
22
27
|
version = @v && !@documentation_title ? "API v#{@v}" : nil
|
|
23
28
|
[page, version, documentation_identity_name].compact.join(' · ')
|
|
@@ -59,6 +64,12 @@ module HaveAPI
|
|
|
59
64
|
documentation_search_query
|
|
60
65
|
@v = version
|
|
61
66
|
documentation_authenticated?(version)
|
|
67
|
+
with_documentation_locale do
|
|
68
|
+
render_localized_version_documentation(version)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_localized_version_documentation(version)
|
|
62
73
|
@help = settings.api_server.describe_version(Context.new(
|
|
63
74
|
settings.api_server,
|
|
64
75
|
version:, user: current_user, params:
|
|
@@ -66,6 +77,7 @@ module HaveAPI
|
|
|
66
77
|
return render_documentation_search_index if documentation_search_index_request?
|
|
67
78
|
|
|
68
79
|
select_documentation_page
|
|
80
|
+
documentation_example_selection
|
|
69
81
|
@documentation_ui = true
|
|
70
82
|
content_type 'text/html'
|
|
71
83
|
erb :doc_layout, layout: :main_layout do
|
|
@@ -81,36 +93,36 @@ module HaveAPI
|
|
|
81
93
|
def documentation_selection
|
|
82
94
|
@documentation_selection ||= begin
|
|
83
95
|
query = request.query_string
|
|
84
|
-
halt 400, '
|
|
96
|
+
halt 400, documentation_t('errors.selection') if query.match?(/%(?![0-9a-f]{2})/i)
|
|
85
97
|
pairs = URI.decode_www_form(query)
|
|
86
98
|
selected = pairs.select { |key, _| key.match?(/\A(?:page|resource)(?:\[|\z)/) }
|
|
87
99
|
unless selected.all? { |key, value| %w[page resource].include?(key) && value.valid_encoding? } &&
|
|
88
100
|
selected.map(&:first).uniq.length == selected.length && selected.length <= 1
|
|
89
|
-
halt 400, '
|
|
101
|
+
halt 400, documentation_t('errors.selection')
|
|
90
102
|
end
|
|
91
103
|
selected.to_h
|
|
92
104
|
end
|
|
93
105
|
rescue ArgumentError
|
|
94
|
-
halt 400, '
|
|
106
|
+
halt 400, documentation_t('errors.selection')
|
|
95
107
|
end
|
|
96
108
|
|
|
97
109
|
def select_documentation_page
|
|
98
110
|
selection = documentation_selection
|
|
99
111
|
@documentation_page = selection.fetch('page', 'overview')
|
|
100
112
|
unless %w[overview authentication resources].include?(@documentation_page)
|
|
101
|
-
halt 400, '
|
|
113
|
+
halt 400, documentation_t('errors.selection')
|
|
102
114
|
end
|
|
103
115
|
if @documentation_page == 'resources' && !documentation_search_query.empty?
|
|
104
|
-
@documentation_title =
|
|
116
|
+
@documentation_title = documentation_t('titles.search', version: @v, query: documentation_search_query)
|
|
105
117
|
end
|
|
106
118
|
return unless selection.has_key?('resource')
|
|
107
119
|
|
|
108
120
|
key = selection['resource']
|
|
109
121
|
unless key.match?(/\A[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\z/)
|
|
110
|
-
halt 400, '
|
|
122
|
+
halt 400, documentation_t('errors.selection')
|
|
111
123
|
end
|
|
112
124
|
@documentation_resource = documentation_resources.find { |entry| entry[:key] == key }
|
|
113
|
-
halt 404, '
|
|
125
|
+
halt 404, documentation_t('errors.resource_not_found') unless @documentation_resource
|
|
114
126
|
@documentation_page = 'resource'
|
|
115
127
|
end
|
|
116
128
|
|
|
@@ -153,8 +165,8 @@ module HaveAPI
|
|
|
153
165
|
end
|
|
154
166
|
|
|
155
167
|
def documentation_action_sections(info, resource_path:, action_name:, authentication: nil)
|
|
156
|
-
sections = { input: '
|
|
157
|
-
sections[:examples] = '
|
|
168
|
+
sections = { input: documentation_t('labels.input_parameters'), output: documentation_t('labels.output_parameters') }
|
|
169
|
+
sections[:examples] = documentation_t('labels.examples') unless info[:examples].empty?
|
|
158
170
|
sections.map do |section, label|
|
|
159
171
|
{ id: documentation_reference_anchor(resource_path:, action_name:, authentication:, section:), label: }
|
|
160
172
|
end
|
|
@@ -169,16 +181,81 @@ module HaveAPI
|
|
|
169
181
|
end
|
|
170
182
|
|
|
171
183
|
def documentation_permalink(id, label)
|
|
172
|
-
"<a class=\"docs-permalink\" href=\"##{escape_html(id)}\" aria-label=\"#{escape_html(
|
|
184
|
+
"<a class=\"docs-permalink\" href=\"##{escape_html(id)}\" aria-label=\"#{escape_html(documentation_t('accessibility.link_to', label:))}\"><span aria-hidden=\"true\">#</span></a>"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def documentation_example_selection
|
|
188
|
+
return @documentation_example_selection if defined?(@documentation_example_selection)
|
|
189
|
+
|
|
190
|
+
keys = %w[example_language example_client example_detail]
|
|
191
|
+
pairs = URI.decode_www_form(request.query_string, Encoding::ASCII_8BIT).map do |key, value|
|
|
192
|
+
[key.force_encoding(Encoding::UTF_8), value.force_encoding(Encoding::UTF_8)]
|
|
193
|
+
end
|
|
194
|
+
selected = pairs.select { |key, _| key.match?(/\Aexample_(?:language|client|detail)(?:\[|\z)/) }
|
|
195
|
+
unless selected.all? { |key, value| keys.include?(key) && key.valid_encoding? && value.valid_encoding? } &&
|
|
196
|
+
selected.map(&:first).uniq.length == selected.length
|
|
197
|
+
halt 400, documentation_t('errors.selection')
|
|
198
|
+
end
|
|
199
|
+
requested = selected.to_h
|
|
200
|
+
clients = documentation_clients
|
|
201
|
+
client = clients.find { |entry| entry.id == requested['example_client'] }
|
|
202
|
+
language = if requested.has_key?('example_language')
|
|
203
|
+
requested['example_language']
|
|
204
|
+
else
|
|
205
|
+
client&.language
|
|
206
|
+
end
|
|
207
|
+
group = clients.select { |entry| entry.language == language }
|
|
208
|
+
if group.empty?
|
|
209
|
+
language = clients.first&.language
|
|
210
|
+
group = clients.select { |entry| entry.language == language }
|
|
211
|
+
end
|
|
212
|
+
client = group.find { |entry| entry.id == requested['example_client'] } || group.first
|
|
213
|
+
@documentation_example_selection = {
|
|
214
|
+
client:, language:, detail: requested['example_detail'] == 'thorough' ? 'thorough' : 'simple',
|
|
215
|
+
explicit: selected.any?
|
|
216
|
+
}
|
|
217
|
+
rescue ArgumentError
|
|
218
|
+
halt 400, documentation_t('errors.selection')
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def documentation_example_detail(levels)
|
|
222
|
+
requested = documentation_example_selection[:detail]
|
|
223
|
+
levels.include?(requested) ? requested : 'simple'
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def documentation_example_form_parameters
|
|
227
|
+
parameters = documentation_selection.to_a
|
|
228
|
+
parameters << ['q', documentation_search_query] unless documentation_search_query.empty?
|
|
229
|
+
parameters << ['doc_lang', documentation_locale_override] if documentation_locale_override
|
|
230
|
+
parameters
|
|
173
231
|
end
|
|
174
232
|
|
|
175
233
|
def documentation_client_metadata
|
|
176
234
|
documentation_clients.map do |client|
|
|
177
|
-
{ id: client.id, language: client.language, language_label: client.language_label, variant: client
|
|
235
|
+
{ id: client.id, language: client.language, language_label: client.language_label, variant: documentation_client_variant(client),
|
|
178
236
|
details: (@documentation_page_details || {}).fetch(client.id, [:simple]).map(&:to_s) }
|
|
179
237
|
end
|
|
180
238
|
end
|
|
181
239
|
|
|
240
|
+
def documentation_client_label(client)
|
|
241
|
+
case client.id
|
|
242
|
+
when 'go' then documentation_t('generated.labels.go', default: client.label)
|
|
243
|
+
when 'rust-async' then documentation_t('generated.labels.rust_async', default: client.label)
|
|
244
|
+
when 'rust-blocking' then documentation_t('generated.labels.rust_blocking', default: client.label)
|
|
245
|
+
when 'typescript-generated' then documentation_t('generated.labels.typescript', default: client.label)
|
|
246
|
+
else client.label
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def documentation_client_variant(client)
|
|
251
|
+
case client.id
|
|
252
|
+
when 'rust-async' then documentation_t('generated.variants.async', default: client.variant)
|
|
253
|
+
when 'rust-blocking' then documentation_t('generated.variants.blocking', default: client.variant)
|
|
254
|
+
when 'typescript-generated' then documentation_t('generated.variants.api_client', default: client.variant)
|
|
255
|
+
else client.variant
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
182
259
|
def documentation_json(value)
|
|
183
260
|
JSON.generate(value).gsub('<', '\\u003c').gsub('>', '\\u003e').gsub('&', '\\u0026')
|
|
184
261
|
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
module HaveAPI
|
|
4
|
+
# Documentation has its own preference; API request locale remains independent.
|
|
5
|
+
module DocumentationLocale
|
|
6
|
+
LOCALES = %w[en cs].freeze
|
|
7
|
+
COOKIE = 'haveapi.docs.locale'.freeze
|
|
8
|
+
|
|
9
|
+
def documentation_locale
|
|
10
|
+
@documentation_locale ||= begin
|
|
11
|
+
explicit = documentation_locale_override
|
|
12
|
+
saved = request.cookies[COOKIE]
|
|
13
|
+
explicit || (saved if LOCALES.include?(saved)) ||
|
|
14
|
+
HaveAPI::I18n.accept_language(request.env['HTTP_ACCEPT_LANGUAGE'], LOCALES)&.to_s || 'en'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def documentation_locale_override
|
|
19
|
+
pairs = URI.decode_www_form(request.query_string)
|
|
20
|
+
values = pairs.select { |pair| pair.first == 'doc_lang' }
|
|
21
|
+
values.first.last if values.length == 1 && LOCALES.include?(values.first.last)
|
|
22
|
+
rescue ArgumentError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def with_documentation_locale(persist: true)
|
|
27
|
+
previous = ::I18n.locale
|
|
28
|
+
@documentation_static_messages = {}
|
|
29
|
+
settings.api_server.activate_locale(documentation_locale)
|
|
30
|
+
add_vary_header('Accept-Language')
|
|
31
|
+
add_vary_header('Cookie')
|
|
32
|
+
if persist && documentation_locale_override && !documentation_search_index_request?
|
|
33
|
+
response.set_cookie(COOKIE, value: documentation_locale, path: root,
|
|
34
|
+
max_age: 31_536_000, same_site: :lax, secure: base_url.start_with?('https:'))
|
|
35
|
+
end
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
@documentation_static_messages = nil
|
|
39
|
+
::I18n.locale = previous
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def documentation_language_parameters
|
|
43
|
+
URI.decode_www_form(request.query_string).reject { |pair| pair.first == 'doc_lang' }
|
|
44
|
+
rescue ArgumentError
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def documentation_t(key, default: nil, **values)
|
|
49
|
+
if @documentation_static_messages && key.is_a?(String) && default.nil? && values.empty?
|
|
50
|
+
memo = (@documentation_static_messages[::I18n.locale] ||= {})
|
|
51
|
+
cached = memo[key]
|
|
52
|
+
return cached.dup if cached
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
full_key = "haveapi.documentation.#{key}"
|
|
56
|
+
fallback = ::I18n.t(full_key, locale: :en, default:, **values)
|
|
57
|
+
result = ::I18n.locale == :en ? fallback : ::I18n.t(full_key, default: fallback, **values)
|
|
58
|
+
|
|
59
|
+
# Own the stored bytes; neither the first result nor later hit copies can
|
|
60
|
+
# mutate labels used elsewhere in this request. Dynamic lookups stay live.
|
|
61
|
+
memo[key] = result.dup.freeze if memo && result.is_a?(String)
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -135,9 +135,9 @@ module HaveAPI
|
|
|
135
135
|
@purposes = %w[pending session].to_h { |kind| [kind, "haveapi.docs.#{binding}.#{kind}"] }.freeze
|
|
136
136
|
@names = %w[pending session].to_h { |kind| [kind, "haveapi_docs_#{Digest::SHA256.hexdigest(@path)[0, 12]}_#{kind}"] }.freeze
|
|
137
137
|
that = self
|
|
138
|
-
sinatra.get(@routes.fetch('login')) { that.login(self) }
|
|
139
|
-
sinatra.get(@routes.fetch('callback')) { that.callback(self) }
|
|
140
|
-
sinatra.post(@routes.fetch('logout')) { that.logout(self) }
|
|
138
|
+
sinatra.get(@routes.fetch('login')) { with_documentation_locale(persist: false) { that.login(self) } }
|
|
139
|
+
sinatra.get(@routes.fetch('callback')) { with_documentation_locale(persist: false) { that.callback(self) } }
|
|
140
|
+
sinatra.post(@routes.fetch('logout')) { with_documentation_locale(persist: false) { that.logout(self) } }
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def authenticate(handler, version)
|
|
@@ -153,7 +153,7 @@ module HaveAPI
|
|
|
153
153
|
handler.instance_variable_set(:@documentation_oauth_csrf, session.fetch('csrf'))
|
|
154
154
|
else
|
|
155
155
|
clear_cookie(handler, 'session')
|
|
156
|
-
handler.instance_variable_set(:@documentation_oauth_message, '
|
|
156
|
+
handler.instance_variable_set(:@documentation_oauth_message, HaveAPI.message('haveapi.documentation.oauth.session_ended'))
|
|
157
157
|
end
|
|
158
158
|
user
|
|
159
159
|
end
|
|
@@ -228,7 +228,7 @@ module HaveAPI
|
|
|
228
228
|
session = read_cookie(handler, 'session')
|
|
229
229
|
csrf = handler.params['csrf']
|
|
230
230
|
unless handler.request.env['HTTP_ORIGIN'] == @origin && session && nonce?(csrf) && secure_equal?(csrf, session['csrf'])
|
|
231
|
-
handler.halt(403, '
|
|
231
|
+
handler.halt(403, handler.documentation_t('oauth.sign_out_invalid'))
|
|
232
232
|
end
|
|
233
233
|
clear_cookie(handler, 'session')
|
|
234
234
|
clear_cookie(handler, 'pending')
|
|
@@ -238,8 +238,8 @@ module HaveAPI
|
|
|
238
238
|
@transport.post(@endpoints.fetch(:revoke), parameters)
|
|
239
239
|
rescue Failure
|
|
240
240
|
handler.halt(502, { 'content-type' => 'text/html; charset=utf-8' },
|
|
241
|
-
|
|
242
|
-
"<p><a href=\"#{CGI.escapeHTML(destination_url('version' => @server.default_version.to_s))}\"
|
|
241
|
+
"<p>#{CGI.escapeHTML(handler.documentation_t('oauth.sign_out_incomplete'))}</p>" \
|
|
242
|
+
"<p><a href=\"#{CGI.escapeHTML(destination_url('version' => @server.default_version.to_s))}\">#{CGI.escapeHTML(handler.documentation_t('oauth.return'))}</a></p>")
|
|
243
243
|
end
|
|
244
244
|
if switch_user
|
|
245
245
|
handler.redirect("#{@origin}#{@routes.fetch('login')}?#{URI.encode_www_form(version: @server.default_version.to_s)}", 303)
|
|
@@ -274,7 +274,7 @@ module HaveAPI
|
|
|
274
274
|
end
|
|
275
275
|
|
|
276
276
|
def require_origin(handler)
|
|
277
|
-
handler.halt(400, '
|
|
277
|
+
handler.halt(400, handler.documentation_t('oauth.origin_required')) unless documentation_origin?(handler)
|
|
278
278
|
end
|
|
279
279
|
|
|
280
280
|
def protect_response(handler, referrer_policy: 'no-referrer')
|
|
@@ -339,12 +339,12 @@ module HaveAPI
|
|
|
339
339
|
|
|
340
340
|
unless @switch_user && selected == [%w[switch_user 1]] && parameters['switch_user'] == '1' &&
|
|
341
341
|
[nil, 'application/x-www-form-urlencoded'].include?(request.media_type)
|
|
342
|
-
handler.halt(403, '
|
|
342
|
+
handler.halt(403, handler.documentation_t('oauth.switch_invalid'))
|
|
343
343
|
end
|
|
344
344
|
|
|
345
345
|
true
|
|
346
346
|
rescue ArgumentError
|
|
347
|
-
handler.halt(403, '
|
|
347
|
+
handler.halt(403, handler.documentation_t('oauth.switch_invalid'))
|
|
348
348
|
end
|
|
349
349
|
|
|
350
350
|
def nonce?(value)
|
|
@@ -392,7 +392,7 @@ module HaveAPI
|
|
|
392
392
|
payload
|
|
393
393
|
rescue ActiveSupport::MessageEncryptor::InvalidMessage, Failure, ArgumentError, TypeError
|
|
394
394
|
clear_cookie(handler, kind)
|
|
395
|
-
handler.instance_variable_set(:@documentation_oauth_message, '
|
|
395
|
+
handler.instance_variable_set(:@documentation_oauth_message, HaveAPI.message('haveapi.documentation.oauth.session_ended')) if kind == 'session'
|
|
396
396
|
nil
|
|
397
397
|
end
|
|
398
398
|
|
|
@@ -405,7 +405,7 @@ module HaveAPI
|
|
|
405
405
|
protect_response(handler)
|
|
406
406
|
link = "#{@origin}#{@routes.fetch('login')}"
|
|
407
407
|
handler.halt(400, { 'content-type' => 'text/html; charset=utf-8' },
|
|
408
|
-
"<p
|
|
408
|
+
"<p>#{CGI.escapeHTML(handler.documentation_t('oauth.sign_in_failed'))} <a href=\"#{CGI.escapeHTML(link)}\">#{CGI.escapeHTML(handler.documentation_t('oauth.try_again'))}</a>.</p>")
|
|
409
409
|
end
|
|
410
410
|
end
|
|
411
411
|
end
|
|
@@ -139,7 +139,7 @@ module HaveAPI
|
|
|
139
139
|
|
|
140
140
|
def invalid_documentation_search_query
|
|
141
141
|
headers 'Cache-Control' => 'private, no-store'
|
|
142
|
-
halt 400, '
|
|
142
|
+
halt 400, documentation_t('errors.search_query')
|
|
143
143
|
end
|
|
144
144
|
|
|
145
145
|
def documentation_search_form_options(id:)
|
|
@@ -159,7 +159,7 @@ module HaveAPI
|
|
|
159
159
|
end
|
|
160
160
|
|
|
161
161
|
def documentation_search_index_url(version:, key:)
|
|
162
|
-
"#{url(settings.api_server.version_prefix(version), false)}?page=resources&search_index=#{key}"
|
|
162
|
+
"#{url(settings.api_server.version_prefix(version), false)}?page=resources&search_index=#{key}&doc_lang=#{documentation_locale}"
|
|
163
163
|
end
|
|
164
164
|
|
|
165
165
|
def documentation_search_index_request?
|
|
@@ -171,7 +171,7 @@ module HaveAPI
|
|
|
171
171
|
index = documentation_search_index
|
|
172
172
|
requested = documentation_search_parameters[:index]
|
|
173
173
|
if requested != 'current' && requested != index.key
|
|
174
|
-
halt 409, '
|
|
174
|
+
halt 409, documentation_t('search.changed')
|
|
175
175
|
end
|
|
176
176
|
|
|
177
177
|
content_type 'text/html', charset: 'utf-8'
|
|
@@ -215,8 +215,8 @@ module HaveAPI
|
|
|
215
215
|
else
|
|
216
216
|
context << "#{result[:resource_path].join('.')} · #{result[:action_name]}"
|
|
217
217
|
end
|
|
218
|
-
context << "#{result[:direction]
|
|
219
|
-
context <<
|
|
218
|
+
context << "#{documentation_t("browser.#{result[:direction]}")} · #{result[:type]}" if result[:kind] == :parameter
|
|
219
|
+
context << documentation_t('browser.authentication', provider: result[:authentication]) if result[:authentication]
|
|
220
220
|
context << result[:label] if result[:kind] != :resource && result[:label] && result[:label] != result[:name]
|
|
221
221
|
context
|
|
222
222
|
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'cgi'
|
|
2
2
|
require 'json'
|
|
3
|
+
require 'haveapi/i18n'
|
|
3
4
|
|
|
4
5
|
module HaveAPI
|
|
5
6
|
# Presents already-authorized/localized metadata without changing validation.
|
|
@@ -10,31 +11,40 @@ module HaveAPI
|
|
|
10
11
|
|
|
11
12
|
renderer = new(type)
|
|
12
13
|
rules = validators.map { |name, options| renderer.rule(name, options) }.join
|
|
13
|
-
%(<div class="docs-validators"><table class="docs-validator-table"><caption
|
|
14
|
+
%(<div class="docs-validators"><table class="docs-validator-table"><caption>#{renderer.text('caption')}</caption><thead><tr><th scope="col">#{renderer.text('rule')}</th><th scope="col">#{renderer.text('details')}</th></tr></thead><tbody>#{rules}</tbody></table></div>)
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def initialize(type)
|
|
17
18
|
@type = type
|
|
18
19
|
end
|
|
19
20
|
|
|
21
|
+
# Escape catalog text before interpolating markup produced by this renderer.
|
|
22
|
+
# Validator metadata is always escaped by value/literal/html first.
|
|
23
|
+
def text(key, **values)
|
|
24
|
+
full_key = "haveapi.documentation.validators.#{key}"
|
|
25
|
+
fallback = ::I18n.t(full_key, locale: :en, skip_interpolation: true)
|
|
26
|
+
template = ::I18n.t(full_key, default: fallback, skip_interpolation: true)
|
|
27
|
+
format(html(template), **values)
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
def rule(name, options)
|
|
21
31
|
content, used = explanation(name.to_s, options)
|
|
22
32
|
if content && options.is_a?(Hash)
|
|
23
33
|
remaining = options.reject { |key, _| used.include?(key.to_s) || key.to_s == 'message' }
|
|
24
|
-
content += "<p><strong
|
|
34
|
+
content += "<p><strong>#{text('message')}</strong> #{value(field(options, :message))}</p>" if option?(options, :message)
|
|
25
35
|
unless remaining.empty?
|
|
26
|
-
content += "<details><summary
|
|
36
|
+
content += "<details><summary>#{text('additional')}</summary>#{data(remaining)}</details>"
|
|
27
37
|
end
|
|
28
38
|
elsif !content
|
|
29
|
-
content = "<p
|
|
39
|
+
content = "<p>#{text('metadata')}</p>#{data(options)}"
|
|
30
40
|
end
|
|
31
41
|
known = %w[present include exclude accept confirm number length format custom].include?(name.to_s)
|
|
32
|
-
title = known ? name
|
|
33
|
-
%(<tr class="docs-validator"><th scope="row">#{
|
|
42
|
+
title = known ? text("names.#{name}") : html(name)
|
|
43
|
+
%(<tr class="docs-validator"><th scope="row">#{title}</th><td>#{content}</td></tr>)
|
|
34
44
|
end
|
|
35
45
|
|
|
36
46
|
def explanation(name, options)
|
|
37
|
-
return ["<p
|
|
47
|
+
return ["<p>#{text('custom', rule: html(options))}</p>", []] if name == 'custom' && options.is_a?(String)
|
|
38
48
|
return unless options.is_a?(Hash)
|
|
39
49
|
# Two differently typed keys must not silently collapse to one option.
|
|
40
50
|
return if options.keys.map(&:to_s).uniq.length != options.length
|
|
@@ -45,20 +55,20 @@ module HaveAPI
|
|
|
45
55
|
when 'accept'
|
|
46
56
|
return unless option?(options, :value)
|
|
47
57
|
|
|
48
|
-
["<p
|
|
58
|
+
["<p>#{text('accept', value: literal(field(options, :value)))}</p>", ['value']]
|
|
49
59
|
when 'confirm'
|
|
50
60
|
return unless field(options, :parameter).is_a?(String) || field(options, :parameter).is_a?(Symbol)
|
|
51
61
|
return unless boolean?(field(options, :equal))
|
|
52
62
|
|
|
53
|
-
relation = field(options, :equal) ? '
|
|
54
|
-
["<p>#{relation
|
|
63
|
+
relation = field(options, :equal) ? 'confirm_equal' : 'confirm_different'
|
|
64
|
+
["<p>#{text(relation, parameter: "<code>#{html(field(options, :parameter))}</code>")}</p>", %w[parameter equal]]
|
|
55
65
|
when 'number' then number(options)
|
|
56
66
|
when 'length' then length(options)
|
|
57
67
|
when 'format'
|
|
58
68
|
return unless field(options, :rx).is_a?(String) && boolean?(field(options, :match))
|
|
59
69
|
|
|
60
|
-
relation = field(options, :match) ? '
|
|
61
|
-
content = "<p>#{relation}
|
|
70
|
+
relation = field(options, :match) ? 'format_match' : 'format_no_match'
|
|
71
|
+
content = "<p>#{text(relation)}</p><pre><code>#{html(field(options, :rx))}</code></pre>"
|
|
62
72
|
content += "<p>#{value(field(options, :description))}</p>" if option?(options, :description)
|
|
63
73
|
[content, %w[rx match description]]
|
|
64
74
|
end
|
|
@@ -67,22 +77,22 @@ module HaveAPI
|
|
|
67
77
|
def presence(options)
|
|
68
78
|
return unless boolean?(field(options, :empty))
|
|
69
79
|
|
|
70
|
-
empty = field(options, :empty) ? '
|
|
71
|
-
["<p
|
|
80
|
+
empty = field(options, :empty) ? 'empty_allowed' : 'empty_disallowed'
|
|
81
|
+
["<p>#{text('present')} #{text(empty)}</p>", ['empty']]
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
def choices(name, options)
|
|
75
85
|
values = field(options, :values)
|
|
76
86
|
return unless values.is_a?(Array) || (name == 'include' && values.is_a?(Hash))
|
|
77
87
|
|
|
78
|
-
label = name == 'include' ? '
|
|
88
|
+
label = name == 'include' ? 'allowed' : 'disallowed'
|
|
79
89
|
items = if values.is_a?(Hash)
|
|
80
90
|
values.map { |machine, label_text| "<li>#{choice_literal(machine)}: #{value(label_text)}</li>" }
|
|
81
91
|
else
|
|
82
92
|
values.map { |machine| "<li>#{choice_literal(machine)}</li>" }
|
|
83
93
|
end
|
|
84
|
-
body = items.empty? ?
|
|
85
|
-
["<p>#{label}:</p>#{body}", ['values']]
|
|
94
|
+
body = items.empty? ? "<p>#{text('empty_set')}</p>" : %(<ul class="docs-validator-values">#{items.join}</ul>)
|
|
95
|
+
["<p>#{text(label)}:</p>#{body}", ['values']]
|
|
86
96
|
end
|
|
87
97
|
|
|
88
98
|
def number(options)
|
|
@@ -90,18 +100,18 @@ module HaveAPI
|
|
|
90
100
|
return unless numeric.all? { |key| !option?(options, key) || finite_number?(field(options, key)) }
|
|
91
101
|
return unless %w[odd even].all? { |key| !option?(options, key) || boolean?(field(options, key)) }
|
|
92
102
|
|
|
93
|
-
lines = ['
|
|
94
|
-
lines <<
|
|
95
|
-
lines <<
|
|
103
|
+
lines = [text('number')]
|
|
104
|
+
lines << text('minimum', value: literal(field(options, :min))) if option?(options, :min)
|
|
105
|
+
lines << text('maximum', value: literal(field(options, :max))) if option?(options, :max)
|
|
96
106
|
if option?(options, :step)
|
|
97
107
|
start = option?(options, :min) ? field(options, :min) : 0
|
|
98
|
-
lines <<
|
|
108
|
+
lines << text('step', value: literal(field(options, :step)), start: literal(start))
|
|
99
109
|
end
|
|
100
|
-
lines <<
|
|
101
|
-
lines << '
|
|
102
|
-
lines << '
|
|
103
|
-
lines << '
|
|
104
|
-
lines << '
|
|
110
|
+
lines << text('multiple', value: literal(field(options, :mod))) if option?(options, :mod)
|
|
111
|
+
lines << text('odd') if field(options, :odd) == true
|
|
112
|
+
lines << text('even') if field(options, :even) == true
|
|
113
|
+
lines << text('odd_disabled') if option?(options, :odd) && field(options, :odd) == false
|
|
114
|
+
lines << text('even_disabled') if option?(options, :even) && field(options, :even) == false
|
|
105
115
|
["<ul>#{lines.map { |line| "<li>#{line}</li>" }.join}</ul>", numeric + %w[odd even]]
|
|
106
116
|
end
|
|
107
117
|
|
|
@@ -112,16 +122,16 @@ module HaveAPI
|
|
|
112
122
|
return if option?(options, :equals) && (option?(options, :min) || option?(options, :max))
|
|
113
123
|
|
|
114
124
|
unit = if %w[String Text].include?(@type)
|
|
115
|
-
'
|
|
125
|
+
text('characters')
|
|
116
126
|
elsif %w[List Array].include?(@type)
|
|
117
|
-
'
|
|
127
|
+
text('items')
|
|
118
128
|
else
|
|
119
129
|
''
|
|
120
130
|
end
|
|
121
131
|
lines = []
|
|
122
|
-
lines <<
|
|
123
|
-
lines <<
|
|
124
|
-
lines <<
|
|
132
|
+
lines << text('length_equal', value: literal(field(options, :equals)), unit:) if option?(options, :equals)
|
|
133
|
+
lines << text('length_minimum', value: literal(field(options, :min)), unit:) if option?(options, :min)
|
|
134
|
+
lines << text('length_maximum', value: literal(field(options, :max)), unit:) if option?(options, :max)
|
|
125
135
|
["<ul>#{lines.map { |line| "<li>#{line}</li>" }.join}</ul>", keys]
|
|
126
136
|
end
|
|
127
137
|
|
|
@@ -152,7 +162,7 @@ module HaveAPI
|
|
|
152
162
|
end
|
|
153
163
|
|
|
154
164
|
def literal(value)
|
|
155
|
-
json_value?(value) ? "<code>#{html(JSON.generate(value))}</code>" : "<span
|
|
165
|
+
json_value?(value) ? "<code>#{html(JSON.generate(value))}</code>" : "<span>#{text('ruby_value')}: <code>#{html(value.inspect)}</code></span>"
|
|
156
166
|
end
|
|
157
167
|
|
|
158
168
|
def choice_literal(value)
|
|
@@ -167,7 +177,7 @@ module HaveAPI
|
|
|
167
177
|
label, text = if json_value?(value)
|
|
168
178
|
['JSON', JSON.pretty_generate(value)]
|
|
169
179
|
else
|
|
170
|
-
['
|
|
180
|
+
[text('ruby_value'), value.inspect]
|
|
171
181
|
end
|
|
172
182
|
"<p>#{label}:</p><pre><code>#{html(text)}</code></pre>"
|
|
173
183
|
end
|