serverest 3.3.0-beta.1 → 3.3.0-beta.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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/app.js +11 -1
- package/src/swagger/index.js +11 -2
- package/src/swagger/scripts/08-flow-panel.js +374 -0
- package/src/swagger/styles/flow-panel.css +231 -0
- package/src/swagger/translations/en.js +72 -0
- package/src/swagger/translations/es.js +72 -0
- package/src/swagger/translations/pt-BR.js +72 -0
package/README.md
CHANGED
|
@@ -132,6 +132,8 @@ Toda resposta das rotas `/login`, `/usuarios`, `/produtos` e `/carrinhos` inform
|
|
|
132
132
|
|---|---|
|
|
133
133
|
| `X-ServeRest-Stages` | Todas as etapas da rota, em ordem de execução, com status, duração e detalhes |
|
|
134
134
|
|
|
135
|
+
A página de documentação desenha essas etapas logo abaixo da resposta de cada "Try it out": mostra onde a requisição parou, por que parou e como seguir, sem precisar ler o cabeçalho.
|
|
136
|
+
|
|
135
137
|
O cabeçalho é listado em `Access-Control-Expose-Headers`. Requisições `OPTIONS` (preflight de CORS) e as demais rotas (`/`, `/version`, `/swagger.json`) não possuem esse cabeçalho.
|
|
136
138
|
|
|
137
139
|
Exemplo de 401 sem token em `POST /carrinhos`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "serverest",
|
|
3
|
-
"version": "3.3.0-beta.
|
|
3
|
+
"version": "3.3.0-beta.2",
|
|
4
4
|
"description": "Servidor REST local de forma rápida e simples para estudo de testes de API",
|
|
5
5
|
"author": "Paulo Gonçalves <author@serverest.dev> (https://www.linkedin.com/in/paulo-goncalves/)",
|
|
6
6
|
"license": "GPL-3.0",
|
package/src/app.js
CHANGED
|
@@ -122,7 +122,17 @@ if (!ehAmbienteDeTestes) {
|
|
|
122
122
|
const uiOptionsBase = {
|
|
123
123
|
customSiteTitle: 'ServeRest',
|
|
124
124
|
customfavIcon: '/favicon.ico',
|
|
125
|
-
customCss
|
|
125
|
+
customCss,
|
|
126
|
+
swaggerOptions: {
|
|
127
|
+
// O swagger-ui-express serializa estas funções pelo código-fonte e elas só executam no navegador.
|
|
128
|
+
// A guarda evita página em branco quando o service worker serve um init mais novo que a página.
|
|
129
|
+
plugins: [
|
|
130
|
+
/* istanbul ignore next */
|
|
131
|
+
function () { return window.SwaggerUIBundle.plugins.DownloadUrl.apply(this, arguments) },
|
|
132
|
+
/* istanbul ignore next */
|
|
133
|
+
function () { return window.serverestFlowPanelPlugin ? window.serverestFlowPanelPlugin.apply(this, arguments) : {} }
|
|
134
|
+
]
|
|
135
|
+
}
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
app.use('/', swaggerUi.serve)
|
package/src/swagger/index.js
CHANGED
|
@@ -7,7 +7,7 @@ const STYLES_DIR = join(__dirname, 'styles')
|
|
|
7
7
|
const SCRIPTS_DIR = join(__dirname, 'scripts')
|
|
8
8
|
const TRANSLATIONS_DIR = join(__dirname, 'translations')
|
|
9
9
|
|
|
10
|
-
const STYLE_FILES = ['topbar.css', 'language-switcher.css', 'release-toast.css']
|
|
10
|
+
const STYLE_FILES = ['topbar.css', 'language-switcher.css', 'release-toast.css', 'flow-panel.css']
|
|
11
11
|
|
|
12
12
|
function loadCustomCss () {
|
|
13
13
|
return STYLE_FILES
|
|
@@ -40,6 +40,14 @@ function buildUiLabelTranslations () {
|
|
|
40
40
|
return entries.join(',\n')
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// O painel sempre renderiza pt-BR e a tradução da página troca o texto, como no resto da UI.
|
|
44
|
+
// ui.response entra porque o painel reaproveita o rótulo "Resposta" já existente.
|
|
45
|
+
function buildFlowPanelTexts () {
|
|
46
|
+
const ptBr = require(join(TRANSLATIONS_DIR, 'pt-BR.js'))
|
|
47
|
+
const keys = Object.keys(ptBr).filter(k => k.startsWith('ui.flow.') || k === 'ui.response')
|
|
48
|
+
return JSON.stringify(keys.reduce((texts, key) => ({ ...texts, [key]: ptBr[key] }), {}))
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
const customCss = loadCustomCss()
|
|
44
52
|
const customJsTemplate = loadCustomJsTemplate()
|
|
45
53
|
|
|
@@ -48,7 +56,8 @@ function buildCustomJsStr (releaseInfo, currentVersion, forceBanner) {
|
|
|
48
56
|
__RELEASE_INFO__: JSON.stringify(releaseInfo),
|
|
49
57
|
__CURRENT_VERSION__: JSON.stringify(currentVersion),
|
|
50
58
|
__FORCE_BANNER__: JSON.stringify(forceBanner),
|
|
51
|
-
__UI_LABEL_TRANSLATIONS__: buildUiLabelTranslations()
|
|
59
|
+
__UI_LABEL_TRANSLATIONS__: buildUiLabelTranslations(),
|
|
60
|
+
__FLOW_PANEL_TEXTS__: buildFlowPanelTexts()
|
|
52
61
|
}
|
|
53
62
|
return Object.keys(replacements).reduce(
|
|
54
63
|
(output, key) => output.split(key).join(replacements[key]),
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/* Painel "Fluxo da requisição": desenha o header X-ServeRest-Stages abaixo da resposta do "Try it out" */
|
|
2
|
+
// O ponto e vírgula inicial é obrigatório: os scripts desta pasta são concatenados e, sem ele, o
|
|
3
|
+
// navegador interpreta este bloco como uma chamada do resultado do script anterior.
|
|
4
|
+
;(function () {
|
|
5
|
+
const texts = __FLOW_PANEL_TEXTS__
|
|
6
|
+
const stagesHeader = 'x-serverest-stages'
|
|
7
|
+
const slowMotionKey = 'serverestFlowSlowMotion'
|
|
8
|
+
|
|
9
|
+
function knownKey (key) {
|
|
10
|
+
return Object.prototype.hasOwnProperty.call(texts, key) ? key : null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function text (key) {
|
|
14
|
+
return knownKey(key) ? texts[key] : ''
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseEntry (piece) {
|
|
18
|
+
const [name, ...params] = piece.split(';').map(part => part.trim())
|
|
19
|
+
const entry = {}
|
|
20
|
+
params.filter(Boolean).forEach(param => {
|
|
21
|
+
const separator = param.indexOf('=')
|
|
22
|
+
const key = separator === -1 ? param : param.slice(0, separator)
|
|
23
|
+
entry[key] = separator === -1 ? '' : param.slice(separator + 1)
|
|
24
|
+
})
|
|
25
|
+
entry.name = name
|
|
26
|
+
if (entry.dur !== undefined) {
|
|
27
|
+
const dur = Number(entry.dur)
|
|
28
|
+
entry.dur = entry.dur === '' || Number.isNaN(dur) ? undefined : dur
|
|
29
|
+
}
|
|
30
|
+
return entry
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseStages (headerValue) {
|
|
34
|
+
if (typeof headerValue !== 'string') return []
|
|
35
|
+
return headerValue.split(',')
|
|
36
|
+
.map(piece => piece.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map(parseEntry)
|
|
39
|
+
.filter(entry => entry.name)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extraParams (entry) {
|
|
43
|
+
return Object.keys(entry)
|
|
44
|
+
.filter(key => !['name', 'st', 'dur'].includes(key))
|
|
45
|
+
.reduce((params, key) => Object.assign(params, { [key]: entry[key] }), {})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildStage (entry) {
|
|
49
|
+
return {
|
|
50
|
+
name: entry.name,
|
|
51
|
+
st: entry.st,
|
|
52
|
+
dur: entry.dur,
|
|
53
|
+
params: extraParams(entry),
|
|
54
|
+
labelKey: knownKey('ui.flow.stage_label.' + entry.name),
|
|
55
|
+
descriptionKey: knownKey('ui.flow.stage_description.' + entry.name),
|
|
56
|
+
statusKey: knownKey('ui.flow.status.' + entry.st),
|
|
57
|
+
width: 0,
|
|
58
|
+
rules: []
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildRule (entry, code) {
|
|
63
|
+
return {
|
|
64
|
+
name: entry.name,
|
|
65
|
+
code,
|
|
66
|
+
st: entry.st,
|
|
67
|
+
item: entry.item,
|
|
68
|
+
params: extraParams(entry),
|
|
69
|
+
labelKey: knownKey('ui.flow.rule_label.' + code),
|
|
70
|
+
statusKey: knownKey('ui.flow.status.' + entry.st)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function failureKeys (stage, rule, status) {
|
|
75
|
+
// 408 marca como failed a etapa em execução no timeout, e 500 ou mais é erro inesperado: a etapa não explica a falha.
|
|
76
|
+
if (status === 408) return { whyKey: 'ui.flow.why.status.timeout', howKey: 'ui.flow.how.status.timeout' }
|
|
77
|
+
if (status >= 500) return { whyKey: 'ui.flow.why.status.unexpected_error', howKey: 'ui.flow.how.status.unexpected_error' }
|
|
78
|
+
if (rule) {
|
|
79
|
+
const why = knownKey('ui.flow.why.rule.' + rule.code)
|
|
80
|
+
const how = knownKey('ui.flow.how.rule.' + rule.code)
|
|
81
|
+
if (why && how) return { whyKey: why, howKey: how }
|
|
82
|
+
}
|
|
83
|
+
if (stage.name === 'auth' && stage.params.reason) {
|
|
84
|
+
const why = knownKey('ui.flow.why.auth.' + stage.params.reason)
|
|
85
|
+
const how = knownKey('ui.flow.how.auth.' + stage.params.reason)
|
|
86
|
+
if (why && how) return { whyKey: why, howKey: how }
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
whyKey: knownKey('ui.flow.why.stage.' + stage.name),
|
|
90
|
+
howKey: knownKey('ui.flow.how.stage.' + stage.name)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildFlowModel (entries, statusCode) {
|
|
95
|
+
if (!Array.isArray(entries) || entries.length === 0) return null
|
|
96
|
+
const stages = []
|
|
97
|
+
entries.forEach(entry => {
|
|
98
|
+
const separator = entry.name.indexOf('.')
|
|
99
|
+
const owner = separator > 0 ? entry.name.slice(0, separator) : null
|
|
100
|
+
const parent = owner ? stages.filter(stage => stage.name === owner).pop() : null
|
|
101
|
+
if (parent) {
|
|
102
|
+
parent.rules.push(buildRule(entry, entry.name.slice(separator + 1)))
|
|
103
|
+
} else {
|
|
104
|
+
stages.push(buildStage(entry))
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
const totalDur = stages.reduce((total, stage) => total + (stage.dur || 0), 0)
|
|
108
|
+
stages.forEach(stage => {
|
|
109
|
+
stage.width = totalDur > 0 ? (stage.dur || 0) / totalDur : 0
|
|
110
|
+
})
|
|
111
|
+
const status = Number(statusCode)
|
|
112
|
+
const failedStage = stages.find(stage => stage.st === 'failed') || null
|
|
113
|
+
const failedRule = failedStage ? failedStage.rules.find(rule => rule.st === 'failed') || null : null
|
|
114
|
+
let summary = 'completed'
|
|
115
|
+
let keys = { whyKey: null, howKey: null }
|
|
116
|
+
if (failedStage) {
|
|
117
|
+
summary = 'stopped'
|
|
118
|
+
keys = failureKeys(failedStage, failedRule, status)
|
|
119
|
+
} else if (status >= 400) {
|
|
120
|
+
summary = 'outside'
|
|
121
|
+
keys = { whyKey: 'ui.flow.why.outside', howKey: 'ui.flow.how.outside' }
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
statusCode: Number.isNaN(status) ? null : status,
|
|
125
|
+
summary,
|
|
126
|
+
failedStage: failedStage ? failedStage.name : null,
|
|
127
|
+
failedRule: failedRule ? failedRule.code : null,
|
|
128
|
+
whyKey: keys.whyKey,
|
|
129
|
+
howKey: keys.howKey,
|
|
130
|
+
stages
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function stepDelay (stage) {
|
|
135
|
+
if (stage.dur === undefined) return 420
|
|
136
|
+
return Math.min(1700, Math.max(420, stage.dur * 480))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function prefersReducedMotion () {
|
|
140
|
+
try {
|
|
141
|
+
return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
142
|
+
} catch (e) {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readSlowMotion () {
|
|
148
|
+
try {
|
|
149
|
+
return window.localStorage.getItem(slowMotionKey) !== 'false'
|
|
150
|
+
} catch (e) {
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function saveSlowMotion (enabled) {
|
|
156
|
+
try {
|
|
157
|
+
window.localStorage.setItem(slowMotionKey, enabled ? 'true' : 'false')
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readResponse (response) {
|
|
162
|
+
try {
|
|
163
|
+
const headers = response && response.get('headers')
|
|
164
|
+
const map = headers && headers.toJS ? headers.toJS() : {}
|
|
165
|
+
const name = Object.keys(map).find(key => key.toLowerCase() === stagesHeader)
|
|
166
|
+
if (!name || map[name] === undefined || map[name] === null) return null
|
|
167
|
+
return { header: String(map[name]), status: response.get('status') }
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function createComponents (React) {
|
|
174
|
+
const h = React.createElement
|
|
175
|
+
|
|
176
|
+
class FlowBoundary extends React.Component {
|
|
177
|
+
constructor (props) {
|
|
178
|
+
super(props)
|
|
179
|
+
this.state = { failed: false }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static getDerivedStateFromError () {
|
|
183
|
+
return { failed: true }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
componentDidCatch () {}
|
|
187
|
+
|
|
188
|
+
render () {
|
|
189
|
+
return this.state.failed ? null : this.props.children
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Todo texto traduzível fica sozinho no seu nó: a tradução da página compara o nó inteiro.
|
|
194
|
+
function label (key, className) {
|
|
195
|
+
return h('span', { key, className }, text(key))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Código desconhecido vai em <code>, que a tradução da página nunca altera.
|
|
199
|
+
function codeOrLabel (key, raw, className) {
|
|
200
|
+
return key ? label(key, className) : h('code', { key: 'raw-' + raw, className }, String(raw === undefined ? '' : raw))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function statusClass (item) {
|
|
204
|
+
return 'serverest-flow-st-' + (item.statusKey ? item.st : 'unknown')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function textBlock (titleKey, bodyKey, className) {
|
|
208
|
+
return h('div', { key: titleKey, className: 'serverest-flow-block ' + className },
|
|
209
|
+
h('strong', { className: 'serverest-flow-block-title' }, text(titleKey)),
|
|
210
|
+
h('p', { className: 'serverest-flow-block-text' }, text(bodyKey))
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function RuleRow (props) {
|
|
215
|
+
const rule = props.rule
|
|
216
|
+
return h('li', { className: 'serverest-flow-rule ' + statusClass(rule) },
|
|
217
|
+
codeOrLabel(rule.statusKey, rule.st, 'serverest-flow-status'),
|
|
218
|
+
codeOrLabel(rule.labelKey, rule.code, 'serverest-flow-name'),
|
|
219
|
+
rule.item !== undefined
|
|
220
|
+
? h('span', { className: 'serverest-flow-item' },
|
|
221
|
+
label('ui.flow.item_position', 'serverest-flow-item-label'),
|
|
222
|
+
h('span', { className: 'serverest-flow-item-value' }, String(rule.item)))
|
|
223
|
+
: null
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function StageRow (props) {
|
|
228
|
+
const { stage, model, expanded, onToggle } = props
|
|
229
|
+
const isFailed = model.failedStage === stage.name && stage.st === 'failed'
|
|
230
|
+
const expandable = !!stage.descriptionKey || (isFailed && !!model.whyKey)
|
|
231
|
+
const content = [
|
|
232
|
+
codeOrLabel(stage.statusKey, stage.st, 'serverest-flow-status'),
|
|
233
|
+
codeOrLabel(stage.labelKey, stage.name, 'serverest-flow-name'),
|
|
234
|
+
stage.dur !== undefined ? h('span', { className: 'serverest-flow-dur' }, stage.dur.toFixed(2) + ' ms') : null
|
|
235
|
+
]
|
|
236
|
+
const head = expandable
|
|
237
|
+
? h('button', { type: 'button', className: 'serverest-flow-row', 'aria-expanded': expanded ? 'true' : 'false', onClick: onToggle }, ...content)
|
|
238
|
+
: h('div', { className: 'serverest-flow-row' }, ...content)
|
|
239
|
+
const details = expandable && expanded
|
|
240
|
+
? h('div', { className: 'serverest-flow-details' },
|
|
241
|
+
stage.descriptionKey ? textBlock('ui.flow.stage_description_title', stage.descriptionKey, 'serverest-flow-block-description') : null,
|
|
242
|
+
isFailed && model.whyKey ? textBlock('ui.flow.why_title', model.whyKey, 'serverest-flow-block-why') : null,
|
|
243
|
+
isFailed && model.howKey ? textBlock('ui.flow.how_title', model.howKey, 'serverest-flow-block-how') : null)
|
|
244
|
+
: null
|
|
245
|
+
const rules = stage.rules.length
|
|
246
|
+
? h('ol', { className: 'serverest-flow-rules' }, stage.rules.map(rule => h(RuleRow, { key: rule.name, rule })))
|
|
247
|
+
: null
|
|
248
|
+
return h('li', { className: 'serverest-flow-stage ' + statusClass(stage) + (isFailed ? ' serverest-flow-stage-failed' : '') }, head, details, rules)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function Summary (props) {
|
|
252
|
+
const model = props.model
|
|
253
|
+
if (model.summary === 'stopped') {
|
|
254
|
+
const stage = model.stages.find(item => item.name === model.failedStage)
|
|
255
|
+
return h('p', { key: 'stopped-' + stage.name, className: 'serverest-flow-summary' },
|
|
256
|
+
label('ui.flow.stopped_at', 'serverest-flow-summary-label'),
|
|
257
|
+
codeOrLabel(stage.labelKey, stage.name, 'serverest-flow-summary-stage'))
|
|
258
|
+
}
|
|
259
|
+
const key = model.summary === 'outside' ? 'ui.flow.outside' : 'ui.flow.completed'
|
|
260
|
+
return h('p', { key, className: 'serverest-flow-summary' }, label(key, 'serverest-flow-summary-label'))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function FlowPanel (props) {
|
|
264
|
+
const { model, animate, showControls, slowMotion, onToggleSlowMotion, onReplay } = props
|
|
265
|
+
const executed = model.stages.filter(stage => stage.st !== 'not_run')
|
|
266
|
+
const [revealed, setRevealed] = React.useState(animate ? 0 : executed.length)
|
|
267
|
+
const [expanded, setExpanded] = React.useState(model.failedStage ? { [model.failedStage]: true } : {})
|
|
268
|
+
const done = revealed >= executed.length
|
|
269
|
+
|
|
270
|
+
React.useEffect(() => {
|
|
271
|
+
if (done) return undefined
|
|
272
|
+
if (!animate) {
|
|
273
|
+
setRevealed(executed.length)
|
|
274
|
+
return undefined
|
|
275
|
+
}
|
|
276
|
+
const timer = setTimeout(() => setRevealed(count => count + 1), stepDelay(executed[revealed]))
|
|
277
|
+
return () => clearTimeout(timer)
|
|
278
|
+
}, [revealed, animate, done])
|
|
279
|
+
|
|
280
|
+
const visible = model.stages.filter(stage => {
|
|
281
|
+
if (stage.st === 'not_run') return done
|
|
282
|
+
return executed.indexOf(stage) <= revealed
|
|
283
|
+
})
|
|
284
|
+
const active = done ? null : executed[revealed]
|
|
285
|
+
const listed = visible.filter(stage => stage !== active)
|
|
286
|
+
|
|
287
|
+
const segments = visible.map(stage => h('span', {
|
|
288
|
+
key: stage.name,
|
|
289
|
+
className: 'serverest-flow-segment ' + statusClass(stage) + (stage === active ? ' serverest-flow-active' : ''),
|
|
290
|
+
style: { flexGrow: stage.width }
|
|
291
|
+
}, codeOrLabel(stage.labelKey, stage.name, 'serverest-flow-segment-label')))
|
|
292
|
+
if (done) {
|
|
293
|
+
segments.push(h('span', { key: 'response', className: 'serverest-flow-segment serverest-flow-segment-response' },
|
|
294
|
+
label('ui.response', 'serverest-flow-segment-label'),
|
|
295
|
+
model.statusCode !== null ? h('span', { className: 'serverest-flow-segment-code' }, String(model.statusCode)) : null))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const controls = showControls
|
|
299
|
+
? h('div', { className: 'serverest-flow-controls' },
|
|
300
|
+
h('label', { className: 'serverest-flow-slow-motion' },
|
|
301
|
+
h('input', { type: 'checkbox', checked: slowMotion, onChange: onToggleSlowMotion }),
|
|
302
|
+
label('ui.flow.slow_motion', 'serverest-flow-slow-motion-label')),
|
|
303
|
+
h('button', { type: 'button', className: 'serverest-flow-replay', onClick: onReplay }, text('ui.flow.replay')))
|
|
304
|
+
: null
|
|
305
|
+
|
|
306
|
+
return h('section', { className: 'serverest-flow' },
|
|
307
|
+
h('div', { className: 'serverest-flow-header' },
|
|
308
|
+
h('h4', { className: 'serverest-flow-title' }, text('ui.flow.title')),
|
|
309
|
+
done ? h(Summary, { key: 'summary-' + model.summary, model }) : null,
|
|
310
|
+
controls),
|
|
311
|
+
h('div', { className: 'serverest-flow-bar' }, segments),
|
|
312
|
+
done && model.summary === 'outside'
|
|
313
|
+
? h('p', { className: 'serverest-flow-outside' },
|
|
314
|
+
label(model.whyKey, 'serverest-flow-outside-why'),
|
|
315
|
+
label(model.howKey, 'serverest-flow-outside-how'))
|
|
316
|
+
: null,
|
|
317
|
+
h('ol', { className: 'serverest-flow-stages' }, listed.map(stage => h(StageRow, {
|
|
318
|
+
key: stage.name,
|
|
319
|
+
stage,
|
|
320
|
+
model,
|
|
321
|
+
expanded: !!expanded[stage.name],
|
|
322
|
+
onToggle: () => setExpanded(current => Object.assign({}, current, { [stage.name]: !current[stage.name] }))
|
|
323
|
+
})))
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function FlowRoot (props) {
|
|
328
|
+
const [run, setRun] = React.useState(0)
|
|
329
|
+
const [slowMotion, setSlowMotion] = React.useState(readSlowMotion)
|
|
330
|
+
const { header, status } = props
|
|
331
|
+
const model = buildFlowModel(parseStages(header), status)
|
|
332
|
+
if (!model) return null
|
|
333
|
+
const reduced = prefersReducedMotion()
|
|
334
|
+
return h(FlowPanel, {
|
|
335
|
+
// Remontar a cada resposta ou repetição evita que o React edite um nó já traduzido.
|
|
336
|
+
key: header + '|' + status + '|' + run,
|
|
337
|
+
model,
|
|
338
|
+
animate: slowMotion && !reduced,
|
|
339
|
+
showControls: !reduced,
|
|
340
|
+
slowMotion,
|
|
341
|
+
onToggleSlowMotion: event => {
|
|
342
|
+
const enabled = !!event.target.checked
|
|
343
|
+
saveSlowMotion(enabled)
|
|
344
|
+
setSlowMotion(enabled)
|
|
345
|
+
},
|
|
346
|
+
onReplay: () => setRun(count => count + 1)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { FlowBoundary, FlowRoot }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function serverestFlowPanelPlugin (system) {
|
|
354
|
+
const React = system && system.React
|
|
355
|
+
if (!React || !React.Component || !React.useState) return {}
|
|
356
|
+
const { FlowBoundary, FlowRoot } = createComponents(React)
|
|
357
|
+
const h = React.createElement
|
|
358
|
+
return {
|
|
359
|
+
wrapComponents: {
|
|
360
|
+
liveResponse: Original => function ServerestLiveResponse (props) {
|
|
361
|
+
const data = readResponse(props.response)
|
|
362
|
+
const panel = data
|
|
363
|
+
? h(FlowBoundary, { key: data.header + '|' + data.status }, h(FlowRoot, { header: data.header, status: data.status }))
|
|
364
|
+
: null
|
|
365
|
+
return h(React.Fragment, null, h(Original, props), panel)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
serverestFlowPanelPlugin.parseStages = parseStages
|
|
372
|
+
serverestFlowPanelPlugin.buildFlowModel = buildFlowModel
|
|
373
|
+
window.serverestFlowPanelPlugin = serverestFlowPanelPlugin
|
|
374
|
+
})()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* Painel "Fluxo da requisição" abaixo da resposta do "Try it out" */
|
|
2
|
+
.swagger-ui .serverest-flow {
|
|
3
|
+
margin: 16px 0 8px;
|
|
4
|
+
padding: 14px 16px;
|
|
5
|
+
border: 1px solid #d8dde7;
|
|
6
|
+
border-radius: 8px;
|
|
7
|
+
background: #ffffff;
|
|
8
|
+
color: #1f2937;
|
|
9
|
+
font-family: sans-serif;
|
|
10
|
+
font-size: 13px;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.swagger-ui .serverest-flow-header {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-wrap: wrap;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 8px 16px;
|
|
18
|
+
margin-bottom: 12px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.swagger-ui .serverest-flow-title {
|
|
22
|
+
margin: 0;
|
|
23
|
+
font-size: 14px;
|
|
24
|
+
font-weight: 700;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.swagger-ui .serverest-flow-summary {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
gap: 6px;
|
|
30
|
+
margin: 0;
|
|
31
|
+
font-weight: 600;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.swagger-ui .serverest-flow-controls {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 12px;
|
|
38
|
+
margin-left: auto;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.swagger-ui .serverest-flow-slow-motion {
|
|
42
|
+
display: inline-flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
gap: 6px;
|
|
45
|
+
cursor: pointer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.swagger-ui .serverest-flow-replay {
|
|
49
|
+
padding: 4px 12px;
|
|
50
|
+
border: 1px solid #9ca3af;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
background: #ffffff;
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.swagger-ui .serverest-flow-bar {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-wrap: wrap;
|
|
60
|
+
gap: 3px;
|
|
61
|
+
min-height: 26px;
|
|
62
|
+
margin-bottom: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.swagger-ui .serverest-flow-segment {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-basis: 0;
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
gap: 4px;
|
|
72
|
+
min-width: max-content;
|
|
73
|
+
min-height: 26px;
|
|
74
|
+
padding: 0 8px;
|
|
75
|
+
border-radius: 4px;
|
|
76
|
+
font-size: 11px;
|
|
77
|
+
white-space: nowrap;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
animation: serverestFlowIn 220ms ease-out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.swagger-ui .serverest-flow-segment-response {
|
|
83
|
+
flex-grow: 0;
|
|
84
|
+
background: #1f2937;
|
|
85
|
+
color: #ffffff;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.swagger-ui .serverest-flow-segment.serverest-flow-active {
|
|
89
|
+
animation: serverestFlowPulse 900ms ease-in-out infinite;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.swagger-ui .serverest-flow-st-ok.serverest-flow-segment { background: #2f9e6b; color: #ffffff; }
|
|
93
|
+
.swagger-ui .serverest-flow-st-failed.serverest-flow-segment { background: #c53030; color: #ffffff; }
|
|
94
|
+
.swagger-ui .serverest-flow-st-skipped.serverest-flow-segment { background: #6b7280; color: #ffffff; }
|
|
95
|
+
.swagger-ui .serverest-flow-st-not_run.serverest-flow-segment { background: #e5e7eb; color: #4b5563; }
|
|
96
|
+
.swagger-ui .serverest-flow-st-unknown.serverest-flow-segment { background: #d1d5db; color: #1f2937; }
|
|
97
|
+
|
|
98
|
+
.swagger-ui .serverest-flow-outside {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-wrap: wrap;
|
|
101
|
+
gap: 4px;
|
|
102
|
+
margin: 0 0 12px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.swagger-ui .serverest-flow-stages,
|
|
106
|
+
.swagger-ui .serverest-flow-rules {
|
|
107
|
+
list-style: none;
|
|
108
|
+
margin: 0;
|
|
109
|
+
padding: 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.swagger-ui .serverest-flow-stage {
|
|
113
|
+
border-top: 1px solid #eef1f5;
|
|
114
|
+
animation: serverestFlowIn 220ms ease-out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.swagger-ui .serverest-flow-row {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: 10px;
|
|
121
|
+
width: 100%;
|
|
122
|
+
padding: 6px 0;
|
|
123
|
+
border: 0;
|
|
124
|
+
background: none;
|
|
125
|
+
color: inherit;
|
|
126
|
+
font: inherit;
|
|
127
|
+
text-align: left;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.swagger-ui button.serverest-flow-row {
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.swagger-ui .serverest-flow-status {
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
min-width: 11em;
|
|
137
|
+
padding: 1px 8px;
|
|
138
|
+
border-radius: 10px;
|
|
139
|
+
font-size: 11px;
|
|
140
|
+
text-align: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.swagger-ui .serverest-flow-st-ok > .serverest-flow-row .serverest-flow-status,
|
|
144
|
+
.swagger-ui .serverest-flow-rule.serverest-flow-st-ok .serverest-flow-status { background: #d1fae5; color: #065f46; }
|
|
145
|
+
.swagger-ui .serverest-flow-st-failed > .serverest-flow-row .serverest-flow-status,
|
|
146
|
+
.swagger-ui .serverest-flow-rule.serverest-flow-st-failed .serverest-flow-status { background: #fee2e2; color: #991b1b; }
|
|
147
|
+
.swagger-ui .serverest-flow-st-skipped > .serverest-flow-row .serverest-flow-status,
|
|
148
|
+
.swagger-ui .serverest-flow-rule.serverest-flow-st-skipped .serverest-flow-status { background: #e5e7eb; color: #374151; }
|
|
149
|
+
.swagger-ui .serverest-flow-st-not_run > .serverest-flow-row .serverest-flow-status,
|
|
150
|
+
.swagger-ui .serverest-flow-rule.serverest-flow-st-not_run .serverest-flow-status { background: #f3f4f6; color: #6b7280; }
|
|
151
|
+
|
|
152
|
+
.swagger-ui .serverest-flow-name {
|
|
153
|
+
font-weight: 600;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.swagger-ui .serverest-flow-dur {
|
|
157
|
+
margin-left: auto;
|
|
158
|
+
color: #6b7280;
|
|
159
|
+
font-variant-numeric: tabular-nums;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.swagger-ui .serverest-flow-stage-failed > .serverest-flow-row .serverest-flow-name {
|
|
163
|
+
color: #991b1b;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.swagger-ui .serverest-flow-details {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-direction: column;
|
|
169
|
+
gap: 8px;
|
|
170
|
+
padding: 0 0 10px 22px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.swagger-ui .serverest-flow-block-title {
|
|
174
|
+
display: block;
|
|
175
|
+
margin-bottom: 2px;
|
|
176
|
+
font-size: 12px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.swagger-ui .serverest-flow-block-text {
|
|
180
|
+
margin: 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.swagger-ui .serverest-flow-block-why,
|
|
184
|
+
.swagger-ui .serverest-flow-block-how {
|
|
185
|
+
padding: 6px 10px;
|
|
186
|
+
border-left: 3px solid #c53030;
|
|
187
|
+
background: #fef2f2;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.swagger-ui .serverest-flow-block-how {
|
|
191
|
+
border-left-color: #2f9e6b;
|
|
192
|
+
background: #f0fdf4;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.swagger-ui .serverest-flow-rule {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-wrap: wrap;
|
|
198
|
+
align-items: center;
|
|
199
|
+
gap: 10px;
|
|
200
|
+
padding: 4px 0 4px 22px;
|
|
201
|
+
color: #4b5563;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.swagger-ui .serverest-flow-item {
|
|
205
|
+
display: inline-flex;
|
|
206
|
+
gap: 4px;
|
|
207
|
+
margin-left: auto;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@keyframes serverestFlowIn {
|
|
211
|
+
0% { opacity: 0; }
|
|
212
|
+
100% { opacity: 1; }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@keyframes serverestFlowPulse {
|
|
216
|
+
0% { opacity: 0.45; }
|
|
217
|
+
50% { opacity: 1; }
|
|
218
|
+
100% { opacity: 0.45; }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@media (prefers-reduced-motion: reduce) {
|
|
222
|
+
.swagger-ui .serverest-flow-segment,
|
|
223
|
+
.swagger-ui .serverest-flow-stage,
|
|
224
|
+
.swagger-ui .serverest-flow-segment.serverest-flow-active {
|
|
225
|
+
animation: none;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.swagger-ui .serverest-flow-controls {
|
|
229
|
+
display: none;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -25,6 +25,78 @@ module.exports = {
|
|
|
25
25
|
'ui.download': 'Download',
|
|
26
26
|
'ui.available_authorizations': 'Available authorizations',
|
|
27
27
|
'ui.example_lower': 'example',
|
|
28
|
+
'ui.flow.title': 'Request flow',
|
|
29
|
+
'ui.flow.stopped_at': 'Stopped at',
|
|
30
|
+
'ui.flow.completed': 'Went through all stages',
|
|
31
|
+
'ui.flow.outside': 'The response came from outside the stages',
|
|
32
|
+
'ui.flow.slow_motion': 'Slow motion',
|
|
33
|
+
'ui.flow.replay': 'Replay',
|
|
34
|
+
'ui.flow.why_title': 'Why it stopped here',
|
|
35
|
+
'ui.flow.how_title': 'How to proceed',
|
|
36
|
+
'ui.flow.stage_description_title': 'What this stage does',
|
|
37
|
+
'ui.flow.item_position': 'Index of the item in the product list, counting from zero:',
|
|
38
|
+
'ui.flow.status.ok': 'Passed',
|
|
39
|
+
'ui.flow.status.failed': 'Stopped here',
|
|
40
|
+
'ui.flow.status.skipped': 'Does not apply in this environment',
|
|
41
|
+
'ui.flow.status.not_run': 'Did not run',
|
|
42
|
+
'ui.flow.stage_label.body': 'Body parsing',
|
|
43
|
+
'ui.flow.stage_label.rate_limit': 'Request limit',
|
|
44
|
+
'ui.flow.stage_label.auth': 'Authentication',
|
|
45
|
+
'ui.flow.stage_label.schema': 'Data validation',
|
|
46
|
+
'ui.flow.stage_label.read': 'Data lookup',
|
|
47
|
+
'ui.flow.stage_label.rules': 'Business rules',
|
|
48
|
+
'ui.flow.stage_label.write': 'Saving',
|
|
49
|
+
'ui.flow.stage_description.body': 'The server reads and parses the request body.',
|
|
50
|
+
'ui.flow.stage_description.rate_limit': 'The server checks that the client has not exceeded the request limit.',
|
|
51
|
+
'ui.flow.stage_description.auth': 'The server checks the token sent in the Authorization header.',
|
|
52
|
+
'ui.flow.stage_description.schema': 'The server checks that the data sent has the expected format.',
|
|
53
|
+
'ui.flow.stage_description.read': 'The server fetches the requested data.',
|
|
54
|
+
'ui.flow.stage_description.rules': 'The server applies the business rules of the route.',
|
|
55
|
+
'ui.flow.stage_description.write': 'The server saves the changes.',
|
|
56
|
+
'ui.flow.rule_label.single_cart': 'One cart per user',
|
|
57
|
+
'ui.flow.rule_label.no_duplicate_product': 'Products without repetition',
|
|
58
|
+
'ui.flow.rule_label.product_exists': 'Existence of the products',
|
|
59
|
+
'ui.flow.rule_label.enough_stock': 'Stock of the products',
|
|
60
|
+
'ui.flow.why.stage.body': 'The request body could not be read. The most common cause is malformed JSON.',
|
|
61
|
+
'ui.flow.how.stage.body': 'Check the quotes, commas and braces of the JSON sent.',
|
|
62
|
+
'ui.flow.why.stage.rate_limit': 'Too many requests were sent in a short time.',
|
|
63
|
+
'ui.flow.how.stage.rate_limit': 'Wait a few seconds and try again. This environment does not accept load testing.',
|
|
64
|
+
'ui.flow.why.auth.token_missing': 'The request did not include the Authorization header.',
|
|
65
|
+
'ui.flow.how.auth.token_missing': 'Log in at POST /login and send the authorization value in the Authorization header.',
|
|
66
|
+
'ui.flow.why.auth.bad_format': 'The Authorization header is not in the format Bearer followed by the token.',
|
|
67
|
+
'ui.flow.how.auth.bad_format': 'Send the value exactly as POST /login returns it, starting with Bearer.',
|
|
68
|
+
'ui.flow.why.auth.token_expired': 'The token has expired.',
|
|
69
|
+
'ui.flow.how.auth.token_expired': 'Log in again at POST /login to generate a new token.',
|
|
70
|
+
'ui.flow.why.auth.token_invalid': 'The token is not valid: it was changed, is incomplete or was not generated by this server.',
|
|
71
|
+
'ui.flow.how.auth.token_invalid': 'Log in at POST /login and use the returned token, without changes.',
|
|
72
|
+
'ui.flow.why.auth.user_not_found': 'The user who owns the token no longer exists. This happens when the user is deleted or when the server data is reset.',
|
|
73
|
+
'ui.flow.how.auth.user_not_found': 'Register the user again and log in to generate a new token.',
|
|
74
|
+
'ui.flow.why.auth.not_admin': 'The token is valid, but the user is not an administrator.',
|
|
75
|
+
'ui.flow.how.auth.not_admin': 'Use the token of a user registered with administrador equal to true.',
|
|
76
|
+
'ui.flow.why.stage.auth': 'The token was not accepted.',
|
|
77
|
+
'ui.flow.how.stage.auth': 'Log in at POST /login and send the authorization value in the Authorization header.',
|
|
78
|
+
'ui.flow.why.stage.schema': 'One or more values sent do not have the expected format.',
|
|
79
|
+
'ui.flow.how.stage.schema': 'The response body points out each field with a problem.',
|
|
80
|
+
'ui.flow.why.stage.read': 'The lookup could not be completed. The most common cause is a record that does not exist.',
|
|
81
|
+
'ui.flow.how.stage.read': 'Check the identifier provided.',
|
|
82
|
+
'ui.flow.why.stage.rules': 'A business rule of the route was not met.',
|
|
83
|
+
'ui.flow.how.stage.rules': 'The message in the response body says which rule it was.',
|
|
84
|
+
'ui.flow.why.rule.single_cart': 'Each user can have one cart at a time, and this user already has one.',
|
|
85
|
+
'ui.flow.how.rule.single_cart': 'Complete or cancel the current purchase before registering another cart.',
|
|
86
|
+
'ui.flow.why.rule.no_duplicate_product': 'The same product appears more than once in the list.',
|
|
87
|
+
'ui.flow.how.rule.no_duplicate_product': 'Send each product only once and adjust the quantity.',
|
|
88
|
+
'ui.flow.why.rule.product_exists': 'One of the products provided does not exist. The check goes item by item and stops at the first problem, so the following items were not checked.',
|
|
89
|
+
'ui.flow.how.rule.product_exists': 'Check the idProduto. The GET /produtos route lists the registered products.',
|
|
90
|
+
'ui.flow.why.rule.enough_stock': 'The requested quantity is greater than the product stock. The check goes item by item and stops at the first problem, so the following items were not checked.',
|
|
91
|
+
'ui.flow.how.rule.enough_stock': 'Reduce the quantity or choose another product. The response body shows the current stock.',
|
|
92
|
+
'ui.flow.why.stage.write': 'The changes could not be saved.',
|
|
93
|
+
'ui.flow.how.stage.write': 'Try again. If the error persists, open an issue in the repository.',
|
|
94
|
+
'ui.flow.why.status.timeout': 'The server went past the time limit to respond, and this was the stage running.',
|
|
95
|
+
'ui.flow.how.status.timeout': 'Try again in a few moments.',
|
|
96
|
+
'ui.flow.why.status.unexpected_error': 'The server found an unexpected error in this stage.',
|
|
97
|
+
'ui.flow.how.status.unexpected_error': 'Try again. If the error persists, open an issue in the repository.',
|
|
98
|
+
'ui.flow.why.outside': 'The response came from outside the stages: the route does not exist or the URL is invalid.',
|
|
99
|
+
'ui.flow.how.outside': 'Check the method and path of the request.',
|
|
28
100
|
'info.description': '**ServeRest is a free REST API that simulates a virtual store to serve as study material for API testing.**\n\n**Don\'t forget to follow the [project author](https://github.com/PauloGoncalvesBH) and star the repository: [github.com/ServeRest/ServeRest](https://github.com/ServeRest/ServeRest)**\n\nThis page documents all routes and how to access them. For more details about ServeRest (how to run locally using Docker or NPM, change authentication timeout, etc), see [the ServeRest repository](https://github.com/serverest/serverest).\n\nUsing Postman? Import the [Swagger JSON](https://raw.githubusercontent.com/ServeRest/ServeRest/trunk/docs/swagger.json) to access the collections.\n\nDoing load testing? Read the \'[Load Testing](https://github.com/ServeRest/ServeRest#teste-de-carga)\' section.\n\n\nThank you ♥ to everyone who supports the project [financially](https://opencollective.com/serverest#section-contributors) or [with code, ideas, and promotion](https://github.com/ServeRest/ServeRest#contribuidores-); thanks to you **more than R$ 2000,00 were donated to the NGO [Todas as letras](https://todasasletras.org/)** so far.\n\nServeRest has a front-end in beta; check it out: [front.serverest.dev](https://front.serverest.dev).\n\nNeed help? [Open an issue](https://github.com/ServeRest/ServeRest/issues) or contact the project maintainer:\n',
|
|
29
101
|
'tags.login.description': 'Authenticate your user to create a cart and, if you are an admin, manage products',
|
|
30
102
|
'tags.usuarios.description': 'Manage users, check login data, and create admin users',
|
|
@@ -25,6 +25,78 @@ module.exports = {
|
|
|
25
25
|
'ui.download': 'Descargar',
|
|
26
26
|
'ui.available_authorizations': 'Autenticaciones disponibles',
|
|
27
27
|
'ui.example_lower': 'ejemplo',
|
|
28
|
+
'ui.flow.title': 'Flujo de la solicitud',
|
|
29
|
+
'ui.flow.stopped_at': 'Se detuvo en',
|
|
30
|
+
'ui.flow.completed': 'Pasó por todas las etapas',
|
|
31
|
+
'ui.flow.outside': 'La respuesta vino de fuera de las etapas',
|
|
32
|
+
'ui.flow.slow_motion': 'Cámara lenta',
|
|
33
|
+
'ui.flow.replay': 'Repetir',
|
|
34
|
+
'ui.flow.why_title': 'Por qué se detuvo aquí',
|
|
35
|
+
'ui.flow.how_title': 'Cómo seguir',
|
|
36
|
+
'ui.flow.stage_description_title': 'Qué hace esta etapa',
|
|
37
|
+
'ui.flow.item_position': 'Índice del ítem en la lista de productos, contando desde cero:',
|
|
38
|
+
'ui.flow.status.ok': 'Pasó',
|
|
39
|
+
'ui.flow.status.failed': 'Se detuvo aquí',
|
|
40
|
+
'ui.flow.status.skipped': 'No se aplica en este entorno',
|
|
41
|
+
'ui.flow.status.not_run': 'No se ejecutó',
|
|
42
|
+
'ui.flow.stage_label.body': 'Lectura del cuerpo',
|
|
43
|
+
'ui.flow.stage_label.rate_limit': 'Límite de solicitudes',
|
|
44
|
+
'ui.flow.stage_label.auth': 'Autenticación',
|
|
45
|
+
'ui.flow.stage_label.schema': 'Validación de los datos',
|
|
46
|
+
'ui.flow.stage_label.read': 'Consulta',
|
|
47
|
+
'ui.flow.stage_label.rules': 'Reglas de negocio',
|
|
48
|
+
'ui.flow.stage_label.write': 'Guardado',
|
|
49
|
+
'ui.flow.stage_description.body': 'El servidor lee e interpreta el cuerpo de la solicitud.',
|
|
50
|
+
'ui.flow.stage_description.rate_limit': 'El servidor verifica que el cliente no haya superado el límite de solicitudes.',
|
|
51
|
+
'ui.flow.stage_description.auth': 'El servidor verifica el token enviado en el encabezado Authorization.',
|
|
52
|
+
'ui.flow.stage_description.schema': 'El servidor verifica que los datos enviados tengan el formato esperado.',
|
|
53
|
+
'ui.flow.stage_description.read': 'El servidor busca los datos solicitados.',
|
|
54
|
+
'ui.flow.stage_description.rules': 'El servidor aplica las reglas de negocio de la ruta.',
|
|
55
|
+
'ui.flow.stage_description.write': 'El servidor guarda los cambios.',
|
|
56
|
+
'ui.flow.rule_label.single_cart': 'Un carrito por usuario',
|
|
57
|
+
'ui.flow.rule_label.no_duplicate_product': 'Productos sin repetición',
|
|
58
|
+
'ui.flow.rule_label.product_exists': 'Existencia de los productos',
|
|
59
|
+
'ui.flow.rule_label.enough_stock': 'Stock de los productos',
|
|
60
|
+
'ui.flow.why.stage.body': 'No se pudo leer el cuerpo de la solicitud. La causa más común es un JSON mal formado.',
|
|
61
|
+
'ui.flow.how.stage.body': 'Revise las comillas, las comas y las llaves del JSON enviado.',
|
|
62
|
+
'ui.flow.why.stage.rate_limit': 'Se enviaron demasiadas solicitudes en poco tiempo.',
|
|
63
|
+
'ui.flow.how.stage.rate_limit': 'Espere unos segundos e intente de nuevo. Este entorno no acepta pruebas de carga.',
|
|
64
|
+
'ui.flow.why.auth.token_missing': 'La solicitud no incluyó el encabezado Authorization.',
|
|
65
|
+
'ui.flow.how.auth.token_missing': 'Inicie sesión en POST /login y envíe el valor de authorization en el encabezado Authorization.',
|
|
66
|
+
'ui.flow.why.auth.bad_format': 'El encabezado Authorization no está en el formato Bearer seguido del token.',
|
|
67
|
+
'ui.flow.how.auth.bad_format': 'Envíe el valor exactamente como lo devuelve POST /login, comenzando por Bearer.',
|
|
68
|
+
'ui.flow.why.auth.token_expired': 'El token expiró.',
|
|
69
|
+
'ui.flow.how.auth.token_expired': 'Inicie sesión de nuevo en POST /login para generar otro token.',
|
|
70
|
+
'ui.flow.why.auth.token_invalid': 'El token no es válido: fue alterado, está incompleto o no fue generado por este servidor.',
|
|
71
|
+
'ui.flow.how.auth.token_invalid': 'Inicie sesión en POST /login y use el token devuelto, sin cambios.',
|
|
72
|
+
'ui.flow.why.auth.user_not_found': 'El usuario dueño del token ya no existe. Esto ocurre cuando se elimina o cuando se reinician los datos del servidor.',
|
|
73
|
+
'ui.flow.how.auth.user_not_found': 'Registre el usuario de nuevo e inicie sesión para generar otro token.',
|
|
74
|
+
'ui.flow.why.auth.not_admin': 'El token es válido, pero el usuario no es administrador.',
|
|
75
|
+
'ui.flow.how.auth.not_admin': 'Use el token de un usuario registrado con administrador igual a true.',
|
|
76
|
+
'ui.flow.why.stage.auth': 'El token no fue aceptado.',
|
|
77
|
+
'ui.flow.how.stage.auth': 'Inicie sesión en POST /login y envíe el valor de authorization en el encabezado Authorization.',
|
|
78
|
+
'ui.flow.why.stage.schema': 'Uno o más datos enviados no tienen el formato esperado.',
|
|
79
|
+
'ui.flow.how.stage.schema': 'El cuerpo de la respuesta señala cada campo con problema.',
|
|
80
|
+
'ui.flow.why.stage.read': 'No se pudo completar la consulta. La causa más común es un registro que no existe.',
|
|
81
|
+
'ui.flow.how.stage.read': 'Revise el identificador informado.',
|
|
82
|
+
'ui.flow.why.stage.rules': 'No se cumplió una regla de negocio de la ruta.',
|
|
83
|
+
'ui.flow.how.stage.rules': 'El mensaje en el cuerpo de la respuesta indica qué regla fue.',
|
|
84
|
+
'ui.flow.why.rule.single_cart': 'Cada usuario puede tener un carrito a la vez, y este usuario ya tiene uno.',
|
|
85
|
+
'ui.flow.how.rule.single_cart': 'Concluya o cancele la compra actual antes de registrar otro carrito.',
|
|
86
|
+
'ui.flow.why.rule.no_duplicate_product': 'El mismo producto aparece más de una vez en la lista.',
|
|
87
|
+
'ui.flow.how.rule.no_duplicate_product': 'Envíe cada producto una sola vez y ajuste la cantidad.',
|
|
88
|
+
'ui.flow.why.rule.product_exists': 'Uno de los productos informados no existe. La verificación se hace ítem por ítem y se detiene en el primer problema, así que los ítems siguientes no fueron verificados.',
|
|
89
|
+
'ui.flow.how.rule.product_exists': 'Revise el idProduto. La ruta GET /produtos lista los productos registrados.',
|
|
90
|
+
'ui.flow.why.rule.enough_stock': 'La cantidad solicitada es mayor que el stock del producto. La verificación se hace ítem por ítem y se detiene en el primer problema, así que los ítems siguientes no fueron verificados.',
|
|
91
|
+
'ui.flow.how.rule.enough_stock': 'Reduzca la cantidad o elija otro producto. El cuerpo de la respuesta informa el stock actual.',
|
|
92
|
+
'ui.flow.why.stage.write': 'No se pudieron guardar los cambios.',
|
|
93
|
+
'ui.flow.how.stage.write': 'Intente de nuevo. Si el error continúa, abra una issue en el repositorio.',
|
|
94
|
+
'ui.flow.why.status.timeout': 'El servidor superó el tiempo límite para responder, y esta era la etapa en ejecución.',
|
|
95
|
+
'ui.flow.how.status.timeout': 'Intente de nuevo en unos instantes.',
|
|
96
|
+
'ui.flow.why.status.unexpected_error': 'El servidor encontró un error inesperado en esta etapa.',
|
|
97
|
+
'ui.flow.how.status.unexpected_error': 'Intente de nuevo. Si el error continúa, abra una issue en el repositorio.',
|
|
98
|
+
'ui.flow.why.outside': 'La respuesta vino de fuera de las etapas: la ruta no existe o la URL es inválida.',
|
|
99
|
+
'ui.flow.how.outside': 'Revise el método y la ruta de la solicitud.',
|
|
28
100
|
'info.description': '**ServeRest es una API REST gratuita que simula una tienda virtual para servir como material de estudio para pruebas de API.**\n\n**No olvides seguir al [autor del proyecto](https://github.com/PauloGoncalvesBH) y dar una estrella al repositorio: [github.com/ServeRest/ServeRest](https://github.com/ServeRest/ServeRest)**\n\nEsta página documenta todas las rutas y cómo acceder a ellas. Para más detalles sobre ServeRest (cómo ejecutar localmente usando Docker o NPM, cambiar el tiempo de expiración de autenticación, etc.) visita [el repositorio de ServeRest](https://github.com/serverest/serverest).\n\n¿Usas Postman? Importa el [JSON de Swagger](https://raw.githubusercontent.com/ServeRest/ServeRest/trunk/docs/swagger.json) para acceder a las colecciones.\n\n¿Vas a hacer pruebas de carga? Lee la sección \'[Pruebas de carga](https://github.com/ServeRest/ServeRest#teste-de-carga)\'.\n\n\nMuchas gracias ♥ a todos los que apoyan el proyecto [financieramente](https://opencollective.com/serverest#section-contributors) o [con código, ideas y difusión](https://github.com/ServeRest/ServeRest#contribuidores-), gracias a ustedes **más de R$ 2000,00 fueron donados a la ONG [Todas as letras](https://todasasletras.org/)** hasta el momento.\n\nServeRest tiene un front-end en beta, no dejes de conocerlo: [front.serverest.dev](https://front.serverest.dev).\n\n¿Necesitas ayuda? [Abre un issue](https://github.com/ServeRest/ServeRest/issues) o contacta al mantenedor del proyecto:\n',
|
|
29
101
|
'tags.login.description': 'Autentica a tu usuario para crear un carrito y, si eres administrador, gestionar los productos',
|
|
30
102
|
'tags.usuarios.description': 'Gestiona usuarios, consulta datos para inicio de sesión y crea administradores',
|
|
@@ -25,6 +25,78 @@ module.exports = {
|
|
|
25
25
|
'ui.download': 'Baixar',
|
|
26
26
|
'ui.available_authorizations': 'Autenticações disponíveis',
|
|
27
27
|
'ui.example_lower': 'exemplo',
|
|
28
|
+
'ui.flow.title': 'Fluxo da requisição',
|
|
29
|
+
'ui.flow.stopped_at': 'Parou em',
|
|
30
|
+
'ui.flow.completed': 'Passou por todas as etapas',
|
|
31
|
+
'ui.flow.outside': 'A resposta veio de fora das etapas',
|
|
32
|
+
'ui.flow.slow_motion': 'Câmera lenta',
|
|
33
|
+
'ui.flow.replay': 'Repetir',
|
|
34
|
+
'ui.flow.why_title': 'Por que parou aqui',
|
|
35
|
+
'ui.flow.how_title': 'Como seguir',
|
|
36
|
+
'ui.flow.stage_description_title': 'O que esta etapa faz',
|
|
37
|
+
'ui.flow.item_position': 'Índice do item na lista de produtos, contando a partir de zero:',
|
|
38
|
+
'ui.flow.status.ok': 'Passou',
|
|
39
|
+
'ui.flow.status.failed': 'Parou aqui',
|
|
40
|
+
'ui.flow.status.skipped': 'Não se aplica neste ambiente',
|
|
41
|
+
'ui.flow.status.not_run': 'Não executou',
|
|
42
|
+
'ui.flow.stage_label.body': 'Leitura do corpo',
|
|
43
|
+
'ui.flow.stage_label.rate_limit': 'Limite de requisições',
|
|
44
|
+
'ui.flow.stage_label.auth': 'Autenticação',
|
|
45
|
+
'ui.flow.stage_label.schema': 'Validação dos dados',
|
|
46
|
+
'ui.flow.stage_label.read': 'Consulta',
|
|
47
|
+
'ui.flow.stage_label.rules': 'Regras de negócio',
|
|
48
|
+
'ui.flow.stage_label.write': 'Gravação',
|
|
49
|
+
'ui.flow.stage_description.body': 'O servidor lê e interpreta o corpo da requisição.',
|
|
50
|
+
'ui.flow.stage_description.rate_limit': 'O servidor confere se o cliente não passou do limite de requisições.',
|
|
51
|
+
'ui.flow.stage_description.auth': 'O servidor confere o token enviado no header Authorization.',
|
|
52
|
+
'ui.flow.stage_description.schema': 'O servidor confere se os dados enviados têm o formato esperado.',
|
|
53
|
+
'ui.flow.stage_description.read': 'O servidor busca os dados pedidos.',
|
|
54
|
+
'ui.flow.stage_description.rules': 'O servidor aplica as regras de negócio da rota.',
|
|
55
|
+
'ui.flow.stage_description.write': 'O servidor grava as alterações.',
|
|
56
|
+
'ui.flow.rule_label.single_cart': 'Um carrinho por usuário',
|
|
57
|
+
'ui.flow.rule_label.no_duplicate_product': 'Produtos sem repetição',
|
|
58
|
+
'ui.flow.rule_label.product_exists': 'Existência dos produtos',
|
|
59
|
+
'ui.flow.rule_label.enough_stock': 'Estoque dos produtos',
|
|
60
|
+
'ui.flow.why.stage.body': 'O corpo da requisição não pôde ser lido. O caso mais comum é um JSON malformado.',
|
|
61
|
+
'ui.flow.how.stage.body': 'Confira as aspas, as vírgulas e as chaves do JSON enviado.',
|
|
62
|
+
'ui.flow.why.stage.rate_limit': 'Foram enviadas requisições demais em pouco tempo.',
|
|
63
|
+
'ui.flow.how.stage.rate_limit': 'Aguarde alguns segundos e tente de novo. Este ambiente não aceita teste de carga.',
|
|
64
|
+
'ui.flow.why.auth.token_missing': 'A requisição não trouxe o header Authorization.',
|
|
65
|
+
'ui.flow.how.auth.token_missing': 'Faça login em POST /login e envie o valor de authorization no header Authorization.',
|
|
66
|
+
'ui.flow.why.auth.bad_format': 'O header Authorization não está no formato Bearer seguido do token.',
|
|
67
|
+
'ui.flow.how.auth.bad_format': 'Envie o valor exatamente como POST /login devolve, começando por Bearer.',
|
|
68
|
+
'ui.flow.why.auth.token_expired': 'O token expirou.',
|
|
69
|
+
'ui.flow.how.auth.token_expired': 'Faça login de novo em POST /login para gerar outro token.',
|
|
70
|
+
'ui.flow.why.auth.token_invalid': 'O token não é válido: foi alterado, está incompleto ou não foi gerado por este servidor.',
|
|
71
|
+
'ui.flow.how.auth.token_invalid': 'Faça login em POST /login e use o token devolvido, sem alterações.',
|
|
72
|
+
'ui.flow.why.auth.user_not_found': 'O usuário dono do token não existe mais. Isso acontece quando ele é excluído ou quando os dados do servidor são reiniciados.',
|
|
73
|
+
'ui.flow.how.auth.user_not_found': 'Cadastre o usuário de novo e faça login para gerar outro token.',
|
|
74
|
+
'ui.flow.why.auth.not_admin': 'O token é válido, mas o usuário não é administrador.',
|
|
75
|
+
'ui.flow.how.auth.not_admin': 'Use o token de um usuário cadastrado com administrador igual a true.',
|
|
76
|
+
'ui.flow.why.stage.auth': 'O token não foi aceito.',
|
|
77
|
+
'ui.flow.how.stage.auth': 'Faça login em POST /login e envie o valor de authorization no header Authorization.',
|
|
78
|
+
'ui.flow.why.stage.schema': 'Um ou mais dados enviados não têm o formato esperado.',
|
|
79
|
+
'ui.flow.how.stage.schema': 'O corpo da resposta aponta cada campo com problema.',
|
|
80
|
+
'ui.flow.why.stage.read': 'A consulta não pôde ser concluída. O caso mais comum é um registro que não existe.',
|
|
81
|
+
'ui.flow.how.stage.read': 'Confira o identificador informado.',
|
|
82
|
+
'ui.flow.why.stage.rules': 'Uma regra de negócio da rota não foi atendida.',
|
|
83
|
+
'ui.flow.how.stage.rules': 'A mensagem no corpo da resposta diz qual regra foi.',
|
|
84
|
+
'ui.flow.why.rule.single_cart': 'Cada usuário pode ter um carrinho por vez, e este usuário já tem um.',
|
|
85
|
+
'ui.flow.how.rule.single_cart': 'Conclua ou cancele a compra atual antes de cadastrar outro carrinho.',
|
|
86
|
+
'ui.flow.why.rule.no_duplicate_product': 'O mesmo produto aparece mais de uma vez na lista.',
|
|
87
|
+
'ui.flow.how.rule.no_duplicate_product': 'Envie cada produto uma única vez e ajuste a quantidade.',
|
|
88
|
+
'ui.flow.why.rule.product_exists': 'Um dos produtos informados não existe. A conferência é feita item a item e para no primeiro problema, então os itens seguintes não foram conferidos.',
|
|
89
|
+
'ui.flow.how.rule.product_exists': 'Confira o idProduto. A rota GET /produtos lista os produtos cadastrados.',
|
|
90
|
+
'ui.flow.why.rule.enough_stock': 'A quantidade pedida é maior que o estoque do produto. A conferência é feita item a item e para no primeiro problema, então os itens seguintes não foram conferidos.',
|
|
91
|
+
'ui.flow.how.rule.enough_stock': 'Reduza a quantidade ou escolha outro produto. O corpo da resposta informa o estoque atual.',
|
|
92
|
+
'ui.flow.why.stage.write': 'A gravação não pôde ser concluída.',
|
|
93
|
+
'ui.flow.how.stage.write': 'Tente de novo. Se o erro continuar, abra uma issue no repositório.',
|
|
94
|
+
'ui.flow.why.status.timeout': 'O servidor passou do tempo limite para responder, e esta era a etapa em execução.',
|
|
95
|
+
'ui.flow.how.status.timeout': 'Tente de novo em instantes.',
|
|
96
|
+
'ui.flow.why.status.unexpected_error': 'O servidor encontrou um erro inesperado nesta etapa.',
|
|
97
|
+
'ui.flow.how.status.unexpected_error': 'Tente de novo. Se o erro continuar, abra uma issue no repositório.',
|
|
98
|
+
'ui.flow.why.outside': 'A resposta veio de fora das etapas: a rota não existe ou a URL é inválida.',
|
|
99
|
+
'ui.flow.how.outside': 'Confira o método e o caminho da requisição.',
|
|
28
100
|
'info.description': '**O ServeRest é uma API REST gratuita que simula uma loja virtual com intuito de servir de material de estudos de testes de API.**\n\n**Não deixe de seguir o [autor do projeto](https://github.com/PauloGoncalvesBH) e deixar um star no repositório: [github.com/ServeRest/ServeRest](https://github.com/ServeRest/ServeRest)**\n\nEssa página documenta todas as rotas e como acessá-las. Para mais detalhes do ServeRest (como executar localmente utilizando Docker ou NPM, alterar timeout de autenticação, etc) acesse [o repositório do ServeRest](https://github.com/serverest/serverest).\n\nEstá utilizando Postman? Importe o [JSON do Swagger](https://raw.githubusercontent.com/ServeRest/ServeRest/trunk/docs/swagger.json) para ter acesso às collections.\n\nVai fazer teste de carga? Leia a seção \'[Teste de Carga](https://github.com/ServeRest/ServeRest#teste-de-carga)\'.\n\n\nMuito obrigado ♥ a todos que apoiam o projeto [financeiramente](https://opencollective.com/serverest#section-contributors) ou [com código, ideias e divulgação](https://github.com/ServeRest/ServeRest#contribuidores-), graças a vocês **mais de R$ 2000,00 foram doados para a ONG [Todas as letras](https://todasasletras.org/)** até o momento.\n\nO ServeRest possui um front, com status em beta, não deixe de conhecer: [front.serverest.dev](https://front.serverest.dev).\n\nPrecisa de apoio? [Abra uma issue](https://github.com/ServeRest/ServeRest/issues) ou contate o mantenedor do projeto:\n',
|
|
29
101
|
'tags.login.description': 'Autentique o seu usuário para montar um carrinho e, se for administrador, gerenciar os produtos',
|
|
30
102
|
'tags.usuarios.description': 'Gerencie os usuários, consulte dados para login e cadastre administrador',
|