Skip to content

Commit f605689

Browse files
authored
feat: provide better panic msgs and docs (#153)
* feat: provide better panic msgs and docs * fix: docs link
1 parent 193c23c commit f605689

File tree

10 files changed

+210
-9
lines changed

10 files changed

+210
-9
lines changed

custom.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ func (c *Custom[T]) process(ctx *p.SchemaCtx) {
4848
ctx.AddIssue(ctx.IssueFromCoerce(fmt.Errorf("expected %T, got %T", new(T), ctx.Data)))
4949
return
5050
}
51-
ptr := ctx.ValPtr.(*T)
51+
ptr, ok := ctx.ValPtr.(*T)
52+
if !ok {
53+
p.Panicf(p.PanicTypeCast, ctx.String(), ctx.DType, ctx.ValPtr)
54+
}
5255
*ptr = d
5356

5457
// run the test
@@ -73,7 +76,11 @@ func (c *Custom[T]) Validate(dataPtr *T, options ...ExecOption) ZogIssueList {
7376

7477
func (c *Custom[T]) validate(ctx *p.SchemaCtx) {
7578
ctx.Processor = &c.test
76-
c.test.Func(ctx.ValPtr.(*T), ctx)
79+
ptr, ok := ctx.ValPtr.(*T)
80+
if !ok {
81+
p.Panicf(p.PanicTypeCast, ctx.String(), ctx.DType, ctx.ValPtr)
82+
}
83+
c.test.Func(ptr, ctx)
7784
}
7885

7986
func (c *Custom[T]) setCoercer(coercer CoercerFunc) {

docs/docs/core-design-decisions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ toc_max_heading_level: 4
1212
- Errors returned by you (for example in a `Preprocess` or `Transform` function) can be the ZogIssue interface or an error. If you return an error, it will be wrapped in a ZogIssue. ZogIssue is just a struct that wraps around an error and adds a message field which is text that can be shown to the user. For more on this see [Errors](/errors)
1313
- You should not depend on test execution order. They might run in parallel in the future
1414

15-
> **A WORD OF CAUTION. ZOG & PANICS**
15+
> **A WORD OF CAUTION. [ZOG & PANICS](/panics)**
1616
> In general Zog will never panic if the input data is wrong but it will panic if you configure it wrong. For example:
1717
>
1818
> - In parse mode Zog will never panic due to invalid input data but will always panic if invalid destination is passed to the `Parse` function. if the destination does not match the schema in terms of types or fields.

docs/docs/panics.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
---
2+
sidebar_position: 199
3+
hide_table_of_contents: false
4+
toc_min_heading_level: 2
5+
toc_max_heading_level: 4
6+
---
7+
8+
# Zog Panics
9+
10+
## When does Zog panic?
11+
12+
Zog follows [TigerStyle](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) asserts. It panics when something in its fundamental assumptions is broken.
13+
14+
In practice this means that Zog will never panic if the input data is wrong but it will panic if you configure it wrong. Most of the time "configured it wrong" means that you have made a mistake in your schema definition which puts Zog into an invalid state and results in a schema that can never succeed.
15+
16+
## Types of Panics
17+
18+
> If you find a panic that is not listed here, please report it as a bug!
19+
20+
### Schema Definition Errors
21+
22+
```go
23+
var schema = z.Struct(z.Schema{
24+
"name": z.String().Required(),
25+
})
26+
27+
// This struct is a valid destination for the schema
28+
type User struct {
29+
Name string
30+
Age int // age will be ignored since it is not a field in the schema
31+
}
32+
33+
// this struct is not a valid structure for the schema. It is missing the name field.
34+
// This will cause Zog to panic in both Parse and Validate mode
35+
type User2 struct {
36+
Email string `zog:"name"` // using struct tag here DOES NOT WORK. This is not the purpose of the struct tag.
37+
Age int
38+
}
39+
schema.Parse(map[string]any{"name": "zog"}, &User{}) // this will panic even if input data is valid. Because the destination is not a valid structure for the schema
40+
schema.Validate(&User2{}) // This will panic because the structure does not match the schema
41+
```
42+
43+
### Type Cast Errors
44+
45+
There are multiple ways in which a type cast error can occur. For example:
46+
47+
###### 1 Destination/Validation value is not a pointer
48+
49+
```go
50+
var schema = z.Struct(z.Schema{
51+
"name": z.String().Required(),
52+
})
53+
54+
var dest User
55+
schema.Parse(map[string]any{"name": "zog"}, dest) // This will panic because dest is not a pointer
56+
schema.Validate(dest) // This will panic because dest is not a pointer
57+
// Fix this by using a pointer
58+
schema.Parse(map[string]any{"name": "zog"}, &dest)
59+
schema.Validate(&dest)
60+
```
61+
62+
> This can only really happen on complex schemas since those are not fully typesafe. Primitive schemas are typesafe and won't let you pass a non-pointer value.
63+
64+
###### 2 Destination/Validation value is not a valid type for the schema
65+
66+
```go
67+
type MyString string
68+
type User struct {
69+
Age MyString
70+
}
71+
var schema = z.Struct(z.Schema{
72+
"age": z.String().Required(),
73+
})
74+
75+
val := User{
76+
Age: MyString("1"),
77+
}
78+
schema.Validate(&val) // This will panic because the schema is expecting a string but the value is of type MyString
79+
```
80+
81+
Same thing will happen if you incorrectly set the type in a z.Custom schema:
82+
83+
```go
84+
85+
type User struct {
86+
ID uuid.UUID
87+
}
88+
89+
var schema = z.Struct(z.Schema{
90+
"id": z.Custom(func (ptr *string, ctx z.Ctx) bool { // Zog can't convert a UUID to a string so this will panic
91+
return true
92+
}),
93+
})
94+
95+
val := User{
96+
ID: uuid.New(),
97+
}
98+
99+
schema.Validate(&val) // This will panic because the schema is expecting a string but the value is of type uuid.UUID
100+
```
101+
102+
Another common example is when you forget to use z.Ptr.
103+
104+
```go
105+
type User struct {
106+
Friends *[]Friend
107+
}
108+
109+
// This is incorrect!
110+
var schema = z.Struct(z.Schema{
111+
"friends": z.Slice(z.Struct(z.Schema{
112+
"name": z.String().Required(),
113+
})),
114+
})
115+
116+
// This is correct!
117+
var schema2 = z.Struct(z.Schema{
118+
"friends": z.Ptr(z.Slice(z.Struct(z.Schema{
119+
"name": z.String().Required(),
120+
}))),
121+
})
122+
```
123+
124+
###### 3 The coercer returns a value of the wrong type
125+
126+
> Only applicable to `schema.Parse()`
127+
128+
```go
129+
var schema = z.Struct(z.Schema{
130+
"name": z.String(z.WithCoercer(func (v any, ctx z.Ctx) (any, error) {
131+
return 1, nil // we are returning an int but the schema is expecting a string
132+
})).Required(),
133+
})
134+
135+
val := User{
136+
Name: "zog",
137+
}
138+
schema.Parse(map[string]any{"name": "zog"}, &val) // This will panic because the coercer is returning an int but the schema is expecting a string
139+
```

internals/contexts.go

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

33
import (
4+
"fmt"
5+
46
zconst "github.com/Oudwins/zog/zconst"
57
)
68

@@ -208,6 +210,10 @@ func (c *SchemaCtx) Free() {
208210
SchemaCtxPool.Put(c)
209211
}
210212

213+
func (c *SchemaCtx) String() string {
214+
return fmt.Sprintf("z.Ctx{Data: %v, ValPtr: %v, Path: %v, DType: %v, CanCatch: %v, Exit: %v, HasCaught: %v }", SafeString(c.Data), SafeString(c.ValPtr), c.Path, c.DType, c.CanCatch, c.Exit, c.HasCaught)
215+
}
216+
211217
// func (c *TestCtx) Issue() *ZogIssue {
212218
// // TODO handle catch here
213219
// zerr := ZogIssuePool.Get().(*ZogIssue)

internals/panics.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package internals
2+
3+
import "fmt"
4+
5+
const (
6+
PanicTypeCast = "Zog Panic: Type Cast Error\n Current context: %s\n Expected valPtr type to correspond with type defined in schema. But it does not. Expected type: *%T, got: %T\nFor more information see: https://zog.dev/panics#type-cast-errors"
7+
PanicTypeCastCoercer = "Zog Panic: Type Cast Error\n Current context: %s\n Expected coercer return value to correspond with type defined in schema. But it does not. Expected type: *%T, got: %T\nFor more information see: https://zog.dev/panics#type-cast-errors"
8+
PanicMissingStructField = "Zog Panic: Struct Schema Definition Error\n Current context: %s\n Provided struct is missing expected schema key: %s.\n This means you have made a mistake in your schema definition.\nFor more information see: https://zog.dev/panics#schema-definition-errors"
9+
)
10+
11+
func Panicf(format string, args ...any) {
12+
panic(fmt.Sprintf(format, args...))
13+
}

internals/utils.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ func SafeString(x any) string {
1111
if x == nil {
1212
return defaultString
1313
}
14-
return fmt.Sprintf("%v", x)
14+
refVal := reflect.ValueOf(x)
15+
for refVal.Kind() == reflect.Ptr {
16+
if refVal.IsNil() {
17+
return defaultString
18+
}
19+
refVal = refVal.Elem()
20+
}
21+
return fmt.Sprintf("%v", refVal.Interface())
1522
}
1623

1724
func SafeError(x error) string {

preprocess.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ func (s *PreprocessSchema[F, T]) process(ctx *p.SchemaCtx) {
3434
}
3535

3636
func (s *PreprocessSchema[F, T]) validate(ctx *p.SchemaCtx) {
37-
out, err := s.fn(ctx.ValPtr.(F), ctx)
37+
v, ok := ctx.ValPtr.(F)
38+
if !ok {
39+
p.Panicf(p.PanicTypeCast, ctx.String(), new(F), ctx.ValPtr)
40+
}
41+
out, err := s.fn(v, ctx)
3842
if err != nil {
3943
ctx.AddIssue(ctx.Issue().SetMessage(err.Error()))
4044
return

struct.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func (v *StructSchema) process(ctx *p.SchemaCtx) {
100100

101101
fieldMeta, ok := structVal.Type().FieldByName(key)
102102
if !ok {
103-
panic(fmt.Sprintf("Struct is missing expected schema key: %s\n see the zog FAQ for more info", key))
103+
p.Panicf(p.PanicMissingStructField, ctx.String(), key)
104104
}
105105
destPtr := structVal.FieldByName(key).Addr().Interface()
106106

struct_validate_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,18 @@ func TestValidateStructGetType(t *testing.T) {
215215
})
216216
assert.Equal(t, zconst.TypeStruct, s.getType())
217217
}
218+
219+
func TestValidateStructInvalidSchema(t *testing.T) {
220+
schema := Struct(Shape{
221+
"field": String(),
222+
})
223+
224+
type TestStruct struct {
225+
Field int
226+
}
227+
228+
var dest TestStruct
229+
assert.Panics(t, func() {
230+
schema.Validate(&dest)
231+
})
232+
}

zogSchema.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ func TestFunc[T any](IssueCode zconst.ZogIssueCode, fn BoolTFunc[T], options ...
5353
func primitiveParsing[T p.ZogPrimitive](ctx *p.SchemaCtx, processors []p.ZProcessor[*T], defaultVal *T, required *p.Test[*T], catch *T, coercer CoercerFunc, isZeroFunc p.IsZeroValueFunc) {
5454
ctx.CanCatch = catch != nil
5555

56-
destPtr := ctx.ValPtr.(*T)
56+
destPtr, ok := ctx.ValPtr.(*T)
57+
if !ok {
58+
p.Panicf(p.PanicTypeCast, ctx.String(), ctx.DType, ctx.ValPtr)
59+
}
5760

5861
// 2. cast data to string & handle default/required
5962
isZeroVal := isZeroFunc(ctx.Data, ctx)
@@ -84,7 +87,11 @@ func primitiveParsing[T p.ZogPrimitive](ctx *p.SchemaCtx, processors []p.ZProces
8487
ctx.AddIssue(ctx.IssueFromCoerce(err))
8588
return
8689
}
87-
*destPtr = v.(T)
90+
x, ok := v.(T)
91+
if !ok {
92+
p.Panicf(p.PanicTypeCastCoercer, ctx.String(), ctx.DType, v)
93+
}
94+
*destPtr = x
8895
}
8996

9097
for _, processor := range processors {
@@ -103,7 +110,10 @@ func primitiveParsing[T p.ZogPrimitive](ctx *p.SchemaCtx, processors []p.ZProces
103110
func primitiveValidation[T p.ZogPrimitive](ctx *p.SchemaCtx, processors []p.ZProcessor[*T], defaultVal *T, required *p.Test[*T], catch *T) {
104111
ctx.CanCatch = catch != nil
105112

106-
valPtr := ctx.ValPtr.(*T)
113+
valPtr, ok := ctx.ValPtr.(*T)
114+
if !ok {
115+
p.Panicf(p.PanicTypeCast, ctx.String(), ctx.DType, ctx.ValPtr)
116+
}
107117

108118
// 2. cast data to string & handle default/required
109119
// Warning. This uses generic IsZeroValue because for Validate we treat zero values as invalid for required fields. This is different from Parse.

0 commit comments

Comments
 (0)