jade-lang 0.4.0 → 0.6.0
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/AGENTS.md +164 -0
- data/CHANGELOG.md +180 -1
- data/README.md +5 -1
- data/docs/interop.md +208 -0
- data/docs/json.md +163 -0
- data/docs/lsp.md +105 -0
- data/docs/stdlib.md +69 -0
- data/docs/syntax.md +458 -0
- data/docs/testing.md +70 -0
- data/lib/jade/api.rb +244 -0
- data/lib/jade/cli/check.rb +97 -0
- data/lib/jade/cli/q.rb +47 -1
- data/lib/jade/cli.rb +4 -2
- data/lib/jade/codegen/boundary/cache.rb +1 -2
- data/lib/jade/codegen/boundary/specialized/list.rb +9 -4
- data/lib/jade/codegen/boundary/specialized/maybe.rb +15 -3
- data/lib/jade/codegen/boundary/specialized/record.rb +7 -3
- data/lib/jade/codegen/boundary/specialized.rb +7 -3
- data/lib/jade/codegen/emitter.rb +11 -2
- data/lib/jade/codegen/function_declaration.rb +7 -6
- data/lib/jade/codegen/helpers.rb +6 -0
- data/lib/jade/codegen/inlines.rb +5 -0
- data/lib/jade/decode.rb +332 -212
- data/lib/jade/frontend/fixity_fixer.rb +3 -2
- data/lib/jade/frontend/semantic_analysis/error/variable_not_found.rb +9 -2
- data/lib/jade/frontend/semantic_analysis/member_access.rb +11 -1
- data/lib/jade/frontend/type_checking/constraints/deriving/decodable.rb +10 -15
- data/lib/jade/frontend/type_checking/constraints/deriving/encodable.rb +7 -16
- data/lib/jade/interop/boundary.rb +12 -4
- data/lib/jade/interop/error.rb +25 -5
- data/lib/jade/lsp/converters.rb +2 -15
- data/lib/jade/lsp/snippets.rb +21 -3
- data/lib/jade/module_loader/module_name.rb +46 -0
- data/lib/jade/module_loader.rb +5 -0
- data/lib/jade/signature.rb +47 -0
- data/lib/jade/stdlib/calendar.rb +20 -19
- data/lib/jade/stdlib/clock.rb +7 -13
- data/lib/jade/stdlib/decimal.rb +3 -16
- data/lib/jade/stdlib/decode.rb +35 -12
- data/lib/jade/stdlib/encode.rb +9 -0
- data/lib/jade/stdlib/intrinsics.rb +9 -1
- data/lib/jade/stdlib/result.rb +1 -1
- data/lib/jade/stdlib/text.rb +131 -0
- data/lib/jade/task.rb +2 -3
- data/lib/jade/version.rb +1 -1
- metadata +14 -2
data/docs/syntax.md
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
# Syntax reference
|
|
2
|
+
|
|
3
|
+
A tour of the language. The shorter "what does Jade look like" answer is in the
|
|
4
|
+
[README](../README.md); this page covers what you'll actually write. Every
|
|
5
|
+
snippet here compiles.
|
|
6
|
+
|
|
7
|
+
## Functions
|
|
8
|
+
|
|
9
|
+
```jade
|
|
10
|
+
module Greetings exposing (greet)
|
|
11
|
+
|
|
12
|
+
def greet(name: String) -> String
|
|
13
|
+
"Hello, " ++ name ++ "!"
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Annotations are required on function signatures; everything else is inferred.
|
|
18
|
+
Bodies close with `end`. A zero-argument function is written without parens —
|
|
19
|
+
`def greet -> String`.
|
|
20
|
+
|
|
21
|
+
→ [`examples/basics_examples.jd`](../examples/basics_examples.jd)
|
|
22
|
+
|
|
23
|
+
## Union types
|
|
24
|
+
|
|
25
|
+
```jade
|
|
26
|
+
module Shapes exposing (Shape)
|
|
27
|
+
|
|
28
|
+
type Shape
|
|
29
|
+
= Circle(Float)
|
|
30
|
+
| Rectangle(Float, Float)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Variants carry positional payloads (above) or keyed payloads when the
|
|
34
|
+
positional args get hard to read:
|
|
35
|
+
|
|
36
|
+
```jade
|
|
37
|
+
module Charges exposing (paid)
|
|
38
|
+
|
|
39
|
+
type Charge
|
|
40
|
+
= Refund(Int)
|
|
41
|
+
| Settled(paid_amount: Int, tax_amount: Int, issued_amount: Int)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def paid(c: Charge) -> Int
|
|
45
|
+
case c
|
|
46
|
+
in Refund(n) then n
|
|
47
|
+
in Settled(r) then r.paid_amount + r.tax_amount
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The keyed payload is an anonymous record, so `r.paid_amount` and
|
|
53
|
+
`{ r | paid_amount: 0 }` work as usual.
|
|
54
|
+
|
|
55
|
+
→ [`examples/custom_types.jd`](../examples/custom_types.jd)
|
|
56
|
+
|
|
57
|
+
## Structs (named records)
|
|
58
|
+
|
|
59
|
+
```jade
|
|
60
|
+
module People exposing (make, make_kw, older, older_pipe)
|
|
61
|
+
|
|
62
|
+
struct Person = {
|
|
63
|
+
name: String,
|
|
64
|
+
age: Int
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def make -> Person
|
|
69
|
+
Person("Paul", 55)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def make_kw -> Person
|
|
74
|
+
Person(name: "Paul", age: 55)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def older(p: Person) -> Person
|
|
79
|
+
{ p | age: p.age + 1 }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def older_pipe(p: Person) -> Person
|
|
84
|
+
p |> .age=(p.age + 1)
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Anonymous records do not coerce into nominal structs: passing
|
|
89
|
+
`{ name: "Paul", age: 55 }` where a `Person` is expected is a type error.
|
|
90
|
+
|
|
91
|
+
→ [`examples/records.jd`](../examples/records.jd)
|
|
92
|
+
|
|
93
|
+
## Anonymous records and tuples
|
|
94
|
+
|
|
95
|
+
```jade
|
|
96
|
+
module Pairs exposing (origin, swap)
|
|
97
|
+
|
|
98
|
+
def origin -> { x: Int, y: Int }
|
|
99
|
+
{ x: 0, y: 0 }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def swap(pair: (Int, String)) -> (String, Int)
|
|
104
|
+
(Tuple.second(pair), Tuple.first(pair))
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Tuple elements are read with `Tuple.first` / `Tuple.second`, or matched in a
|
|
109
|
+
`case` (`in (a, b) then …`).
|
|
110
|
+
|
|
111
|
+
## Primitives and `Never`
|
|
112
|
+
|
|
113
|
+
`Int`, `Float`, `Bool`, `String`, `Char`. `Char` literals are single
|
|
114
|
+
characters in single quotes: `'a'`, `'Z'`.
|
|
115
|
+
|
|
116
|
+
`Never` is a type with no values — a variant that can't be built. The compiler
|
|
117
|
+
knows this, so you can destructure past it:
|
|
118
|
+
|
|
119
|
+
```jade
|
|
120
|
+
module Unwrap exposing (unwrap)
|
|
121
|
+
|
|
122
|
+
def unwrap(r: Result(Int, Never)) -> Int
|
|
123
|
+
Ok(n) = r
|
|
124
|
+
n
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Zero-arg functions as values
|
|
129
|
+
|
|
130
|
+
A zero-arg function is referenced by name, with or without parens:
|
|
131
|
+
|
|
132
|
+
```jade
|
|
133
|
+
module Geometry exposing (area)
|
|
134
|
+
|
|
135
|
+
def pi -> Float
|
|
136
|
+
3.14
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def area(radius: Float) -> Float
|
|
141
|
+
pi * radius * radius
|
|
142
|
+
end
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Bare constructors are values too: `True`, `Nothing`. Constructors with payloads
|
|
146
|
+
are called: `Just(x)`, `Ok(value)`.
|
|
147
|
+
|
|
148
|
+
## Pattern matching
|
|
149
|
+
|
|
150
|
+
```jade
|
|
151
|
+
module Sizes exposing (describe_size)
|
|
152
|
+
|
|
153
|
+
def describe_size(n: Int) -> String
|
|
154
|
+
case n
|
|
155
|
+
in 0 then "empty"
|
|
156
|
+
in 1 then "one"
|
|
157
|
+
else "many"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`case` must cover every shape the value can take — exhaustiveness is enforced by
|
|
163
|
+
the compiler. The trailing wildcard is written `else`.
|
|
164
|
+
|
|
165
|
+
A branch with multiple statements drops `then` and indents under the header:
|
|
166
|
+
|
|
167
|
+
```jade
|
|
168
|
+
module Areas exposing (area)
|
|
169
|
+
|
|
170
|
+
type Shape
|
|
171
|
+
= Circle(Float)
|
|
172
|
+
| Rectangle(Float, Float)
|
|
173
|
+
| Triangle(Float, Float, Float)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def area(shape: Shape) -> Float
|
|
177
|
+
case shape
|
|
178
|
+
in Circle(r) then 3.14 * r * r
|
|
179
|
+
in Rectangle(w, h) then w * h
|
|
180
|
+
in Triangle(a, b, c)
|
|
181
|
+
s = (a + b + c) / 2.0
|
|
182
|
+
s * (s - a) * (s - b) * (s - c)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Lists** match with `[]` and `[head | tail]`; the rest after `|` must be a
|
|
188
|
+
name or wildcard:
|
|
189
|
+
|
|
190
|
+
```jade
|
|
191
|
+
module Sums exposing (sum)
|
|
192
|
+
|
|
193
|
+
def sum(list: List(Int)) -> Int
|
|
194
|
+
case list
|
|
195
|
+
in [] then 0
|
|
196
|
+
in [x | xs] then x + sum(xs)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Destructuring with `=`** binds the fields of a record (use `case` for a list
|
|
202
|
+
— a partial `=` binding on a list is rejected):
|
|
203
|
+
|
|
204
|
+
```jade
|
|
205
|
+
module Names exposing (greet)
|
|
206
|
+
|
|
207
|
+
struct Person = {
|
|
208
|
+
name: String,
|
|
209
|
+
age: Int
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def greet(person: Person) -> String
|
|
214
|
+
{ name:, age: } = person
|
|
215
|
+
"Hi " ++ name
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**The `<-` operator** unwraps the value inside a `Maybe`, `Result`, or `Task`
|
|
220
|
+
and short-circuits if it isn't there, so you write the success path top to
|
|
221
|
+
bottom:
|
|
222
|
+
|
|
223
|
+
```jade
|
|
224
|
+
module SafeSum exposing (safe_sum)
|
|
225
|
+
|
|
226
|
+
def safe_sum(a: Maybe(Int), b: Maybe(Int)) -> Maybe(Int)
|
|
227
|
+
x <- a
|
|
228
|
+
y <- b
|
|
229
|
+
Just(x + y)
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
If `a` or `b` is `Nothing`, the rest of the function is skipped. `Result`
|
|
234
|
+
short-circuits on `Err`; `Task` on its failure arm.
|
|
235
|
+
|
|
236
|
+
→ [`examples/pattern_matching.jd`](../examples/pattern_matching.jd),
|
|
237
|
+
[`examples/maybe_examples.jd`](../examples/maybe_examples.jd)
|
|
238
|
+
|
|
239
|
+
## Conditionals
|
|
240
|
+
|
|
241
|
+
A ternary for one-liners, a block `if`/`else`/`end` when a branch needs more
|
|
242
|
+
than one statement:
|
|
243
|
+
|
|
244
|
+
```jade
|
|
245
|
+
module Abs exposing (abs)
|
|
246
|
+
|
|
247
|
+
def abs(n: Int) -> Int
|
|
248
|
+
n < 0 ? 0 - n : n
|
|
249
|
+
end
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Pipes, lambdas, currying
|
|
253
|
+
|
|
254
|
+
```jade
|
|
255
|
+
module Nums exposing (clean)
|
|
256
|
+
|
|
257
|
+
def clean(numbers: List(Int)) -> List(Int)
|
|
258
|
+
numbers
|
|
259
|
+
|> List.filter((x) -> { x > 0 })
|
|
260
|
+
|> List.map((x) -> { x * 2 })
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Lambdas are `(params) -> { body }` — the one construct that uses braces.
|
|
265
|
+
|
|
266
|
+
A `_` in an argument position curries the call: each `_` becomes a parameter,
|
|
267
|
+
left to right, so `add(_, 5)` is a one-argument function.
|
|
268
|
+
|
|
269
|
+
```jade
|
|
270
|
+
module Curry exposing (add_five, add_one)
|
|
271
|
+
|
|
272
|
+
def add(a: Int, b: Int) -> Int
|
|
273
|
+
a + b
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def add_five(n: Int) -> Int
|
|
278
|
+
add(_, 5)(n)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def add_one(n: Int) -> Int
|
|
283
|
+
add(1, _)(n)
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
It's most useful for building decoders field by field:
|
|
288
|
+
|
|
289
|
+
```jade
|
|
290
|
+
module Decoding exposing (person)
|
|
291
|
+
|
|
292
|
+
import Decode exposing (DecodeError)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
struct Person = {
|
|
296
|
+
name: String,
|
|
297
|
+
age: Int
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def person(json: String) -> Result(Person, DecodeError)
|
|
302
|
+
decoder = Decode.succeed(Person(_, _))
|
|
303
|
+
|> Decode.required("name", Decode.string)
|
|
304
|
+
|> Decode.required("age", Decode.int)
|
|
305
|
+
|
|
306
|
+
Decode.decode_string(decoder, json)
|
|
307
|
+
end
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`_` is only valid as a direct argument inside a call — not as a bare expression
|
|
311
|
+
or operator operand.
|
|
312
|
+
|
|
313
|
+
## Interfaces
|
|
314
|
+
|
|
315
|
+
Resolved at compile time from the argument types, like a Ruby module you
|
|
316
|
+
`extend` — except the compiler picks the implementation. `Eq` (`==` / `!=`),
|
|
317
|
+
`Comparable` (`compare`, returning `LT` / `EQ` / `GT`), and `Appendable` (`++`
|
|
318
|
+
on `String` and `List`) ship built in:
|
|
319
|
+
|
|
320
|
+
```jade
|
|
321
|
+
module Compare exposing (are_equal, larger)
|
|
322
|
+
|
|
323
|
+
def are_equal(a: a, b: a) -> Bool
|
|
324
|
+
a == b
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def larger(a: a, b: a) -> a
|
|
329
|
+
case compare(a, b)
|
|
330
|
+
in GT then a
|
|
331
|
+
else b
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
You can define your own, with implementations dispatched by type:
|
|
337
|
+
|
|
338
|
+
```jade
|
|
339
|
+
module Shows exposing (describe)
|
|
340
|
+
|
|
341
|
+
struct Person = {
|
|
342
|
+
name: String,
|
|
343
|
+
age: Int
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
interface Show(a) with
|
|
348
|
+
show : a -> String
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
implements Show(Person) with
|
|
353
|
+
show: (p) -> { p.name ++ " (age " ++ String.from_int(p.age) ++ ")" }
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def describe(p: Person) -> String
|
|
358
|
+
show(p)
|
|
359
|
+
end
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
The right-hand side of an `implements` clause is an inline lambda or a function
|
|
363
|
+
reference (`show: show_person`).
|
|
364
|
+
|
|
365
|
+
→ [`examples/interfaces.jd`](../examples/interfaces.jd)
|
|
366
|
+
|
|
367
|
+
## Modules and imports
|
|
368
|
+
|
|
369
|
+
One module per file; `exposing` lists the public surface (alphabetized).
|
|
370
|
+
`import` pulls names in by module or selectively — `Maybe(..)` brings the type
|
|
371
|
+
in along with its constructors:
|
|
372
|
+
|
|
373
|
+
```jade
|
|
374
|
+
module Wallet exposing (balance)
|
|
375
|
+
|
|
376
|
+
import Maybe exposing (Maybe(..), map)
|
|
377
|
+
import List
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def balance(amount: Maybe(Int)) -> Int
|
|
381
|
+
Maybe.with_default(amount, 0)
|
|
382
|
+
end
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## How it compiles
|
|
386
|
+
|
|
387
|
+
Source on the left, the Ruby it compiles to on the right. Nothing in the
|
|
388
|
+
compiled column is machinery you can't trace.
|
|
389
|
+
|
|
390
|
+
<table>
|
|
391
|
+
<tr><th>Jade</th><th>Compiled Ruby</th></tr>
|
|
392
|
+
<tr><td>
|
|
393
|
+
|
|
394
|
+
```jade
|
|
395
|
+
module Sample exposing (area)
|
|
396
|
+
|
|
397
|
+
type Shape
|
|
398
|
+
= Circle(Float)
|
|
399
|
+
| Rectangle(Float, Float)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def area(shape: Shape) -> Float
|
|
403
|
+
case shape
|
|
404
|
+
in Circle(r) then 3.14 * r * r
|
|
405
|
+
in Rectangle(w, h) then w * h
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
</td><td>
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
module Sample
|
|
414
|
+
extend self
|
|
415
|
+
|
|
416
|
+
Circle = Data.define(:_1) do
|
|
417
|
+
def circle?; true; end
|
|
418
|
+
def rectangle?; false; end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
Rectangle = Data.define(:_1, :_2) do
|
|
422
|
+
def circle?; false; end
|
|
423
|
+
def rectangle?; true; end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
module Internal
|
|
427
|
+
extend self
|
|
428
|
+
|
|
429
|
+
def area(shape)
|
|
430
|
+
case shape
|
|
431
|
+
in Sample::Circle(r) then ((3.14 * r) * r)
|
|
432
|
+
in Sample::Rectangle(w, h) then (w * h)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def self.area(*)
|
|
438
|
+
raise Jade::Interop::NotExposed.new(
|
|
439
|
+
module_name: "Sample", function_name: :area,
|
|
440
|
+
hint: "argument 1 of type Shape has no Decodable instance",
|
|
441
|
+
)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
</td></tr>
|
|
447
|
+
</table>
|
|
448
|
+
|
|
449
|
+
(The compiled file also carries a short `require` header for the runtime and
|
|
450
|
+
stdlib.) When something behaves unexpectedly, the path is the same as in any
|
|
451
|
+
Ruby project: open the file, read the code.
|
|
452
|
+
|
|
453
|
+
A function is exposed to Ruby only when all its parameters are `Decodable` and
|
|
454
|
+
its return is `Encodable`; the public `Sample.area` is generated alongside
|
|
455
|
+
`Internal.area` to decode the Ruby args, run the function, and encode the
|
|
456
|
+
return. Here `area`'s parameter is a `Shape`, which has no `Decodable` instance —
|
|
457
|
+
so the function isn't exposed, and the public method raises `NotExposed` if
|
|
458
|
+
called. See [interop.md](interop.md) for that side.
|
data/docs/testing.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Most Jade tests are plain unit tests over pure functions: pass in data, assert
|
|
4
|
+
on the return value, no mocks. The only thing that needs special support is a
|
|
5
|
+
`Task` — code that talks to the outside world. That's what this page covers.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
Include one of two RSpec helpers:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# strict: any unstubbed Task is a test failure
|
|
13
|
+
RSpec.configure { |c| c.include Jade::Tasks::RSpec }
|
|
14
|
+
|
|
15
|
+
# loose: real port bodies run unless you've replaced them
|
|
16
|
+
RSpec.configure { |c| c.include Jade::Tasks::RSpec::Loose }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Use **strict** for unit specs (an unstubbed `Task` means the test reached the
|
|
20
|
+
outside world by accident). Use **loose** for higher-level specs that should let
|
|
21
|
+
real bodies through unless you've stubbed them.
|
|
22
|
+
|
|
23
|
+
## Stubbing a Task
|
|
24
|
+
|
|
25
|
+
`all_calls_to(task, …)` sets a persistent answer; `next_call_to(task, …)` queues
|
|
26
|
+
a one-shot. Both take a value or a block `{ |t, *args| t.ok(…) }`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
it 'sends a welcome mail to the new address' do
|
|
30
|
+
all_calls_to(Mailer.deliver) { |t, _email| t.ok(true) }
|
|
31
|
+
|
|
32
|
+
expect(Signup::Internal.run('ada@example.com').run).to be_ok(true)
|
|
33
|
+
expect(Mailer.deliver).to have_been_called.with('ada@example.com')
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Queued answers win until exhausted, then the persistent one takes over:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
next_call_to(Rng.roll, 1)
|
|
41
|
+
next_call_to(Rng.roll, 2)
|
|
42
|
+
all_calls_to(Rng.roll, 0) # call 1 → 1, call 2 → 2, call 3+ → 0
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`have_been_called` chains `.with(...)`, `.once`, `.times(n)`, and negates with
|
|
46
|
+
`not_to`.
|
|
47
|
+
|
|
48
|
+
## Matchers
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
expect(result).to be_ok # is Ok
|
|
52
|
+
expect(result).to be_ok(42) # Ok(42)
|
|
53
|
+
expect(result).to be_err("smtp down")
|
|
54
|
+
expect(maybe).to be_just(5)
|
|
55
|
+
expect(maybe).to be_nothing
|
|
56
|
+
|
|
57
|
+
# look_like matches a union variant by name and payload
|
|
58
|
+
expect(shape).to look_like(:Circle, 10.0)
|
|
59
|
+
expect(shape).to look_like(:Square, 1.0)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`be_ok` and friends compose with ordinary matchers:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
expect(result).to be_ok(have_attributes(name: 'Ada', age: 40))
|
|
66
|
+
expect(result).to be_ok(kind_of(Integer))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Pass a `'Module::Name'` string to `look_like` when the short variant name is
|
|
70
|
+
ambiguous.
|