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.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -8
  3. data/README.md +3 -422
  4. data/Rakefile +17 -7
  5. data/benchmarks/performance_compare.rb +27 -6
  6. data/doc/cs/client-examples.md +535 -0
  7. data/doc/cs/create-client.md +118 -0
  8. data/doc/cs/json-schema.html +1182 -0
  9. data/doc/cs/protocol.md +595 -0
  10. data/doc/cs/readme.md +432 -0
  11. data/doc/cs/typed-input-validation.md +122 -0
  12. data/doc/{client-examples.md → en/client-examples.md} +20 -10
  13. data/doc/{documentation-ui.md → en/documentation-ui.md} +42 -6
  14. data/doc/{performance.md → en/performance.md} +11 -0
  15. data/doc/{protocol.md → en/protocol.md} +2 -2
  16. data/doc/en/readme.md +423 -0
  17. data/haveapi.gemspec +2 -2
  18. data/lib/haveapi/client_example.rb +18 -5
  19. data/lib/haveapi/client_examples/curl.rb +5 -5
  20. data/lib/haveapi/client_examples/dynamic_helpers.rb +10 -10
  21. data/lib/haveapi/client_examples/go_client.rb +1 -1
  22. data/lib/haveapi/client_examples/http.rb +4 -4
  23. data/lib/haveapi/client_examples/js_client.rb +10 -10
  24. data/lib/haveapi/client_examples/php_client.rb +24 -25
  25. data/lib/haveapi/client_examples/python_client.rb +11 -11
  26. data/lib/haveapi/client_examples/ruby_cli.rb +6 -9
  27. data/lib/haveapi/client_examples/ruby_client.rb +24 -16
  28. data/lib/haveapi/client_examples/rust_client.rb +1 -1
  29. data/lib/haveapi/client_examples/ts_generated_client.rb +1 -1
  30. data/lib/haveapi/documentation_context.rb +14 -2
  31. data/lib/haveapi/documentation_helpers.rb +92 -15
  32. data/lib/haveapi/documentation_locale.rb +65 -0
  33. data/lib/haveapi/documentation_oauth.rb +12 -12
  34. data/lib/haveapi/documentation_search.rb +5 -5
  35. data/lib/haveapi/documentation_validators.rb +43 -33
  36. data/lib/haveapi/locales/cs.yml +468 -0
  37. data/lib/haveapi/locales/en.yml +467 -0
  38. data/lib/haveapi/public/css/documentation.css +8 -4
  39. data/lib/haveapi/public/js/documentation.js +73 -20
  40. data/lib/haveapi/server.rb +32 -47
  41. data/lib/haveapi/shared_documentation_helpers.rb +53 -5
  42. data/lib/haveapi/version.rb +1 -1
  43. data/lib/haveapi/views/doc_layout.erb +3 -2
  44. data/lib/haveapi/views/doc_sidebars/create-client.erb +10 -10
  45. data/lib/haveapi/views/doc_sidebars/json-schema.erb +2 -2
  46. data/lib/haveapi/views/doc_sidebars/protocol.erb +24 -24
  47. data/lib/haveapi/views/documentation/search_form.erb +5 -4
  48. data/lib/haveapi/views/documentation/search_results.erb +6 -6
  49. data/lib/haveapi/views/index.erb +3 -3
  50. data/lib/haveapi/views/main_layout.erb +4 -4
  51. data/lib/haveapi/views/shared_documentation.erb +8 -5
  52. data/lib/haveapi/views/shared_sidebar.erb +11 -11
  53. data/lib/haveapi/views/version_page/auth_body.erb +7 -7
  54. data/lib/haveapi/views/version_page/client_auth.erb +1 -1
  55. data/lib/haveapi/views/version_page/client_example.erb +1 -1
  56. data/lib/haveapi/views/version_page/client_init.erb +1 -1
  57. data/lib/haveapi/views/version_page/client_overview.erb +7 -5
  58. data/lib/haveapi/views/version_page/example_panels.erb +5 -2
  59. data/lib/haveapi/views/version_page/resource_body.erb +24 -24
  60. data/lib/haveapi/views/version_page/resource_directory.erb +2 -2
  61. data/lib/haveapi/views/version_page/usage.erb +5 -5
  62. data/lib/haveapi/views/version_page.erb +11 -11
  63. data/lib/haveapi/views/version_sidebar/auth_nav.erb +2 -2
  64. data/lib/haveapi/views/version_sidebar/authentication_control.erb +8 -8
  65. data/lib/haveapi/views/version_sidebar/authentication_status.erb +1 -1
  66. data/lib/haveapi/views/version_sidebar/brand.erb +16 -5
  67. data/lib/haveapi/views/version_sidebar.erb +33 -18
  68. data/spec/client_examples/assets/documentation_test.cjs +107 -9
  69. data/spec/client_examples/comment_capture.rb +72 -0
  70. data/spec/client_examples/comments_spec.rb +162 -0
  71. data/spec/client_examples/context_spec.rb +18 -2
  72. data/spec/client_examples/detail_spec.rb +68 -0
  73. data/spec/documentation/client_overview_spec.rb +60 -0
  74. data/spec/documentation/example_selection_spec.rb +192 -0
  75. data/spec/documentation/language_settings_spec.rb +132 -0
  76. data/spec/documentation/locale_spec.rb +367 -0
  77. data/spec/documentation/navigation_spec.rb +61 -3
  78. data/spec/documentation/oauth_spec.rb +42 -0
  79. data/spec/documentation/search_index_routes_spec.rb +8 -4
  80. data/spec/documentation/search_spec.rb +1 -1
  81. data/spec/documentation/shared_pages_spec.rb +63 -2
  82. data/spec/documentation/validators_spec.rb +25 -0
  83. data/spec/extensions/exception_mailer_spec.rb +2 -2
  84. data/spec/packaging_spec.rb +68 -9
  85. data/spec/performance/event_baseline_spec.rb +1 -1
  86. data/spec/performance/source_versions_spec.rb +111 -2
  87. metadata +24 -11
  88. /data/doc/{create-client.md → en/create-client.md} +0 -0
  89. /data/doc/{documentation-oauth.md → en/documentation-oauth.md} +0 -0
  90. /data/doc/{json-schema.html → en/json-schema.html} +0 -0
  91. /data/doc/{typed-input-validation.md → en/typed-input-validation.md} +0 -0
@@ -86,9 +86,33 @@ Version-specific navigation puts Authentication before Resources and indents
86
86
  authentication methods beneath it. Technical references are linked under
87
87
  HaveAPI internals in the versions-index sidebar, and retain their direct URLs.
88
88
 
89
+ ## Documentation language
90
+
91
+ Open **Documentation settings** and use **Language**, above **Theme**, to select
92
+ **English** or **Česky**. Before a choice is saved, the documentation follows the
93
+ browser's weighted `Accept-Language` preferences, including regional tags such
94
+ as `cs-CZ` and `en-GB`, and falls back to English. Detecting a language does not
95
+ save it. Changing the select saves the choice and reloads the current page,
96
+ preserving its path, query and fragment.
97
+
98
+ A valid `doc_lang=en` or `doc_lang=cs` query takes precedence over the saved
99
+ `haveapi.docs.locale` cookie, then browser preferences, then English. The cookie
100
+ is host-only, scoped to the API mount, uses SameSite=Lax, and is Secure on HTTPS.
101
+ If cookies are unavailable, the query still selects the language for that page,
102
+ but the choice cannot persist through the cookie. With JavaScript, only the
103
+ select is shown. Without JavaScript, use the submit button after choosing a
104
+ language; native form submission does not preserve the fragment.
105
+ Search uses the page's effective documentation language without saving a choice.
106
+
107
+ The API reference and HaveAPI internal articles share this setting. Articles
108
+ without a Czech translation show the English source with a notice. Article
109
+ links and existing section fragments retain their public URLs. Documentation
110
+ language does not change API action requests, OPTIONS responses, account
111
+ language or the API's request-locale configuration.
112
+
89
113
  ## Themes and examples
90
114
 
91
- The sidebar controls theme, language, client variant and example detail for
115
+ The sidebar controls theme, programming language, client variant and example detail for
92
116
  all examples on the page. Open **Documentation settings** and use the **Theme**
93
117
  controls to select **Automatic**, **Light** or **Dark**. Automatic follows the
94
118
  operating system. The versions index and shared internal pages use the same
@@ -99,10 +123,10 @@ client, authenticated when required. **With error handling** demonstrates
99
123
  additional error checks or diagnostics. Initialization and authentication
100
124
  examples remain on their respective pages.
101
125
 
102
- Choose a language first. The Client selector appears when that language has
103
- multiple variants, such as Callback and Async. Examples stays visible and shows
104
- the choices supported by the selected client on the current page. A selector
105
- with only one choice is disabled.
126
+ Choose a programming language first (labeled **Jazyk** in Czech). The Client selector appears when that language has
127
+ multiple variants, such as Callback and Async. The Examples selector appears
128
+ when the selected client supports more than one detail level on the current
129
+ page. If only one level is available, examples use it automatically.
106
130
 
107
131
  A client can offer only Simple on an initialization page and both levels on an
108
132
  action page. Each example uses With error handling only when that group supports
@@ -114,7 +138,19 @@ Use the copy icon inside a code block's top-right corner to copy its displayed c
114
138
  If the browser cannot copy it, select the code and copy it manually. Copying an
115
139
  example does not change the selected language, detail or scroll position.
116
140
 
117
- The browser stores these choices in `haveapi.docs.preferences.v1`. Preference
141
+ Without JavaScript, choose a programming language and press **Apply**. The page
142
+ reloads with that language's client variants. Choose the variant and example
143
+ detail, then press **Apply** again. The selected example opens in each group;
144
+ other examples stay available in their disclosure panels. These GET submissions
145
+ retain the current page, search query and explicit documentation language, and
146
+ may drop the fragment. They do not save example preferences. With JavaScript,
147
+ changes apply immediately and the Apply button is hidden. Explicit example
148
+ choices in a URL take precedence over saved choices for that page without
149
+ saving them on load.
150
+
151
+ The browser stores theme, programming language, client variant and example detail
152
+ in `haveapi.docs.preferences.v1`; its `language` field selects the programming
153
+ language. The documentation language uses its separate cookie. Preference
118
154
  data contains no credentials or API responses. Choices are checked against the
119
155
  clients available in the current API; an unavailable saved client uses an
120
156
  available fallback.
@@ -291,6 +291,17 @@ This rule permits coordinated package-version changes without changing baseline
291
291
  selection or the separate historical 0.30 maintenance-lineage proof. Response
292
292
  changes remain diagnostics under the existing per-revision stability policy.
293
293
 
294
+ Ordinary `source_versions` capture also normalizes one reviewed server-gemspec
295
+ packaging change: the exact shallow `doc/*` file-list assignment and its exact
296
+ recursive `doc/**/*` form with `File.file?` filtering. Only one matching
297
+ assignment is accepted; unknown or duplicate statements retain their strict
298
+ bytes. The receipt records `file_selection: documentation-files-v1`, preserves
299
+ raw contents and hashes, and is reconstructed during replay. Removing or changing
300
+ the marker fails validation. Legacy receipts still use their original raw
301
+ file-selection rule. All other gemspec bytes, evaluated dependencies and Ruby
302
+ requirements, runtime and lock inputs remain strict. The historical maintenance
303
+ comparison does not use this packaging normalization.
304
+
294
305
  ## GitHub workflow and baseline selection
295
306
 
296
307
  The `servers/ruby performance` workflow runs on pushes, ordinary pull requests,
@@ -67,8 +67,8 @@ be encoded in any of the supported output formats.
67
67
 
68
68
  ## API version
69
69
 
70
- <a href="/doc/protocol.png">
71
- <img src="/doc/protocol.png"
70
+ <a href="../protocol.png">
71
+ <img src="../protocol.png"
72
72
  alt="Diagram representing the structure of an API version"
73
73
  width="600">
74
74
  </a>
data/doc/en/readme.md ADDED
@@ -0,0 +1,423 @@
1
+ # HaveAPI
2
+ Server-side implementation of the [HaveAPI](https://github.com/vpsfreecz/haveapi)
3
+ protocol in Ruby in the form of a framework that can be used to create
4
+ self-descriptive RESTful web APIs. The framework features a DSL aimed at
5
+ creating API resources, actions and specifying input/output parameters. HaveAPI
6
+ handles everything from HTTP communication, authentication, parsing of input
7
+ parameters and formatting output, so that users can focus on their bussiness
8
+ logic.
9
+
10
+ ## Server framework features
11
+ - Handles network communication, authentication, authorization, input/output formats
12
+ and parameters, you need only to define resources and actions
13
+ - By writing the code you get the documentation for free
14
+ - Auto-generated online HTML documentation
15
+ - ORM integration
16
+ - ActiveRecord
17
+ - integrated with controller
18
+ - loads and documents validators from models
19
+ - eager loading
20
+ - easy to support other ORM frameworks
21
+
22
+ ## Usage
23
+ This text might not be complete or up-to-date, as things still often change.
24
+ Full use of HaveAPI may be seen
25
+ in [vpsadmin-api](https://github.com/vpsfreecz/vpsadmin-api)
26
+ ([online doc](https://api.vpsfree.cz)), which may serve as an example of how
27
+ are things meant to be used.
28
+
29
+ All resources and actions are represented by classes. They all must be stored
30
+ in a module, whose name is later given to HaveAPI. HaveAPI then searches all
31
+ classes in this module and builds your API.
32
+
33
+ For the purposes of this document, all resources will be in module `MyAPI`.
34
+
35
+ ### Example
36
+ This is a basic example, it does not show all options and functions.
37
+
38
+ Let's assume a model:
39
+
40
+ ```ruby
41
+ class User < ActiveRecord::Base
42
+ validates :login, :full_name, :role, presence: true
43
+ validates :login, format: {
44
+ with: /[a-zA-Z\.\-]{3,30}/,
45
+ message: 'not a valid login'
46
+ }, uniqueness: true
47
+ validates :role, inclusion: {
48
+ in: %w(admin user),
49
+ message '%{value} is not a valid role'
50
+ }
51
+
52
+ # An example authentication with plain text password
53
+ def self.authenticate(username, password)
54
+ u = User.find_by(login: username)
55
+
56
+ if u
57
+ u if u.password == password
58
+ end
59
+ end
60
+ end
61
+ ```
62
+
63
+ Resource user might look like this:
64
+ ```ruby
65
+ module MyAPI
66
+ class User < HaveAPI::Resource
67
+ # This resource belongs to version 1.
68
+ # It is also possible to put resource to multiple versions, e.g. [1, 2]
69
+ version 1
70
+
71
+ # Provide description for this resource
72
+ desc 'Manage users'
73
+
74
+ # ActiveRecord model to load validators from
75
+ model ::User
76
+
77
+ # Require authentication, this is the default
78
+ auth true
79
+
80
+ # Create a named group of shared params, that may be later included
81
+ # by actions.
82
+ params(:id) do
83
+ id :id, label: 'User ID'
84
+ end
85
+
86
+ params(:common) do
87
+ string :login, label: 'Login', desc: 'Used for authentication'
88
+ string :full_name, label: 'Full name'
89
+ string :role, label: 'User role', desc: 'admin or user'
90
+ end
91
+
92
+ # Actions
93
+ # Module HaveAPI::Actions::Default contains helper classes that define
94
+ # HTTP methods and routes for generic actions.
95
+ class Index < HaveAPI::Actions::Default::Index
96
+ desc 'List all users'
97
+
98
+ # There are no input parameters
99
+
100
+ # Output parameters
101
+ output(:object_list) do
102
+ use :id
103
+ use :common
104
+ end
105
+
106
+ # Determine if current user can use this action.
107
+ # allow/deny immediately returns from this block.
108
+ # Default rule is deny.
109
+ authorize do |u|
110
+ allow if u.role == 'admin'
111
+ deny # deny is implicit, so it may be omitted
112
+ end
113
+
114
+ # Provide example usage
115
+ example do
116
+ request({})
117
+ response({
118
+ users: [
119
+ {
120
+ id: 1,
121
+ login: 'myuser',
122
+ full_name: 'My Very Name'
123
+ }
124
+ ]
125
+ })
126
+ comment 'Get a list of all users like this'
127
+ end
128
+
129
+ # Helper method returning a query for all users
130
+ def query
131
+ ::User.all
132
+ end
133
+
134
+ # This method is called if the request has meta[:count] = true
135
+ def count
136
+ query.count
137
+ end
138
+
139
+ # Execute action, return the list
140
+ def exec
141
+ with_pagination(query)
142
+ end
143
+ end
144
+
145
+ class Create < HaveAPI::Actions::Default::Create
146
+ desc 'Create new user'
147
+
148
+ input do
149
+ use :common
150
+ end
151
+
152
+ output do
153
+ use :id
154
+ use :common
155
+ end
156
+
157
+ authorize do |u|
158
+ allow if u.role == 'admin'
159
+ deny
160
+ end
161
+
162
+ example do
163
+ request({
164
+ user: {
165
+ login: 'anotherlogin',
166
+ full_name: 'My Very New Name'
167
+ }
168
+ })
169
+ response({
170
+ user: {
171
+ id: 2
172
+ }
173
+ })
174
+ comment 'Create new user like this'
175
+ end
176
+
177
+ def exec
178
+ user = ::User.new(input)
179
+
180
+ if user.save
181
+ ok!(user)
182
+ else
183
+ error!('save failed', user.errors.to_hash)
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ ```
190
+
191
+ ### What you get
192
+ From this piece of code, HaveAPI will generate a self-describing API.
193
+ It will contain resource `User` with actions `Index` and `Create`,
194
+ using which you can list existing users and create new ones.
195
+
196
+ You can use any of the available clients to work with the API.
197
+
198
+ ### Run the example
199
+ ```ruby
200
+ api = HaveAPI::Server.new(MyAPI)
201
+
202
+ # Use HTTP basic auth
203
+ class BasicAuth < HaveAPI::Authentication::Basic::Provider
204
+ def find_user(request, username, password)
205
+ User.authenticate(username, password)
206
+ end
207
+ end
208
+
209
+ api.use_version(:all)
210
+ api.auth_chain << BasicAuth
211
+ api.mount('/')
212
+
213
+ api.start!
214
+ ```
215
+
216
+ This should start the application using WEBrick. Check
217
+ [http://localhost:4567](http://localhost:4567).
218
+
219
+ - `GET /` - a list of API versions
220
+ - `GET /doc` - HaveAPI documentation
221
+ - `GET /v1/` - documentation for version 1
222
+ - `OPTIONS /` - description for the whole API
223
+ - `OPTIONS /v1/` - description for API version 1
224
+
225
+ and more.
226
+
227
+ ## Localization
228
+
229
+ HaveAPI can translate framework-owned response messages, validation errors and
230
+ validator descriptions using Ruby `i18n`. The JSON envelope shape does not
231
+ change; `message`, `errors` and self-description validator messages are still
232
+ plain strings. Existing application-supplied strings passed to `error!` or
233
+ custom validator `message:` options are returned unchanged.
234
+
235
+ English is the default locale. Czech translations are bundled and can be
236
+ selected with `Accept-Language: cs` or a regional tag such as
237
+ `Accept-Language: cs-CZ`.
238
+
239
+ ```ruby
240
+ api = HaveAPI::Server.new(MyAPI)
241
+
242
+ api.default_locale = :en
243
+ api.available_locales = %i[en cs]
244
+ api.locale_header = 'Accept-Language'
245
+
246
+ api.locale do |request:, current_user:, default_locale:|
247
+ current_user&.language&.code || default_locale
248
+ end
249
+ ```
250
+
251
+ The explicit request header has precedence over the resolver. The resolver is
252
+ called again after authentication, so applications can use authenticated user
253
+ preferences when the client did not request a locale. If `locale_header` is set
254
+ to a custom header name, HaveAPI also allows that header in CORS preflight
255
+ responses. The header value uses the same syntax as `Accept-Language`.
256
+
257
+ Actions can opt into application translations by passing lazy messages to
258
+ `error!` or validator options:
259
+
260
+ ```ruby
261
+ error!(api_message('my_api.errors.quota_exceeded', limit: max_limit))
262
+
263
+ input do
264
+ string :name, required: {
265
+ message: HaveAPI.message('my_api.validation.name_required')
266
+ }
267
+ end
268
+ ```
269
+
270
+ Use `api_t(key, **values)` when an immediate string translation is needed in an
271
+ action. Use `HaveAPI.message(key, **values)` in class-level DSL blocks such as
272
+ `input`.
273
+
274
+ Action parameter labels and descriptions can be translated from the
275
+ self-description context. Set `parameter_i18n_scope` to an application locale
276
+ namespace and keep the existing English labels/descriptions as fallbacks:
277
+
278
+ ```ruby
279
+ api.parameter_i18n_scope = 'my_api'
280
+
281
+ input do
282
+ string :hostname,
283
+ label: 'Hostname',
284
+ desc: 'VPS hostname'
285
+ end
286
+ ```
287
+
288
+ HaveAPI first looks up the exact action parameter key:
289
+
290
+ ```yaml
291
+ cs:
292
+ my_api:
293
+ resources:
294
+ vps:
295
+ actions:
296
+ create:
297
+ input:
298
+ hostname:
299
+ label: "Hostname"
300
+ description: "Nazev VPS"
301
+ ```
302
+
303
+ The generated path is
304
+ `resources.<resource_path>.actions.<action>.<input|output>.<name>`.
305
+ Metadata parameters include the metadata type and direction, for example
306
+ `resources.vps.actions.create.meta.global.output.action_state_id.label`.
307
+
308
+ If the exact key is missing, HaveAPI falls back to resource input/output keys,
309
+ resource attributes and then shared attributes:
310
+
311
+ ```yaml
312
+ cs:
313
+ my_api:
314
+ resources:
315
+ vps:
316
+ input:
317
+ hostname:
318
+ label: "Hostname"
319
+ description: "Nazev VPS"
320
+ output:
321
+ hostname:
322
+ label: "Hostname"
323
+ attributes:
324
+ hostname:
325
+ label: "Hostname"
326
+ attributes:
327
+ hostname:
328
+ label: "Hostname"
329
+ ```
330
+
331
+ Choice labels declared through the `choices`/`include` validator use the same
332
+ parameter metadata path with `choices.<value>.label` appended. If choices were
333
+ declared as a value-to-label map, the declared label is used as the English
334
+ fallback. If choices were declared as an array, untranslated self-description
335
+ keeps the array form; a locale that supplies labels is returned as the existing
336
+ HaveAPI `{value: label}` map form.
337
+
338
+ ```yaml
339
+ cs:
340
+ my_api:
341
+ resources:
342
+ vps:
343
+ attributes:
344
+ state:
345
+ choices:
346
+ running:
347
+ label: "bezi"
348
+ stopped:
349
+ label: "vypnuto"
350
+ ```
351
+
352
+ Metadata parameters fall back through resource and global metadata keys:
353
+
354
+ ```yaml
355
+ cs:
356
+ my_api:
357
+ resources:
358
+ vps:
359
+ meta:
360
+ global:
361
+ output:
362
+ count:
363
+ label: "Return item count"
364
+ meta:
365
+ global:
366
+ output:
367
+ count:
368
+ label: "Return item count"
369
+ ```
370
+
371
+ Framework-owned HaveAPI metadata, such as pagination and built-in meta
372
+ parameters, is translated by HaveAPI itself and is not looked up in the
373
+ application parameter scope. Use `label_key` and `desc_key` when a parameter
374
+ needs an explicit key that is not derived from its action location:
375
+
376
+ ```ruby
377
+ string :hostname,
378
+ label: 'Hostname',
379
+ desc: 'VPS hostname',
380
+ label_key: 'my_api.attributes.hostname.label',
381
+ desc_key: 'my_api.attributes.hostname.description'
382
+ ```
383
+
384
+ HaveAPI also exposes `api.parameter_metadata_i18n_items` for maintenance tools
385
+ that generate application locale catalogs from declared parameters.
386
+
387
+ ### Run with rackup
388
+ Use the same code as above, only the last line would be
389
+
390
+ ```ruby
391
+ run api.app
392
+ ```
393
+
394
+ ## Authentication
395
+ HaveAPI defines an interface for creating authentication providers.
396
+ HTTP basic auth and token providers are built-in.
397
+
398
+ Authentication options are self-described. A client can choose what authentication
399
+ method it understands and wants to use.
400
+
401
+ ## Authorization
402
+ HaveAPI provides means for authorizing user access to actions. This process
403
+ is not self-described.
404
+
405
+ If the user is authenticated when requesting self-description, only allowed
406
+ resources, actions and parameters will be returned.
407
+
408
+ ## Validation of input data
409
+ Because validators are a part of the API's documentation, the clients can
410
+ perform client-side validation before the data is sent to the API server.
411
+
412
+ ## Blocking actions
413
+ Blocking mode is for actions whose execution is not immediate but takes an unspecified
414
+ amount of time. HaveAPI protocol allows clients to monitor progress of such actions
415
+ or cancel their execution.
416
+
417
+ ## Contributing
418
+
419
+ 1. Fork it ( https://github.com/vpsfreecz/haveapi/fork )
420
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
421
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
422
+ 4. Push to the branch (`git push origin my-new-feature`)
423
+ 5. Create a new Pull Request
data/haveapi.gemspec CHANGED
@@ -9,13 +9,13 @@ Gem::Specification.new do |s|
9
9
  s.description = 'Framework for creating self-describing APIs'
10
10
  s.authors = 'Jakub Skokan'
11
11
  s.email = 'jakub.skokan@vpsfree.cz'
12
- s.files = `git ls-files -z`.split("\x0") + Dir.glob('doc/*')
12
+ s.files = `git ls-files -z`.split("\x0") + Dir.glob('doc/**/*').select { |path| File.file?(path) }
13
13
  s.license = 'MIT'
14
14
 
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.32.1'
18
+ s.add_dependency 'haveapi-client', '~> 0.32.2'
19
19
  s.add_dependency 'i18n', '>= 1.6', '< 2'
20
20
  s.add_dependency 'json'
21
21
  s.add_dependency 'mail'
@@ -171,6 +171,19 @@ module HaveAPI
171
171
  context&.detail == :thorough
172
172
  end
173
173
 
174
+ def example_comment(key, default:, **values)
175
+ if context.respond_to?(:example_comment)
176
+ context.example_comment(key, default:, **values)
177
+ else
178
+ values.empty? ? default : format(default, values)
179
+ end
180
+ end
181
+
182
+ # Prefix every physical prose line before combining it with executable source.
183
+ def comment_lines(text, prefix)
184
+ text.gsub(/\r\n?/, "\n").split("\n", -1).map { |line| "#{prefix}#{line}" }.join("\n")
185
+ end
186
+
174
187
  def version_url
175
188
  File.join(base_url, "v#{version}", '/')
176
189
  end
@@ -194,7 +207,7 @@ module HaveAPI
194
207
  info = auth_field(auth_token_actions(description), action, {})
195
208
  input = auth_field(info, :input, {})
196
209
  unless [nil, :hash, 'hash', :object, 'object'].include?(auth_field(input, :layout))
197
- raise UnsupportedAuthentication, 'This token example requires object input.'
210
+ raise UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.token_object_input')
198
211
  end
199
212
 
200
213
  auth_field(input, :parameters, {})
@@ -211,7 +224,7 @@ module HaveAPI
211
224
  when 'Boolean' then excluding.include?(false)
212
225
  when 'Datetime' then '2026-01-01T00:00:00Z'
213
226
  when 'Custom' then {}
214
- else raise UnsupportedAuthentication, 'This token example has an unsupported input type.'
227
+ else raise UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.token_input_type')
215
228
  end
216
229
  validators = auth_field(parameter, :validators, {})
217
230
  inclusion = auth_field(auth_field(validators, :include, {}), :values)
@@ -220,14 +233,14 @@ module HaveAPI
220
233
  if acceptance.is_a?(Hash) && (acceptance.has_key?(:value) || acceptance.has_key?('value'))
221
234
  value = auth_field(acceptance, :value)
222
235
  elsif values.is_a?(Array)
223
- raise UnsupportedAuthentication, 'This token example has no advertised input choice.' if values.empty?
236
+ raise UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.token_input_choice_missing') if values.empty?
224
237
 
225
238
  value = values.find { |choice| !excluding.include?(choice) }
226
239
  end
227
240
  value = 'fixed' if name.to_s == 'lifetime'
228
241
  value = value.to_s if value.is_a?(Symbol) && %w[String Text].include?(type)
229
242
  if excluding.include?(value)
230
- raise UnsupportedAuthentication, 'This token client cannot send the advertised login input value.'
243
+ raise UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.token_input_value')
231
244
  end
232
245
 
233
246
  compatible = case type
@@ -240,7 +253,7 @@ module HaveAPI
240
253
  end
241
254
  compatible &&= values.map { |item| item.is_a?(Symbol) ? item.to_s : item }.include?(value) if values.is_a?(Array)
242
255
  compatible &&= value == auth_field(acceptance, :value) if acceptance.is_a?(Hash)
243
- raise UnsupportedAuthentication, 'This token example cannot represent its advertised input choice.' unless compatible
256
+ raise UnsupportedAuthentication, HaveAPI.t('haveapi.documentation.limitations.token_input_choice') unless compatible
244
257
 
245
258
  value
246
259
  end
@@ -17,13 +17,13 @@ module HaveAPI::ClientExamples
17
17
  case method
18
18
  when :basic
19
19
  <<~END
20
- # Password is asked on standard input
20
+ #{comment_lines(example_comment('curl_password_stdin', default: 'Password is asked on standard input'), '# ')}
21
21
  $ curl --request OPTIONS \\
22
22
  --user user \\
23
23
  #{Shellwords.escape(base_url)}
24
24
  # Password: secret
25
25
 
26
- # Password given on the command line
26
+ #{comment_lines(example_comment('curl_password_argument', default: 'Password given on the command line'), '# ')}
27
27
  $ curl --request OPTIONS \\
28
28
  --user user:secret \\
29
29
  #{Shellwords.escape(base_url)}
@@ -33,20 +33,20 @@ module HaveAPI::ClientExamples
33
33
  login = auth_token_credentials(desc)
34
34
 
35
35
  <<~END
36
- # Acquire the token
36
+ #{comment_lines(example_comment('acquire_token', default: 'Acquire the token'), '# ')}
37
37
  $ curl --request POST \\
38
38
  --header 'Content-Type: application/json' \\
39
39
  --data-binary #{Shellwords.escape(JSON.generate(token: login))} \\
40
40
  #{Shellwords.escape(File.join(base_url, '_auth', 'token', 'tokens'))}
41
41
 
42
- # Use a previously acquired token
42
+ #{comment_lines(example_comment('use_previous_token', default: 'Use a previously acquired token'), '# ')}
43
43
  $ curl --request OPTIONS \\
44
44
  --header #{Shellwords.escape("#{desc[:http_header]}: example-token")} \\
45
45
  #{Shellwords.escape(base_url)}
46
46
  END
47
47
 
48
48
  when :oauth2
49
- '# See RFC 6749 for authorization process and RFC 6750 for access token usage.'
49
+ comment_lines(example_comment('oauth_rfc', default: 'See RFC 6749 for authorization process and RFC 6750 for access token usage.'), '# ')
50
50
  end
51
51
  rescue HaveAPI::ClientExample::UnsupportedAuthentication => e
52
52
  { status: :limitation, reason: e.message }.freeze