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
|
@@ -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
|
|
104
|
-
the
|
|
105
|
-
|
|
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
|
-
|
|
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="
|
|
71
|
-
<img src="
|
|
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.
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
-
'
|
|
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
|