Skip to content

Commit 4b1d0e0

Browse files
authored
feat: add be familly of functions. add exampes. (#6)
1 parent 7361613 commit 4b1d0e0

File tree

8 files changed

+263
-18
lines changed

8 files changed

+263
-18
lines changed

README.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,25 @@ Occasionally a little more, so made a home for them, DRY.
1919

2020
- Error handling is not always necessary. Panic on error should be
2121
easier than ignoring them.
22-
- Lightweight assertions.
23-
22+
- Lightweight assertions. No deps, 3 digit loc count.
23+
- `must` is designed to stand out like `unsafe`.
24+
So normal error handling and none panicky helper functions should live else where.
25+
2426
### Turn errors into panics
2527

2628
``` go
2729
// NoError panics on error.
30+
//
31+
// Use this because test coverage.
2832
func NoError(err error) {
2933
if err != nil {
3034
panic(err)
3135
}
3236
}
3337

3438
// Value panics on error, otherwise returns the first value.
39+
//
40+
// Use this instead of writing a Must version of your function.
3541
func Value[T any](out T, err error) T {
3642
if err != nil {
3743
panic(err)
@@ -40,27 +46,31 @@ func Value[T any](out T, err error) T {
4046
}
4147
```
4248

43-
Also Value2 is available for longer function signatures.
49+
Also `Value2` is available for longer function signatures.
50+
`V` and `V2` are their short aliases since the `must.` prefix already stands out enough.
4451

4552
### Turn assertions into panics
4653

47-
`must.True` and friends.
54+
Functions like `True(bool)` and friends. To assert a condition or panic.
55+
56+
`B1`, `B2`... are generic assertions that pass their first inputs unmodified with compile time type info preserved.
57+
They return a function to specify what assertions are made on each field.
4858

4959
Assertions have optional debug arguments, to provide additional information when
5060
violated. Usually, just pass in the line comment as string.
5161

5262
### Panic test helpers
5363

54-
`must.Panic` and `must.Recover`. Useful for writing tests for panics cases.
64+
`must.Panic` and `must.Recover`. Useful for writing tests for panicky cases.
5565

5666
## Compatibility
5767

58-
Since this library is heavily dependent on generics. 1.18 is the minimum Go version supported for now.
68+
Since this library is heavily dependent on generics. **1.18** is the minimum Go version supported until a new go version has a feature too good to pass.
5969

60-
Tagged versions will follow sematic versioning. Untagged master/main branch is for development.
70+
Tagged versions follow **sematic versioning**. Untagged master/main branch is for development.
6171

62-
Values set in panics are only for debugging. No guarantees are provided on how panics are argumented.
72+
Values used in panics are only for debugging. No guarantees are provided on how panics are constructed. This means the recovered value and stack track could change even in bugfix versions.
6373

64-
## Planning
74+
## Version 1.0 milestone
6575

66-
Generic assertions ;-)
76+
- Gain enough downstream usage cases to prove API fitness and stability.

be.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package must
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
)
7+
8+
type allowAll struct{}
9+
10+
// Any implements a Checker that always returns true.
11+
var Any = allowAll{}
12+
13+
const anyDebugMessage = "Any allows all values"
14+
15+
func (allowAll) MustBe(v any) bool {
16+
return true
17+
}
18+
19+
func (allowAll) Debug(v any) any {
20+
return anyDebugMessage
21+
}
22+
23+
// Checker can be used by B# functions to check if value is allowed.
24+
type Checker interface {
25+
// MustBe returns true iff value is allowed.
26+
MustBe(v any) bool
27+
// Debug is used to explain why the value is not allowed.
28+
Debug(v any) any
29+
}
30+
31+
// B1 asserts on one value.
32+
//
33+
// v is the value to be checked. If check is passed v is returned, otherwise panics.
34+
//
35+
// b checks v, there are a few cases:
36+
//
37+
// - A Checker (such as must.Any for all values); or
38+
// - a literal value to test for equality using `==`,
39+
// if types are the same and literal is comparable; or
40+
// - a nil value, which will match nil of any type.
41+
// - All other cases panic, room for future expansion.
42+
//
43+
// v and b are separated to two function calls for clarity,
44+
// v has to go first so the compiler can auto type it.
45+
//
46+
// debug does not participate in functionality. If the assert need a line comment,
47+
// then use it as the debug message is suggested.
48+
func B1[V any](v V) func(b any, debug ...any) V {
49+
return func(b any, debug ...any) V {
50+
checker, ok := b.(Checker)
51+
if ok {
52+
if !checker.MustBe(v) {
53+
panicWith(debug, checker.Debug(v))
54+
}
55+
return v
56+
}
57+
if b == nil {
58+
value := reflect.ValueOf(v)
59+
if value.IsValid() && !value.IsNil() {
60+
panicWith(append(debug, v), "value must be nil")
61+
}
62+
return v
63+
}
64+
bAsV, ok := b.(V)
65+
if ok {
66+
var equal bool
67+
r := Recover(func() { equal = any(bAsV) == any(v) })
68+
if r != nil {
69+
debug = append(debug, fmt.Sprintf("type(%T) is not comparable", bAsV), r)
70+
}
71+
if !equal {
72+
panicWith(append(debug, v, b), "values must equal")
73+
}
74+
return v
75+
}
76+
panicWith(debug, fmt.Sprintf("must use Checker interface or the same comparable type as input. expected %T got %T", v, b))
77+
return v // unreachable
78+
}
79+
}
80+
81+
// B2 is 2 valued version of B1. To only Check one value, use must.Any on the other value to skip it.
82+
func B2[V1 any, V2 any](v1 V1, v2 V2) func(b1, b2 any, debug ...any) (V1, V2) {
83+
return func(b1, b2 any, debug ...any) (V1, V2) {
84+
B1(v1)(b1, append(debug, "#1")...)
85+
B1(v2)(b2, append(debug, "#2")...)
86+
return v1, v2
87+
}
88+
}
89+
90+
// B3 is 3 valued version of B1. To only Check some value, use must.Any on the other values to skip it.
91+
func B3[V1 any, V2 any, V3 any](v1 V1, v2 V2, v3 V3) func(b1, b2, b3 any, debug ...any) (V1, V2, V3) {
92+
return func(b1, b2, b3 any, debug ...any) (V1, V2, V3) {
93+
B1(v1)(b1, append(debug, "#1")...)
94+
B1(v2)(b2, append(debug, "#2")...)
95+
B1(v3)(b3, append(debug, "#3")...)
96+
return v1, v2, v3
97+
}
98+
}

be_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package must_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/xiegeo/must"
7+
)
8+
9+
func ExampleB3() {
10+
// lets say we have an io.RuneReader
11+
readRune := func() (r rune, size int, err error) {
12+
return 'a', 1, nil
13+
}
14+
15+
// that must return 1 byte runes and never err out
16+
char, _, _ := must.B3(readRune())(must.Any, 1, nil)
17+
fmt.Println(string(char))
18+
19+
// output:
20+
// a
21+
}

bool_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package must_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/xiegeo/must"
7+
)
8+
9+
func ExampleTrue() {
10+
trueFunc := func() bool {
11+
return true
12+
}
13+
14+
must.True(trueFunc(), "a no-op")
15+
fmt.Printf("True:%#v\n", must.Panic(func() {
16+
must.True(false, "this will panic")
17+
}))
18+
19+
// True can also be written using B1
20+
must.B1(trueFunc())(true)
21+
fmt.Printf("B1:%#v\n", must.Panic(func() {
22+
must.B1(false)(true, "this will also panic")
23+
}))
24+
25+
// Output:
26+
// True:[]interface {}{"this will panic", "must be true"}
27+
// B1:[]interface {}{"this will also panic", false, true, "values must equal"}
28+
}
29+
30+
func ExampleFalse() {
31+
must.False(false, "a no-op")
32+
fmt.Printf("False:%#v\n", must.Panic(func() {
33+
must.False(true, "this will panic")
34+
}))
35+
36+
// False can also be written using B1
37+
must.B1(false)(false)
38+
fmt.Printf("B1:%#v\n", must.Panic(func() {
39+
must.B1(true)(false, "this will also panic")
40+
}))
41+
42+
// Output:
43+
// False:[]interface {}{"this will panic", "must be false"}
44+
// B1:[]interface {}{"this will also panic", true, false, "values must equal"}
45+
}

error.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package must
22

33
// NoError panics on error.
4+
//
5+
// Use this because test coverage.
46
func NoError(err error) {
57
if err != nil {
68
panic(err)
79
}
810
}
911

1012
// Value panics on error, otherwise returns the first value.
13+
//
14+
// Use this instead of writing a Must version of your function.
1115
func Value[T any](out T, err error) T {
1216
if err != nil {
1317
panic(err)
@@ -17,10 +21,7 @@ func Value[T any](out T, err error) T {
1721

1822
// V is short for Value, which panics on error, otherwise returns the first value.
1923
func V[T any](out T, err error) T {
20-
if err != nil {
21-
panic(err)
22-
}
23-
return out
24+
return Value(out, err)
2425
}
2526

2627
// Value2 panics on error, otherwise returns the first 2 values.
@@ -33,8 +34,5 @@ func Value2[T any, U any](out1 T, out2 U, err error) (T, U) {
3334

3435
// V2 is short for Value2, which panics on error, otherwise returns the first 2 values.
3536
func V2[T any, U any](out1 T, out2 U, err error) (T, U) {
36-
if err != nil {
37-
panic(err)
38-
}
39-
return out1, out2
37+
return Value2(out1, out2, err)
4038
}

error_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package must_test
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
"github.com/xiegeo/must"
8+
)
9+
10+
func ExampleNoError() {
11+
var err error // create nil error
12+
must.NoError(err)
13+
fmt.Printf("NoError:%#v\n", must.Panic(func() {
14+
must.NoError(io.EOF)
15+
}))
16+
17+
// NoError can also be written using B1
18+
_ = must.B1(err)(nil)
19+
fmt.Printf("B1:%v\n", must.Panic(func() { // use %v because %#v prints out pointer address
20+
_ = must.B1(io.EOF)(nil)
21+
}))
22+
23+
// Output:
24+
// NoError:&errors.errorString{s:"EOF"}
25+
// B1:[EOF value must be nil]
26+
}
27+
28+
func ExampleValue() {
29+
var err error // create nil error
30+
fmt.Printf("Value nil error:%#v\n", must.Value(5, err))
31+
fmt.Printf("Value with error:%#v\n", must.Panic(func() {
32+
_ = must.V(6, io.EOF)
33+
}))
34+
35+
// Value can also be written using B2
36+
value, _ := must.B2(7, err)(must.Any, nil)
37+
fmt.Printf("B2 nil error:%#v\n", value)
38+
fmt.Printf("B2 with error:%v\n", must.Panic(func() { // use %v because %#v prints out pointer address
39+
_, _ = must.B2(8, io.EOF)(must.Any, nil)
40+
}))
41+
42+
// Output:
43+
// Value nil error:5
44+
// Value with error:&errors.errorString{s:"EOF"}
45+
// B2 nil error:7
46+
// B2 with error:[#2 EOF value must be nil]
47+
}

panic.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ func Panic(f func(), debug ...any) (r any) {
1515

1616
func panicWith(debug []any, last any) {
1717
if len(debug) > 0 {
18+
if last == nil {
19+
panic(debug)
20+
}
1821
panic(append(debug, last))
1922
}
2023
panic(last)

panic_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package must_test
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/xiegeo/must"
7+
)
8+
9+
func ExamplePanic() {
10+
// must.Panic caches a panic and returns the thrown value
11+
fmt.Println(must.Panic(func() { panic("foo") }))
12+
13+
// must.Panic will panic if function did not panic
14+
fmt.Println(must.Panic(func() { must.Panic(func() {}) }))
15+
16+
// use must.Recover instead if function might not panic
17+
fmt.Println(must.Recover(func() {}))
18+
19+
// output:
20+
// foo
21+
// function expected to panic but did not
22+
// <nil>
23+
}

0 commit comments

Comments
 (0)