1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
//! What a route does when it cannot succeed.
//!
//! ```text
//! cargo run -p kynos --example errors
//! ```
//!
//! Five things are worth noticing, because together they are the whole error
//! model:
//!
//! * **Nothing lists the statuses.** `StoreError` says 404, 409 and 507 by
//! deriving them; `Path<UserPath>` says 400 by being fallible; the
//! operation's `responses` is the union. There is no `responses(...)`
//! attribute to keep in step with the code, because there is nothing to keep
//! in step.
//! * **`?` works the way it does everywhere else.** `RowMissing` is the store's
//! own failure and knows nothing about HTTP. `#[from]` turns it into the
//! variant that does.
//! * **The status is never chosen at run time.** `Problem` — the RFC 9457
//! document that actually goes on the wire — carries its status in a field
//! and therefore cannot be returned from a handler at all. Naming an error
//! type is what makes the status set a `const`.
//! * **A rejection is one type per extractor, and each names only its own
//! statuses.** `Path<T>` rejects with [`PathRejection`], `Query<T>` with
//! [`QueryRejection`], `Json<T>` with [`BodyRejection`]. A single shared union
//! would be sound and would still make a handler that reads one path
//! parameter advertise the 401 it can never answer — which a client generator
//! turns into dead retry logic.
//! * **Four of the responses below come from no handler at all.** A fallback
//! policy produces the 404 and the 405, `catch_panics` produces the 500, and
//! an interceptor produces the 503. Each reaches the description by a
//! different route, and not one of them appears in a signature.
//!
//! [`middleware.rs`](middleware.rs) covers contributions properly, and
//! [`composition.rs`](composition.rs) covers the fallback policies as router
//! structure. They appear here because a reader asking "where did this status
//! come from" should find every answer in one file.
//!
//! [`PathRejection`]: kynos::error::rejection::PathRejection
//! [`QueryRejection`]: kynos::error::rejection::QueryRejection
//! [`BodyRejection`]: kynos::error::rejection::BodyRejection
use Ipv4Addr;
use ;
use ;
/// A user of the service.
/// What `/users/{id}` captures.
/// How the user list is paged.
///
/// A second fallible extractor, and the reason 400 is listed once rather than
/// twice: `QueryRejection` and `PathRejection` are different types that happen
/// to agree on a status, and an operation's `responses` is a union rather than a
/// list.
/// The store's own failure.
///
/// Deliberately says nothing about HTTP: a storage layer that knows about
/// status codes is one that cannot be reused, and one whose tests need a
/// request.
;
/// What the API says when the store cannot do what was asked.
///
/// One variant per failure a consumer should be able to tell apart. The
/// `base` is the prefix every variant's `type` URI shares, so a client can
/// branch on a stable identifier rather than on prose.
///
/// `OverQuota` reaches a client as this, which is the shape every error in the
/// service takes:
///
/// ```json
/// {
/// "type": "https://errors.example.com/over-quota",
/// "title": "Quota exceeded",
/// "status": 507,
/// "detail": "the store is over its 10000 row budget",
/// "limit": 10000
/// }
/// ```
///
/// `detail` is the `Display` output, so `thiserror` writes it once. `limit` sits
/// beside the registered members rather than nested under them, which is what
/// RFC 9457 calls an extension.
/// Refuses everything while the store is being migrated.
///
/// The fourth way a status reaches an operation. Because this can answer before
/// the handler runs, every operation it covers must document that it can — which
/// is what the contribution does, and what a `tower::Layer` has no way to say.
///
/// [`middleware.rs`](middleware.rs) is where interceptors are the subject; this
/// one exists so that every status this service can answer with is visible in
/// one file.
/// The 503 the window answers with.
///
/// An `ApiError` like every other failure in this file, so the status no
/// handler mentions still reaches a client in the shape all the others do.
;
/// Fetches one user.
///
/// The `?` converts `RowMissing` into `StoreError::NotFound`, and the operation
/// documents 404 because of it. It also documents 400, because `Path<UserPath>`
/// can reject a parameter that will not parse — a response no line of this file
/// mentions and every consumer will nonetheless see.
async
/// Lists users.
///
/// `catch_panics` puts a recovery boundary around this one operation and
/// contributes the 500 that boundary can produce. Without it a panic is not a
/// documented response — it is the connection ending, which no description can
/// express and no client can tell apart from a network failure.
async
/// Creates a user.
async
// The signature a real store would have -- it takes the row it is storing.
// This stub always fails, so it never reaches the move, but narrowing the
// parameter to fit the stub would make the example misleading.
async