Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 29 additions & 24 deletions boolean.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,35 @@ import (
"github.com/Oudwins/zog/zconst"
)

var _ PrimitiveZogSchema[bool] = &BoolSchema{}
var _ PrimitiveZogSchema[bool] = &BoolSchema[bool]{}

type BoolSchema struct {
type BoolSchema[T ~bool] struct {
preTransforms []p.PreTransform
tests []p.Test
postTransforms []p.PostTransform
defaultVal *bool
defaultVal *T
required *p.Test
catch *bool
catch *T
coercer conf.CoercerFunc
}

// ! INTERNALS

// Returns the type of the schema
func (v *BoolSchema) getType() zconst.ZogType {
func (v *BoolSchema[T]) getType() zconst.ZogType {
return zconst.TypeBool
}

// Sets the coercer for the schema
func (v *BoolSchema) setCoercer(c conf.CoercerFunc) {
func (v *BoolSchema[T]) setCoercer(c conf.CoercerFunc) {
v.coercer = c
}

// ! USER FACING FUNCTIONS

// Returns a new Bool Schema
func Bool(opts ...SchemaOption) *BoolSchema {
b := &BoolSchema{
func Bool(opts ...SchemaOption) *BoolSchema[bool] {
b := &BoolSchema[bool]{
coercer: conf.Coercers.Bool, // default coercer
}
for _, opt := range opts {
Expand All @@ -44,7 +44,7 @@ func Bool(opts ...SchemaOption) *BoolSchema {
}

// Parse data into destination pointer
func (v *BoolSchema) Parse(data any, dest *bool, options ...ExecOption) p.ZogIssueList {
func (v *BoolSchema[T]) Parse(data any, dest *T, options ...ExecOption) p.ZogIssueList {
errs := p.NewErrsList()
defer errs.Free()
ctx := p.NewExecCtx(errs, conf.IssueFormatter)
Expand All @@ -60,12 +60,12 @@ func (v *BoolSchema) Parse(data any, dest *bool, options ...ExecOption) p.ZogIss
}

// Internal function to process the data
func (v *BoolSchema) process(ctx *p.SchemaCtx) {
func (v *BoolSchema[T]) process(ctx *p.SchemaCtx) {
primitiveProcessor(ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch, v.coercer, p.IsParseZeroValue)
}

// Validate data against schema
func (v *BoolSchema) Validate(val *bool, options ...ExecOption) p.ZogIssueList {
func (v *BoolSchema[T]) Validate(val *T, options ...ExecOption) p.ZogIssueList {
errs := p.NewErrsList()
defer errs.Free()
ctx := p.NewExecCtx(errs, conf.IssueFormatter)
Expand All @@ -81,13 +81,13 @@ func (v *BoolSchema) Validate(val *bool, options ...ExecOption) p.ZogIssueList {
}

// Internal function to validate data
func (v *BoolSchema) validate(ctx *p.SchemaCtx) {
func (v *BoolSchema[T]) validate(ctx *p.SchemaCtx) {
primitiveValidator(ctx, v.preTransforms, v.tests, v.postTransforms, v.defaultVal, v.required, v.catch)
}

// GLOBAL METHODS

func (v *BoolSchema) Test(t p.Test, options ...TestOption) *BoolSchema {
func (v *BoolSchema[T]) Test(t p.Test, options ...TestOption) *BoolSchema[T] {
for _, opt := range options {
opt(&t)
}
Expand All @@ -97,14 +97,14 @@ func (v *BoolSchema) Test(t p.Test, options ...TestOption) *BoolSchema {
}

// Create a custom test function for the schema. This is similar to Zod's `.refine()` method.
func (v *BoolSchema) TestFunc(testFunc p.TestFunc, options ...TestOption) *BoolSchema {
func (v *BoolSchema[T]) TestFunc(testFunc p.TestFunc, options ...TestOption) *BoolSchema[T] {
test := TestFunc("", testFunc)
v.Test(test, options...)
return v
}

// Adds pretransform function to schema
func (v *BoolSchema) PreTransform(transform p.PreTransform) *BoolSchema {
func (v *BoolSchema[T]) PreTransform(transform p.PreTransform) *BoolSchema[T] {
if v.preTransforms == nil {
v.preTransforms = []p.PreTransform{}
}
Expand All @@ -113,7 +113,7 @@ func (v *BoolSchema) PreTransform(transform p.PreTransform) *BoolSchema {
}

// Adds posttransform function to schema
func (v *BoolSchema) PostTransform(transform p.PostTransform) *BoolSchema {
func (v *BoolSchema[T]) PostTransform(transform p.PostTransform) *BoolSchema[T] {
if v.postTransforms == nil {
v.postTransforms = []p.PostTransform{}
}
Expand All @@ -123,7 +123,7 @@ func (v *BoolSchema) PostTransform(transform p.PostTransform) *BoolSchema {

// ! MODIFIERS
// marks field as required
func (v *BoolSchema) Required(options ...TestOption) *BoolSchema {
func (v *BoolSchema[T]) Required(options ...TestOption) *BoolSchema[T] {
r := p.Required()
for _, opt := range options {
opt(&r)
Expand All @@ -133,31 +133,36 @@ func (v *BoolSchema) Required(options ...TestOption) *BoolSchema {
}

// marks field as optional
func (v *BoolSchema) Optional() *BoolSchema {
func (v *BoolSchema[T]) Optional() *BoolSchema[T] {
v.required = nil
return v
}

// sets the default value
func (v *BoolSchema) Default(val bool) *BoolSchema {
func (v *BoolSchema[T]) Default(val T) *BoolSchema[T] {
v.defaultVal = &val
return v
}

// sets the catch value (i.e the value to use if the validation fails)
func (v *BoolSchema) Catch(val bool) *BoolSchema {
func (v *BoolSchema[T]) Catch(val T) *BoolSchema[T] {
v.catch = &val
return v
}

// UNIQUE METHODS

func (v *BoolSchema) True() *BoolSchema {
v.tests = append(v.tests, p.EQ[bool](true))
func (v *BoolSchema[T]) True() *BoolSchema[T] {
v.tests = append(v.tests, p.EQ[T](T(true)))
return v
}

func (v *BoolSchema) False() *BoolSchema {
v.tests = append(v.tests, p.EQ[bool](false))
func (v *BoolSchema[T]) False() *BoolSchema[T] {
v.tests = append(v.tests, p.EQ[T](T(false)))
return v
}

func (v *BoolSchema[T]) EQ(val T) *BoolSchema[T] {
v.tests = append(v.tests, p.EQ[T](val))
return v
}
2 changes: 1 addition & 1 deletion boolean_validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestBoolValidateOptional(t *testing.T) {
name string
data bool
expected bool
proc *BoolSchema
proc *BoolSchema[bool]
expectErr bool
}{
{
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/configuration.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 8
sidebar_position: 20
---

# Configuration
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/context.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
sidebar_position: 6
sidebar_position: 7
---

# Zog Context

## What is context?

Zog uses a `z.Ctx` interface to pass around information related to a specific `schema.Parse()` or `schema.Validate()` call. Currently use of the parse context is quite limited but it will be expanded upon in the future. It can be used for the following:
Expand Down
69 changes: 69 additions & 0 deletions docs/docs/custom-schemas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
sidebar_position: 5
toc_min_heading_level: 2
toc_max_heading_level: 4
---

# Creating Custom Schemas

> Please read the [Anatomy of a Schema](/core-concepts/anatomy-of-schema) page before continuing.

Currently Zog plans to support three different ways of creating custom schemas. Although this is subject to change and some of these are not yet implemented so keep an eye out on this page as it gets updated, [more details on my thoughts here](https://github.com/Oudwins/zog/discussions/132).

1. Generics on Primitive Schemas for custom strings, numbers, booleans, etc...
2. Custom Schemas Interface you can implement to create a 100% custom schema (Not yet implemented)
3. A system by which you can define a schema for a custom type or interface that after some transformation can become a normal zog schema.

## Creating Custom Schemas for Primitive Types

This is quite simple to do for the supported primitive types (string, number, boolean). Here is an example:

```go
// definition in your code
type Env string
const (
Prod Env = "prod".
Dev Env = "env"
)
func EnvSchema() *StringSchema[Env] {
s := &z.StringSchema[Env]
return s.OneOf([]Env{Prod, Dev}) // you can also just return the schema and define the tests when calling it it doesn't matter
}
// usage
type S struct {
Environment Env
}
schema := z.Struct(
z.Schema{
"Environment": EnvSchema() // All string methods will now be typed to Env type
}
)
```
Comment on lines +21 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Code Example (Primitive Types): Go Syntax and Consistency

  • In line 25, the constant declaration uses an extraneous period ("prod".). It is recommended to remove the period.
  • In line 29, the instantiation s := &z.StringSchema[Env] may be more idiomatically written as s := &z.StringSchema[Env]{} to clearly initialize a new instance.
-  Prod Env = "prod".
+  Prod Env = "prod"

- s := &z.StringSchema[Env]
+ s := &z.StringSchema[Env]{}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```go
// definition in your code
type Env string
const (
Prod Env = "prod".
Dev Env = "env"
)
func EnvSchema() *StringSchema[Env] {
s := &z.StringSchema[Env]
return s.OneOf([]Env{Prod, Dev}) // you can also just return the schema and define the tests when calling it it doesn't matter
}
// usage
type S struct {
Environment Env
}
schema := z.Struct(
z.Schema{
"Environment": EnvSchema() // All string methods will now be typed to Env type
}
)
```


This becomes a little more complex if you need to use `Parse` instead of just `Validate` since you need to define a custom `Coercer` function. Here is what I would recommend and it is also very similar to the way Zog creates the schemas you use:

```go
// Definition
func EnvSchema(opts ...z.SchemaOption) *StringSchema[Env] {
s := &StringSchema[Env]{}
ops = append([]z.SchemaOption{
// This is required if you want to use Parse since we don't use reflection to set the value you need to coerce it manually
WithCoercer(func(x any) (any, error) {
v, e := conf.DefaultCoercers.String(x)
if e != nil {
return nil, e
}
return Env(v.(string)), nil
}),
...opts,
})
for _, op := range ops {
op(s)
}
return s
}
// Usage is the same as before
```

> Why is this so verbose?
> Although we considered introducing an API that would allow you to define this types of schemas in a more concise way (and we may still do so), to keep code consistency & reusability we recommend that you make a factory function like the one above for your custom types. And we felt that providing a simpler API could lead to people just inlining the schema's which would make it impossible to reuse them.
2 changes: 1 addition & 1 deletion docs/docs/errors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 5
sidebar_position: 6
toc_min_heading_level: 2
toc_max_heading_level: 4
---
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/performance.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 9
sidebar_position: 21
---

# Performance
Expand Down
3 changes: 2 additions & 1 deletion docs/docs/zog-schemas.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
sidebar_position: 7
sidebar_position: 8
---

# Zog Schemas
Expand Down Expand Up @@ -147,6 +147,7 @@ z.Float().OneOf([]float64{1.0, 2.0, 3.0}) // validates float is one of the value
// Tests / Validators
z.Bool().True() // validates bool is true
z.Bool().False() // validates bool is false
z.Bool().EQ(true) // validates bool is equal to true
```

### Times & Dates
Expand Down
2 changes: 1 addition & 1 deletion internals/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func Required() Test {
}

type LengthCapable[K any] interface {
~[]any | ~[]K | string | map[any]any | ~chan any
~[]any | ~[]K | ~string | map[any]any | ~chan any
}

func LenMin[T LengthCapable[any]](n int) Test {
Expand Down
Loading
Loading