@opencraw/mcp 0.1.45 → 0.1.48
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 +28 -1
- package/dist/diff-tool/diff-tool.handler.d.ts +42 -0
- package/dist/diff-tool/index.d.ts +3 -0
- package/dist/docs-tool/docs-folder.repository.d.ts +24 -0
- package/dist/docs-tool/docs-root.client.d.ts +8 -0
- package/dist/docs-tool/docs-tool.handler.d.ts +21 -0
- package/dist/docs-tool/docs-topics.algorithm.d.ts +39 -0
- package/dist/docs-tool/index.d.ts +3 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.esm.js +517 -1
- package/dist/list-tool/index.d.ts +3 -0
- package/dist/list-tool/list-recipes-tool.handler.d.ts +32 -0
- package/dist/main.d.ts +7 -0
- package/dist/probe-tool/index.d.ts +2 -0
- package/dist/probe-tool/probe-tool.handler.d.ts +27 -0
- package/dist/recipe-source/index.d.ts +3 -0
- package/dist/recipe-source/recipe-source.mapper.d.ts +21 -0
- package/dist/run-tool/index.d.ts +3 -0
- package/dist/run-tool/run-tool.handler.d.ts +51 -0
- package/dist/server/create-server.use-case.d.ts +11 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/src/docs-tool/docs-folder.repository.d.ts +24 -0
- package/dist/src/docs-tool/docs-root.client.d.ts +8 -0
- package/dist/src/docs-tool/docs-tool.handler.d.ts +21 -0
- package/dist/src/docs-tool/docs-topics.algorithm.d.ts +39 -0
- package/dist/src/docs-tool/index.d.ts +3 -0
- package/dist/src/server/create-server.use-case.d.ts +1 -1
- package/dist/src/studio-tool/index.d.ts +2 -0
- package/dist/src/studio-tool/studio-command.client.d.ts +11 -0
- package/dist/src/studio-tool/studio-instance.repository.d.ts +22 -0
- package/dist/src/studio-tool/studio-tools.handler.d.ts +68 -0
- package/dist/studio-tool/index.d.ts +2 -0
- package/dist/studio-tool/studio-command.client.d.ts +11 -0
- package/dist/studio-tool/studio-instance.repository.d.ts +22 -0
- package/dist/studio-tool/studio-tools.handler.d.ts +68 -0
- package/dist/validate-tool/index.d.ts +3 -0
- package/dist/validate-tool/validate-tool.handler.d.ts +31 -0
- package/docs/how-it-works/01-recipes.md +698 -0
- package/docs/how-it-works/02-page-steps.md +672 -0
- package/docs/how-it-works/03-data-steps.md +813 -0
- package/docs/how-it-works/04-flow-steps.md +738 -0
- package/docs/how-it-works/05-templates-and-scope.md +428 -0
- package/docs/how-it-works/06-documents.md +1413 -0
- package/docs/how-it-works/07-mapping-and-transforms.md +699 -0
- package/docs/how-it-works/08-policies.md +405 -0
- package/docs/how-it-works/09-running.md +804 -0
- package/docs/how-it-works/README.md +225 -0
- package/docs/recipes/README.md +22 -0
- package/docs/recipes/access.md +440 -0
- package/docs/recipes/authoring.md +1503 -0
- package/docs/recipes/azure-durable.md +180 -0
- package/docs/recipes/captcha.md +204 -0
- package/docs/recipes/movies-example.md +12 -0
- package/docs/recipes/programmatic-authoring.md +101 -0
- package/docs/recipes/quick-guide.md +157 -0
- package/docs/recipes/vehicles-example.md +9 -0
- package/docs/recipes/worker-mode.md +221 -0
- package/docs/studio/manual.md +314 -0
- package/docs/studio/pdf-agreements.md +150 -0
- package/package.json +29 -4
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="../assets/opencraw-logo.svg" alt="OpenCraw" width="360">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Policies
|
|
6
|
+
|
|
7
|
+
[← How OpenCraw works](README.md) · Previous: [8. Mapping and transforms](07-mapping-and-transforms.md) · Next: [10. Running](09-running.md)
|
|
8
|
+
|
|
9
|
+
After its transforms, each field's value is coerced to the field's type and validated. Four things can go
|
|
10
|
+
wrong on the way, and each one ends in one of two ways:
|
|
11
|
+
|
|
12
|
+
| What went wrong | Example | Who decides |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| The value is **missing** | a season with no overtime losses recorded | the missing-value policy, in full precedence |
|
|
15
|
+
| The value **can't be coerced** | a relative link into a `url` field | the same policy, in full precedence |
|
|
16
|
+
| The value **fails validation** | `3` in a field with `min: 5` | the same policy, but only `skip-record` helps |
|
|
17
|
+
| A **transform throws** | the `integer` transform on an empty cell, a hook that throws | the mapping rule's own `onMissing` only |
|
|
18
|
+
|
|
19
|
+
The two endings: the record is **rejected** (`RecordRejectedError`: it is dropped, counted, and the walk goes
|
|
20
|
+
on), or the mapping **fails** (`MappingFailedError`: the step that emitted fails, and by default the recipe
|
|
21
|
+
stops). This page shows each case on real data. The reference is
|
|
22
|
+
[authoring.md §7](../recipes/authoring.md#7-policies-what-happens-when-something-is-missing-or-fails).
|
|
23
|
+
|
|
24
|
+
## What counts as missing
|
|
25
|
+
|
|
26
|
+
A value is missing when it is `undefined` (the path isn't there, `regex` found no match, `first` of an empty
|
|
27
|
+
list), `null`, or `""`. An empty list is a value. An `object` field that ends up with no members is missing too.
|
|
28
|
+
|
|
29
|
+
Blank text, `""` or only spaces, is missing in a field of every type except `string` and `json`. An empty table
|
|
30
|
+
cell mapped into an `integer`, `number`, `currency`, `date`, `boolean`, `url` or `enum` field is missing, so the
|
|
31
|
+
field's `default`, `null` or its policy applies: it is never a coercion error, and a `boolean` field doesn't turn
|
|
32
|
+
it into `false`. A `string` or `json` field keeps text as it is, so there `""` is missing and `" "` is a value.
|
|
33
|
+
The [precedence](#the-precedence) scene below maps an empty cell into `integer` fields with no transform.
|
|
34
|
+
|
|
35
|
+
An explicit `number` or `integer` **transform** is another matter: it is asked to read a number, and blank text
|
|
36
|
+
has none, so it throws (see [transform failures](#transform-failures-look-only-at-the-rule)).
|
|
37
|
+
|
|
38
|
+
## The precedence
|
|
39
|
+
|
|
40
|
+
When a field's value is missing, the policy comes from the first of these that is set:
|
|
41
|
+
|
|
42
|
+
1. the mapping rule's `onMissing` (in the input recipe);
|
|
43
|
+
2. the field's `onMissing` (in the output recipe);
|
|
44
|
+
3. `default`, when the field has a `default` value;
|
|
45
|
+
4. the output recipe's `onMissing`;
|
|
46
|
+
5. built in: `fail` when the field is `required`, `null` otherwise.
|
|
47
|
+
|
|
48
|
+
The rule comes first because it belongs to one site: an input recipe can relax or tighten a field for its own
|
|
49
|
+
source without changing the shared output recipe.
|
|
50
|
+
|
|
51
|
+
The hockey table on [scrapethissite.com](https://www.scrapethissite.com/pages/forms/?q=Boston) leaves the "OT
|
|
52
|
+
Losses" column empty before 1999, when the league started counting overtime losses. The scene reads Boston's
|
|
53
|
+
seasons from 1997 to 2001 ([recipes](recipes/policy-precedence)) into an output recipe whose `onMissing` is
|
|
54
|
+
`skip-record`, with four `integer` fields fed from the same cell, with no transform. Before 1999 the cell is
|
|
55
|
+
empty, and blank text in an `integer` field is missing:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
"onMissing": "skip-record",
|
|
59
|
+
"fields": {
|
|
60
|
+
"otLosses": { "type": "integer", "default": 0 },
|
|
61
|
+
"otLossesField": { "type": "integer", "default": 0, "onMissing": "null" },
|
|
62
|
+
"otLossesRule": { "type": "integer", "default": 0, "onMissing": "default" },
|
|
63
|
+
"otLossesNote": { "type": "integer", "default": "-1" }
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
and in the input recipe, `"otLossesRule": { "from": "ot", "onMissing": "null" }`.
|
|
68
|
+
|
|
69
|
+
<!-- capture:policy-precedence records n=3 -->
|
|
70
|
+
```json
|
|
71
|
+
{"team":"Boston Bruins","year":1997,"otLosses":0,"otLossesField":null,"otLossesRule":null,"otLossesNote":-1}
|
|
72
|
+
{"team":"Boston Bruins","year":1998,"otLosses":0,"otLossesField":null,"otLossesRule":null,"otLossesNote":-1}
|
|
73
|
+
{"team":"Boston Bruins","year":1999,"otLosses":6,"otLossesField":6,"otLossesRule":6,"otLossesNote":6}
|
|
74
|
+
```
|
|
75
|
+
<!-- /capture -->
|
|
76
|
+
|
|
77
|
+
<!-- capture:policy-precedence mapping fields=otLosses,otLossesRule record=0 -->
|
|
78
|
+
| Field | Step | Value |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `otLosses` | read | `""` |
|
|
81
|
+
| | **field** | `0` |
|
|
82
|
+
| `otLossesRule` | read | `""` |
|
|
83
|
+
| | **field** | `null` |
|
|
84
|
+
<!-- /capture -->
|
|
85
|
+
|
|
86
|
+
For 1997 and 1998, with the cell empty:
|
|
87
|
+
|
|
88
|
+
- `otLosses`: no rule or field policy, and the field has a `default`, so step 3 applies: `0`. The output
|
|
89
|
+
recipe's `skip-record` (step 4) is never reached, which is why no season is dropped.
|
|
90
|
+
- `otLossesField`: the field's own `onMissing: "null"` (step 2) comes before its `default`: `null`.
|
|
91
|
+
- `otLossesRule`: the field says `default`, the rule says `null`, and the rule wins (step 1): `null`.
|
|
92
|
+
- `otLossesNote`: step 3 again, with the `default` written as the text `"-1"`. A `default` is coerced to its
|
|
93
|
+
field's type like a mapped value, so the field holds the integer `-1`.
|
|
94
|
+
|
|
95
|
+
From 1999 the cell has a number, and no policy is consulted.
|
|
96
|
+
|
|
97
|
+
The loader coerces and validates every `default` before anything runs. With `"default": "not recorded"` on
|
|
98
|
+
`otLossesNote`, `loadRecipeSet` rejects the set with a `RecipeBindingError`, and no page is requested:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
recipes do not bind
|
|
102
|
+
ot-season fields.otLossesNote.default: "not recorded" is not a valid default: integer field: no number in "not recorded"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
A `default` that breaks the field's `min`, `max` or `pattern` fails the same way, and so does
|
|
106
|
+
`onMissing: "default"` on a field (or a rule) whose field has no `default`.
|
|
107
|
+
|
|
108
|
+
## The four policies
|
|
109
|
+
|
|
110
|
+
| Policy | Effect on a missing value |
|
|
111
|
+
|---|---|
|
|
112
|
+
| `skip-record` | `RecordRejectedError`: the record is dropped, `rejected` is counted, a `record:reject` event (`✖` in the trace) names the field, and the walk goes on. |
|
|
113
|
+
| `fail` | `MappingFailedError`: the step that emitted fails, and goes through its `onError`. |
|
|
114
|
+
| `null` | The field is `null`, when it is `nullable` or not `required`. A required field that isn't `nullable` fails instead. |
|
|
115
|
+
| `default` | The field's `default`, coerced to the field's type (`"0"` in a `number` field is `0`). The loader checks it: a `default` the field can't take, or a `default` policy on a field without one, is a `RecipeBindingError`. |
|
|
116
|
+
|
|
117
|
+
The output recipe's own `onMissing` takes only `fail`, `skip-record` or `null`; `default` belongs to a field.
|
|
118
|
+
|
|
119
|
+
## One missing tag, three outcomes
|
|
120
|
+
|
|
121
|
+
Page 3 of the quotes API on [quotes.toscrape.com](https://quotes.toscrape.com/api/quotes?page=3) has a quote
|
|
122
|
+
with no tags: its eighth, by J.K. Rowling. Three input recipes read pages 3 and 4 into the same output recipe,
|
|
123
|
+
whose `mainTag` is `required` and mapped with `first` on the tags, so it is missing for that quote
|
|
124
|
+
([recipes](recipes/policy-quotes)):
|
|
125
|
+
|
|
126
|
+
- `quotes-reject` puts `"onMissing": "skip-record"` on the `mainTag` rule;
|
|
127
|
+
- `quotes-skip-step` has no policy, and `"onError": { "policy": "skip" }` on the emitting `forEach`;
|
|
128
|
+
- `quotes-stop` has no policy anywhere, so the built-in `fail` applies.
|
|
129
|
+
|
|
130
|
+
<!-- capture:policy-quotes summary -->
|
|
131
|
+
```text
|
|
132
|
+
quotes-reject: 19 emitted, 1 rejected, 0 duplicates, 2 pages
|
|
133
|
+
quotes-skip-step: 17 emitted, 0 rejected, 0 duplicates, 2 pages
|
|
134
|
+
quotes-stop: 7 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.2 (forEach) failed: mapping failed: mainTag: missing
|
|
135
|
+
```
|
|
136
|
+
<!-- /capture -->
|
|
137
|
+
|
|
138
|
+
<!-- capture:policy-quotes trace grep=✖|↷|■ -->
|
|
139
|
+
```text
|
|
140
|
+
✖ record rejected: mainTag: missing
|
|
141
|
+
■ quotes-reject: 19 emitted, 1 rejected, 0 duplicates, 2 pages, … ms
|
|
142
|
+
↷ steps.2 forEach skipped: mapping failed: mainTag: missing
|
|
143
|
+
■ quotes-skip-step: 17 emitted, 0 rejected, 0 duplicates, 1 steps skipped, 2 pages, … ms
|
|
144
|
+
✖ step steps.2 (forEach) failed: mapping failed: mainTag: missing
|
|
145
|
+
■ quotes-stop: 7 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
146
|
+
✖ stopped: step steps.2 (forEach) failed: mapping failed: mainTag: missing
|
|
147
|
+
```
|
|
148
|
+
<!-- /capture -->
|
|
149
|
+
|
|
150
|
+
**`quotes-reject`: the record is rejected.** The quote is dropped and counted, and the walk goes on to the
|
|
151
|
+
next quote and to page 4: 19 records and 1 rejected. The captured records show the rejected one in its place,
|
|
152
|
+
with the field and the reason:
|
|
153
|
+
|
|
154
|
+
<!-- capture:policy-quotes records n=9 -->
|
|
155
|
+
```json
|
|
156
|
+
{"text":"“I love you without knowing how, or when, or from where. I love you simply, without problems or pride: I love you in this way because I do not know any other…","author":"Pablo Neruda","mainTag":"love"}
|
|
157
|
+
{"text":"“For every minute you are angry you lose sixty seconds of happiness.”","author":"Ralph Waldo Emerson","mainTag":"happiness"}
|
|
158
|
+
{"text":"“If you judge people, you have no time to love them.”","author":"Mother Teresa","mainTag":"attributed-no-source"}
|
|
159
|
+
{"text":"“Anyone who thinks sitting in church can make you a Christian must also think that sitting in a garage can make you a car.”","author":"Garrison Keillor","mainTag":"humor"}
|
|
160
|
+
{"text":"“Beauty is in the eye of the beholder and it may be necessary from time to time to give a stupid or misinformed beholder a black eye.”","author":"Jim Henson","mainTag":"humor"}
|
|
161
|
+
{"text":"“Today you are You, that is truer than true. There is no one alive who is Youer than You.”","author":"Dr. Seuss","mainTag":"comedy"}
|
|
162
|
+
{"text":"“If you want your children to be intelligent, read them fairy tales. If you want them to be more intelligent, read them more fairy tales.”","author":"Albert Einstein","mainTag":"children"}
|
|
163
|
+
{"rejected":{"field":"mainTag","reason":"missing"}}
|
|
164
|
+
{"text":"“Logic will get you from A to Z; imagination will get you everywhere.”","author":"Albert Einstein","mainTag":"imagination"}
|
|
165
|
+
```
|
|
166
|
+
<!-- /capture -->
|
|
167
|
+
|
|
168
|
+
<!-- capture:policy-quotes trace lines=14 -->
|
|
169
|
+
```text
|
|
170
|
+
▶ quotes-reject (api)
|
|
171
|
+
⇄ access direct (direct)
|
|
172
|
+
⇢ page 1 https://quotes.toscrape.com/api/quotes?page=3
|
|
173
|
+
· steps.0 request … ms
|
|
174
|
+
· steps.1 extract quotes … ms
|
|
175
|
+
✚ record (no key)
|
|
176
|
+
✚ record (no key)
|
|
177
|
+
✚ record (no key)
|
|
178
|
+
✚ record (no key)
|
|
179
|
+
✚ record (no key)
|
|
180
|
+
✚ record (no key)
|
|
181
|
+
✚ record (no key)
|
|
182
|
+
✖ record rejected: mainTag: missing
|
|
183
|
+
✚ record (no key)
|
|
184
|
+
…
|
|
185
|
+
```
|
|
186
|
+
<!-- /capture -->
|
|
187
|
+
|
|
188
|
+
**`quotes-stop`: the mapping fails, and the recipe stops.** A `MappingFailedError` isn't handled where the
|
|
189
|
+
record is made. It rejects inside the step that emitted, here the `forEach`, which fails as a whole, not only
|
|
190
|
+
the one iteration. The `forEach` has no `onError`, and neither has the recipe, so the policy is `fail`: the
|
|
191
|
+
recipe stops with the error in its report (`errorKind: "mapping"`) and a `✖ stopped` line. The seven records
|
|
192
|
+
written before stay in the sink. Page 4 is never read: it is a second start point of the same recipe, and the
|
|
193
|
+
recipe has stopped. Another input recipe in the same run still runs, unless the crawler was created with
|
|
194
|
+
`onRecipeError: "stop"`.
|
|
195
|
+
|
|
196
|
+
**`quotes-skip-step`: the step is skipped.** With `onError: skip` on the `forEach`, the failure becomes a
|
|
197
|
+
`step:skip` (`↷`). The walk goes on after the `forEach`, and page 4 is read. But the whole `forEach` was
|
|
198
|
+
skipped: the two quotes after Rowling's on page 3 were never mapped, so the recipe has 17 records, not 19. Use
|
|
199
|
+
`skip-record` to drop one record; `onError` acts on steps. (The `summary` above doesn't count skipped steps;
|
|
200
|
+
the `■` line in the trace does.)
|
|
201
|
+
|
|
202
|
+
## Coercion failures follow the full precedence
|
|
203
|
+
|
|
204
|
+
A value that can't be converted to the field's type goes through the same precedence as a missing one (fixed
|
|
205
|
+
in [#65](https://github.com/Open-Craw/OpenCraw/issues/65): before, the output recipe's `onMissing` was
|
|
206
|
+
ignored). If it resolves to `skip-record`, the record is rejected. **Anything else fails the mapping**: a
|
|
207
|
+
`null` or `default` policy doesn't replace a value that is there but wrong.
|
|
208
|
+
|
|
209
|
+
The scene maps the first three links of books.toscrape.com's Mystery category, as the page writes them
|
|
210
|
+
(`../../../sharp-objects_997/index.html`), into a `url` field, which needs an absolute URL
|
|
211
|
+
([recipes](recipes/policy-coercion)). The output recipe's `onMissing` is `skip-record`. `link-as-is` maps the
|
|
212
|
+
link as it is; `link-null` adds `"onMissing": "null"` to the rule:
|
|
213
|
+
|
|
214
|
+
<!-- capture:policy-coercion summary -->
|
|
215
|
+
```text
|
|
216
|
+
link-as-is: 0 emitted, 3 rejected, 0 duplicates, 1 pages
|
|
217
|
+
link-null: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.2 (forEach) failed: mapping failed: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
|
|
218
|
+
```
|
|
219
|
+
<!-- /capture -->
|
|
220
|
+
|
|
221
|
+
<!-- capture:policy-coercion records n=3 -->
|
|
222
|
+
```json
|
|
223
|
+
{"rejected":{"field":"url","reason":"\"../../../sharp-objects_997/index.html\" is not an absolute URL (use the absoluteUrl transform)"}}
|
|
224
|
+
{"rejected":{"field":"url","reason":"\"../../../in-a-dark-dark-wood_963/index.html\" is not an absolute URL (use the absoluteUrl transform)"}}
|
|
225
|
+
{"rejected":{"field":"url","reason":"\"../../../the-past-never-ends_942/index.html\" is not an absolute URL (use the absoluteUrl transform)"}}
|
|
226
|
+
```
|
|
227
|
+
<!-- /capture -->
|
|
228
|
+
|
|
229
|
+
<!-- capture:policy-coercion trace grep=▶|✖ -->
|
|
230
|
+
```text
|
|
231
|
+
▶ link-as-is (api)
|
|
232
|
+
✖ record rejected: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
|
|
233
|
+
✖ record rejected: url: "../../../in-a-dark-dark-wood_963/index.html" is not an absolute URL (use the absoluteUrl transform)
|
|
234
|
+
✖ record rejected: url: "../../../the-past-never-ends_942/index.html" is not an absolute URL (use the absoluteUrl transform)
|
|
235
|
+
▶ link-null (api)
|
|
236
|
+
✖ step steps.2 (forEach) failed: mapping failed: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
|
|
237
|
+
■ link-null: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
238
|
+
✖ stopped: step steps.2 (forEach) failed: mapping failed: url: "../../../sharp-objects_997/index.html" is not an absolute URL (use the absoluteUrl transform)
|
|
239
|
+
```
|
|
240
|
+
<!-- /capture -->
|
|
241
|
+
|
|
242
|
+
For `link-as-is`, nothing is set on the rule or the field and there is no `default`, so the output recipe's
|
|
243
|
+
`skip-record` applies and each book is rejected. For `link-null`, the rule says `null`, which isn't
|
|
244
|
+
`skip-record`, so the first book fails the mapping and the recipe stops. The fix is the `absoluteUrl`
|
|
245
|
+
transform, which the error message names. The rejection holds the field and the reason apart; the trace line
|
|
246
|
+
joins them, with the field's path once.
|
|
247
|
+
|
|
248
|
+
## Transform failures look only at the rule
|
|
249
|
+
|
|
250
|
+
When a transform throws, the only policy consulted is the mapping rule's own `onMissing`, and only
|
|
251
|
+
`skip-record` counts: it reads as "without this field the record is worthless". The field's policy, its
|
|
252
|
+
`default` and the output recipe's `onMissing` are not consulted, and the mapping fails.
|
|
253
|
+
|
|
254
|
+
The same hockey seasons, 1997 to 2001, into an output recipe whose `onMissing` is `skip-record` and whose
|
|
255
|
+
`otLosses` is an `integer` field. Three input recipes map the empty cell three ways
|
|
256
|
+
([recipes](recipes/policy-transform)):
|
|
257
|
+
|
|
258
|
+
- `ot-integer-skip`: `integer` transform, `"onMissing": "skip-record"` on the rule;
|
|
259
|
+
- `ot-integer`: `integer` transform, nothing else;
|
|
260
|
+
- `ot-plain`: no transform.
|
|
261
|
+
|
|
262
|
+
<!-- capture:policy-transform summary -->
|
|
263
|
+
```text
|
|
264
|
+
ot-integer-skip: 3 emitted, 2 rejected, 0 duplicates, 1 pages
|
|
265
|
+
ot-integer: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.2 (forEach) failed: mapping failed: otLosses: transform "integer": no number in ""
|
|
266
|
+
ot-plain: 3 emitted, 2 rejected, 0 duplicates, 1 pages
|
|
267
|
+
```
|
|
268
|
+
<!-- /capture -->
|
|
269
|
+
|
|
270
|
+
<!-- capture:policy-transform trace grep=▶|✖ -->
|
|
271
|
+
```text
|
|
272
|
+
▶ ot-integer-skip (api)
|
|
273
|
+
✖ record rejected: otLosses: transform "integer": no number in ""
|
|
274
|
+
✖ record rejected: otLosses: transform "integer": no number in ""
|
|
275
|
+
▶ ot-integer (api)
|
|
276
|
+
✖ step steps.2 (forEach) failed: mapping failed: otLosses: transform "integer": no number in ""
|
|
277
|
+
■ ot-integer: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
278
|
+
✖ stopped: step steps.2 (forEach) failed: mapping failed: otLosses: transform "integer": no number in ""
|
|
279
|
+
▶ ot-plain (api)
|
|
280
|
+
✖ record rejected: otLosses: missing
|
|
281
|
+
✖ record rejected: otLosses: missing
|
|
282
|
+
```
|
|
283
|
+
<!-- /capture -->
|
|
284
|
+
|
|
285
|
+
<!-- capture:policy-transform records n=5 -->
|
|
286
|
+
```json
|
|
287
|
+
{"rejected":{"field":"otLosses","reason":"transform \"integer\": no number in \"\""}}
|
|
288
|
+
{"rejected":{"field":"otLosses","reason":"transform \"integer\": no number in \"\""}}
|
|
289
|
+
{"team":"Boston Bruins","year":1999,"otLosses":6}
|
|
290
|
+
{"team":"Boston Bruins","year":2000,"otLosses":8}
|
|
291
|
+
{"team":"Boston Bruins","year":2001,"otLosses":9}
|
|
292
|
+
```
|
|
293
|
+
<!-- /capture -->
|
|
294
|
+
|
|
295
|
+
- `ot-integer-skip`: the `integer` transform throws on `""`: it was asked for a number and there is none. The
|
|
296
|
+
rule says `skip-record`, so 1997 and 1998 are rejected, and 1999 to 2001 kept.
|
|
297
|
+
- `ot-integer`: the same error, and the rule says nothing. The output recipe's `skip-record` isn't consulted
|
|
298
|
+
for a transform failure, so the first season stops the recipe.
|
|
299
|
+
- `ot-plain`: no transform, so no transform failure. `""` reaches the `integer` field, where blank text is
|
|
300
|
+
missing ([What counts as missing](#what-counts-as-missing)). Nothing on the rule or the field, and no
|
|
301
|
+
`default`, so the output recipe's `skip-record` applies: the two seasons are rejected as `missing`.
|
|
302
|
+
|
|
303
|
+
An `integer` field reads the number itself, so a transform that only does the same adds a way to fail. Keep the
|
|
304
|
+
`number` transform for what the field can't do, such as reading with a `locale`, or before a step that needs a
|
|
305
|
+
number (`sum`), and give its rule `skip-record` when the cell can be empty.
|
|
306
|
+
|
|
307
|
+
## Validation failures
|
|
308
|
+
|
|
309
|
+
After coercion, a present value is checked against the field's rules: `min` and `max` for numbers,
|
|
310
|
+
`minLength`, `maxLength` and `pattern` for text, `minLength` and `maxLength` for arrays
|
|
311
|
+
([authoring.md §1.1](../recipes/authoring.md#11-field-spec)). A value that breaks one is resolved like a
|
|
312
|
+
coercion failure: the policy in full precedence, `skip-record` rejects the record, anything else fails the
|
|
313
|
+
mapping (`mapping failed: n: 3 is below the minimum 5`). None of this guide's sites gave a value out of range,
|
|
314
|
+
so there is no captured example.
|
|
315
|
+
|
|
316
|
+
## Rejected or failed
|
|
317
|
+
|
|
318
|
+
| | `RecordRejectedError` | `MappingFailedError` |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| Raised by | `skip-record` for a missing, unconvertible or invalid value; a transform failure with the rule's `skip-record` | every other policy for those; a transform failure without the rule's `skip-record`; `each` over something that isn't a list |
|
|
321
|
+
| Message | `record rejected: <field>: <reason>` | `mapping failed: <field>: <reason>` |
|
|
322
|
+
| Event | `record:reject` with `field`, `reason`, and the snapshot under `debug` | with `fail`, `error` and then `recipe:finish` with `error`; with `skip`, `step:skip` |
|
|
323
|
+
| Report | `rejected` + 1 | with `fail`, `error` and `errorKind: "mapping"`; with `skip`, `stepsSkipped` + 1 |
|
|
324
|
+
| The walk | goes on; `maxRecords` counts only emitted records | the emitting step fails through its `onError`: `fail` (the default) stops the recipe, `skip` skips the whole step |
|
|
325
|
+
| Records written before | kept | kept |
|
|
326
|
+
|
|
327
|
+
## The items of `each`
|
|
328
|
+
|
|
329
|
+
An `each` rule builds a list of objects, and each member of each item goes through the same steps as a field:
|
|
330
|
+
coercion, validation, then the missing-value policy with its `default`. The precedence is the same too, with the
|
|
331
|
+
rule inside `each.fields` first. A problem is reported at the item's index (`variants[1].size`).
|
|
332
|
+
|
|
333
|
+
PokeAPI lists the abilities a Pokémon had in older generations. For Bulbasaur the one entry has
|
|
334
|
+
`"ability": null`, because its hidden ability didn't exist before generation V:
|
|
335
|
+
|
|
336
|
+
<!-- capture:policy-each-gap scope ids=past -->
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"past": [
|
|
340
|
+
{
|
|
341
|
+
"generation": {
|
|
342
|
+
"name": "generation-iv",
|
|
343
|
+
"url": "https://pokeapi.co/api/v2/generation/4/"
|
|
344
|
+
},
|
|
345
|
+
"abilities": [
|
|
346
|
+
{
|
|
347
|
+
"is_hidden": true,
|
|
348
|
+
"slot": 3,
|
|
349
|
+
"ability": null
|
|
350
|
+
}
|
|
351
|
+
]
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
<!-- /capture -->
|
|
357
|
+
|
|
358
|
+
The output recipe makes `name` required in each item ([recipes](recipes/policy-each-gap)). Two input recipes
|
|
359
|
+
map it: `pokeapi-past` sets no policy, and `pokeapi-past-skip` puts `"onMissing": "skip-record"` on the `name`
|
|
360
|
+
rule inside `each.fields`:
|
|
361
|
+
|
|
362
|
+
<!-- capture:policy-each-gap summary -->
|
|
363
|
+
```text
|
|
364
|
+
pokeapi-past-skip: 0 emitted, 1 rejected, 0 duplicates, 1 pages
|
|
365
|
+
pokeapi-past: 0 emitted, 0 rejected, 0 duplicates, 1 pages, stopped: step steps.3 (emit) failed: mapping failed: pastAbilities[0].name: missing
|
|
366
|
+
```
|
|
367
|
+
<!-- /capture -->
|
|
368
|
+
|
|
369
|
+
<!-- capture:policy-each-gap trace grep=✖ -->
|
|
370
|
+
```text
|
|
371
|
+
✖ record rejected: pastAbilities[0].name: missing
|
|
372
|
+
✖ step steps.3 (emit) failed: mapping failed: pastAbilities[0].name: missing
|
|
373
|
+
■ pokeapi-past: 0 emitted, 0 rejected, 0 duplicates, 1 pages, … ms
|
|
374
|
+
✖ stopped: step steps.3 (emit) failed: mapping failed: pastAbilities[0].name: missing
|
|
375
|
+
```
|
|
376
|
+
<!-- /capture -->
|
|
377
|
+
|
|
378
|
+
With no policy anywhere, the required member's built-in `fail` applies and the recipe stops. With the rule's
|
|
379
|
+
`skip-record`, the record is rejected and the rejection names the member at its index:
|
|
380
|
+
|
|
381
|
+
<!-- capture:policy-each-gap record -->
|
|
382
|
+
```json
|
|
383
|
+
{
|
|
384
|
+
"rejected": {
|
|
385
|
+
"field": "pastAbilities[0].name",
|
|
386
|
+
"reason": "missing"
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
<!-- /capture -->
|
|
391
|
+
|
|
392
|
+
The loader checks `each` rules as well. A name in `each.fields` that the items don't have, or a required member
|
|
393
|
+
that no rule maps (and that has no `default`), is a `RecipeBindingError`. Renaming `name` to `title` in the rule
|
|
394
|
+
gives both:
|
|
395
|
+
|
|
396
|
+
```text
|
|
397
|
+
recipes do not bind
|
|
398
|
+
pokeapi-past mapping.pastAbilities.fields.title: the items of "pastAbilities" have no field "title"
|
|
399
|
+
pokeapi-past mapping.pastAbilities.fields: required field of the items of "pastAbilities" "name" is not mapped
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## What's next
|
|
403
|
+
|
|
404
|
+
[Running](09-running.md): sessions, access, blocks, retries and the other things that decide whether a page
|
|
405
|
+
arrives at all.
|