• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Oudwins / zog / 16250290102

13 Jul 2025 02:43PM UTC coverage: 86.259% (+0.6%) from 85.639%
16250290102

push

github

web-flow
feat: like primitive types (#172)

* wp

* test: tests

* test: fixed stringlike tests

* docs: primitive like schemas

* refactor: numeric type

* docs: fix

58 of 60 new or added lines in 3 files covered. (96.67%)

2172 of 2518 relevant lines covered (86.26%)

56.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.08
/string.go
1
package zog
2

3
import (
4
        "net/url"
5
        "regexp"
6
        "strings"
7

8
        "github.com/Oudwins/zog/conf"
9
        p "github.com/Oudwins/zog/internals"
10
        "github.com/Oudwins/zog/internals/is"
11
        "github.com/Oudwins/zog/zconst"
12
)
13

14
var (
15
        _ PrimitiveZogSchema[string] = (*StringSchema[string])(nil)
16
        _ NotStringSchema[string]    = (*StringSchema[string])(nil)
17
)
18

19
type likeString interface {
20
        ~string
21
}
22

23
type NotStringSchema[T likeString] interface {
24
        OneOf(enum []T, options ...TestOption) *StringSchema[T]
25
        Len(n int, options ...TestOption) *StringSchema[T]
26
        Email(options ...TestOption) *StringSchema[T]
27
        URL(options ...TestOption) *StringSchema[T]
28
        HasPrefix(s T, options ...TestOption) *StringSchema[T]
29
        HasSuffix(s T, options ...TestOption) *StringSchema[T]
30
        Contains(sub T, options ...TestOption) *StringSchema[T]
31
        ContainsUpper(options ...TestOption) *StringSchema[T]
32
        ContainsDigit(options ...TestOption) *StringSchema[T]
33
        ContainsSpecial(options ...TestOption) *StringSchema[T]
34
        UUID(options ...TestOption) *StringSchema[T]
35
        Match(regex *regexp.Regexp, options ...TestOption) *StringSchema[T]
36

37
        // `Test` method is missing here as we require the user to define their own test for their use case.
38
        // `Not` method is missing here as we do not want the user to do `Not` chaining.
39
        // `NotNil`, `Min`, `Max` methods are not included as they are opposites of each other.
40
}
41

42
type StringSchema[T likeString] struct {
43
        processors []p.ZProcessor[*T]
44
        defaultVal *T
45
        required   *p.Test[*T]
46
        catch      *T
47
        coercer    CoercerFunc
48
        isNot      bool
49
}
50

51
// ! INTERNALS
52

53
// Returns the type of the schema
54
func (v *StringSchema[T]) getType() zconst.ZogType {
363✔
55
        return zconst.TypeString
363✔
56
}
363✔
57

58
// Sets the coercer for the schema
59
func (v *StringSchema[T]) setCoercer(c CoercerFunc) {
26✔
60
        v.coercer = c
26✔
61
}
26✔
62

63
// ! USER FACING FUNCTIONS
64

65
func StringLike[T likeString](opts ...SchemaOption) *StringSchema[T] {
41✔
66
        s := &StringSchema[T]{
41✔
67
                coercer: func(val any) (any, error) {
85✔
68
                        v, err := conf.Coercers.String(val)
44✔
69
                        if err != nil {
44✔
NEW
70
                                return nil, err
×
NEW
71
                        }
×
72
                        return T(v.(string)), nil
44✔
73
                },
74
        }
75
        for _, opt := range opts {
42✔
76
                opt(s)
1✔
77
        }
1✔
78
        return s
41✔
79
}
80

81
// Returns a new String Shape
82
func String(opts ...SchemaOption) *StringSchema[string] {
182✔
83
        s := &StringSchema[string]{
182✔
84
                coercer: conf.Coercers.String, // default coercer
182✔
85
        }
182✔
86
        for _, opt := range opts {
185✔
87
                opt(s)
3✔
88
        }
3✔
89
        return s
182✔
90
}
91

92
// Parses the data into the destination string. Returns a list of ZogIssues
93
func (v *StringSchema[T]) Parse(data any, dest *T, options ...ExecOption) ZogIssueList {
137✔
94
        errs := p.NewErrsList()
137✔
95
        defer errs.Free()
137✔
96

137✔
97
        ctx := p.NewExecCtx(errs, conf.IssueFormatter)
137✔
98
        defer ctx.Free()
137✔
99
        for _, opt := range options {
138✔
100
                opt(ctx)
1✔
101
        }
1✔
102

103
        path := p.NewPathBuilder()
137✔
104
        defer path.Free()
137✔
105
        sctx := ctx.NewSchemaCtx(data, dest, path, v.getType())
137✔
106
        defer sctx.Free()
137✔
107
        v.process(sctx)
137✔
108

137✔
109
        return errs.List
137✔
110
}
111

112
// Internal function to process the data
113
func (v *StringSchema[T]) process(ctx *p.SchemaCtx) {
245✔
114
        primitiveParsing(ctx, v.processors, v.defaultVal, v.required, v.catch, v.coercer, p.IsParseZeroValue)
245✔
115
}
245✔
116

117
// Validate Given string
118
func (v *StringSchema[T]) Validate(data *T, options ...ExecOption) ZogIssueList {
78✔
119
        errs := p.NewErrsList()
78✔
120
        defer errs.Free()
78✔
121
        ctx := p.NewExecCtx(errs, conf.IssueFormatter)
78✔
122
        defer ctx.Free()
78✔
123
        for _, opt := range options {
79✔
124
                opt(ctx)
1✔
125
        }
1✔
126

127
        path := p.NewPathBuilder()
78✔
128
        defer path.Free()
78✔
129
        sctx := ctx.NewSchemaCtx(data, data, path, v.getType())
78✔
130
        defer sctx.Free()
78✔
131
        v.validate(sctx)
78✔
132
        return errs.List
78✔
133
}
134

135
// Internal function to validate the data
136
func (v *StringSchema[T]) validate(ctx *p.SchemaCtx) {
149✔
137
        primitiveValidation(ctx, v.processors, v.defaultVal, v.required, v.catch)
149✔
138
}
149✔
139

140
// Transform: trims the input data of whitespace if it is a string
141
func (v *StringSchema[T]) Trim() *StringSchema[T] {
3✔
142
        v.processors = append(v.processors, &p.TransformProcessor[*T]{
3✔
143
                Transform: func(val *T, ctx Ctx) error {
8✔
144
                        *val = T(strings.TrimSpace(string(*val)))
5✔
145
                        return nil
5✔
146
                },
5✔
147
        })
148

149
        return v
3✔
150
}
151

152
// Adds a transform function to the schema. Runs in the order it is called
153
func (v *StringSchema[T]) Transform(transform p.Transform[*T]) *StringSchema[T] {
9✔
154
        v.processors = append(v.processors, &p.TransformProcessor[*T]{Transform: transform})
9✔
155
        return v
9✔
156
}
9✔
157

158
// ! MODIFIERS
159

160
// marks field as required
161
func (v *StringSchema[T]) Required(options ...TestOption) *StringSchema[T] {
70✔
162
        r := p.Required[*T]()
70✔
163
        for _, opt := range options {
75✔
164
                opt(&r)
5✔
165
        }
5✔
166
        v.required = &r
70✔
167
        return v
70✔
168
}
169

170
// marks field as optional
171
func (v *StringSchema[T]) Optional() *StringSchema[T] {
6✔
172
        v.required = nil
6✔
173
        return v
6✔
174
}
6✔
175

176
// sets the default value
177
func (v *StringSchema[T]) Default(val T) *StringSchema[T] {
7✔
178
        v.defaultVal = &val
7✔
179
        return v
7✔
180
}
7✔
181

182
// sets the catch value (i.e the value to use if the validation fails)
183
func (v *StringSchema[T]) Catch(val T) *StringSchema[T] {
7✔
184
        v.catch = &val
7✔
185
        return v
7✔
186
}
7✔
187

188
// ! Tests
189
// custom test function call it -> schema.Test(t z.Test, opts ...TestOption)
190
func (v *StringSchema[T]) Test(t Test[*T]) *StringSchema[T] {
13✔
191
        x := p.Test[*T](t)
13✔
192
        v.processors = append(v.processors, &x)
13✔
193
        return v
13✔
194
}
13✔
195

196
// Create a custom test function for the schema. This is similar to Zod's `.refine()` method.
197
func (v *StringSchema[T]) TestFunc(testFunc BoolTFunc[*T], options ...TestOption) *StringSchema[T] {
8✔
198
        test := p.NewTestFunc("", p.BoolTFunc[*T](testFunc), options...)
8✔
199
        v.Test(Test[*T](*test))
8✔
200
        return v
8✔
201
}
8✔
202

203
// Test: checks that the value is one of the enum values
204
func (v *StringSchema[T]) OneOf(enum []T, options ...TestOption) *StringSchema[T] {
4✔
205
        t, fn := p.In(enum)
4✔
206
        return v.addTest(t, fn, options...)
4✔
207
}
4✔
208

209
// Test: checks that the value is at least n characters long
210
func (v *StringSchema[T]) Min(n int, options ...TestOption) *StringSchema[T] {
45✔
211
        t, fn := p.LenMin[T](n)
45✔
212
        return v.addTest(t, fn, options...)
45✔
213
}
45✔
214

215
// Test: checks that the value is at most n characters long
216
func (v *StringSchema[T]) Max(n int, options ...TestOption) *StringSchema[T] {
7✔
217
        t, fn := p.LenMax[T](n)
7✔
218
        return v.addTest(t, fn, options...)
7✔
219
}
7✔
220

221
// Test: checks that the value is exactly n characters long
222
func (v *StringSchema[T]) Len(n int, options ...TestOption) *StringSchema[T] {
17✔
223
        t, fn := p.Len[T](n)
17✔
224
        return v.addTest(t, fn, options...)
17✔
225
}
17✔
226

227
// Test: checks that the value is a valid email address
228
func (v *StringSchema[T]) Email(options ...TestOption) *StringSchema[T] {
15✔
229
        t := p.Test[*T]{IssueCode: zconst.IssueCodeEmail}
15✔
230
        fn := func(v *T, ctx Ctx) bool {
33✔
231
                return is.Email(string(*v))
18✔
232
        }
18✔
233
        return v.addTest(t, fn, options...)
15✔
234
}
235

236
// Test: checks that the value is a valid URL
237
func (v *StringSchema[T]) URL(options ...TestOption) *StringSchema[T] {
10✔
238
        t := p.Test[*T]{IssueCode: zconst.IssueCodeURL}
10✔
239
        fn := func(v *T, ctx Ctx) bool {
24✔
240
                u, err := url.Parse(string(*v))
14✔
241
                return err == nil && u.Scheme != "" && u.Host != ""
14✔
242
        }
14✔
243
        return v.addTest(t, fn, options...)
10✔
244
}
245

246
// Test: checks that the value has the prefix
247
func (v *StringSchema[T]) HasPrefix(s T, options ...TestOption) *StringSchema[T] {
12✔
248
        t := p.Test[*T]{IssueCode: zconst.IssueCodeHasPrefix, Params: make(map[string]any, 1)}
12✔
249
        t.Params[zconst.IssueCodeHasPrefix] = string(s)
12✔
250
        fn := func(v *T, ctx Ctx) bool {
27✔
251
                return strings.HasPrefix(string(*v), string(s))
15✔
252
        }
15✔
253
        return v.addTest(t, fn, options...)
12✔
254
}
255

256
// Test: checks that the value has the suffix
257
func (v *StringSchema[T]) HasSuffix(s T, options ...TestOption) *StringSchema[T] {
6✔
258
        t := p.Test[*T]{
6✔
259
                IssueCode: zconst.IssueCodeHasSuffix,
6✔
260
                Params:    make(map[string]any, 1),
6✔
261
        }
6✔
262
        t.Params[zconst.IssueCodeHasSuffix] = string(s)
6✔
263
        fn := func(v *T, ctx Ctx) bool {
15✔
264
                return strings.HasSuffix(string(*v), string(s))
9✔
265
        }
9✔
266
        return v.addTest(t, fn, options...)
6✔
267
}
268

269
// Test: checks that the value contains the substring
270
func (v *StringSchema[T]) Contains(sub T, options ...TestOption) *StringSchema[T] {
25✔
271
        t := p.Test[*T]{
25✔
272
                IssueCode: zconst.IssueCodeContains,
25✔
273
                Params:    make(map[string]any, 1),
25✔
274
        }
25✔
275
        t.Params[zconst.IssueCodeContains] = string(sub)
25✔
276
        fn := func(v *T, ctx Ctx) bool {
53✔
277
                return strings.Contains(string(*v), string(sub))
28✔
278
        }
28✔
279
        return v.addTest(t, fn, options...)
25✔
280
}
281

282
// Test: checks that the value contains an uppercase letter
283
func (v *StringSchema[T]) ContainsUpper(options ...TestOption) *StringSchema[T] {
3✔
284
        t := p.Test[*T]{IssueCode: zconst.IssueCodeContainsUpper}
3✔
285
        fn := func(v *T, ctx Ctx) bool {
9✔
286
                for _, r := range string(*v) {
50✔
287
                        if r >= 'A' && r <= 'Z' {
47✔
288
                                return true
3✔
289
                        }
3✔
290
                }
291
                return false
3✔
292
        }
293

294
        return v.addTest(t, fn, options...)
3✔
295
}
296

297
// Test: checks that the value contains a digit
298
func (v *StringSchema[T]) ContainsDigit(options ...TestOption) *StringSchema[T] {
3✔
299
        t := p.Test[*T]{IssueCode: zconst.IssueCodeContainsDigit}
3✔
300
        fn := func(v *T, ctx Ctx) bool {
9✔
301
                for _, r := range string(*v) {
42✔
302
                        if r >= '0' && r <= '9' {
39✔
303
                                return true
3✔
304
                        }
3✔
305
                }
306
                return false
3✔
307
        }
308

309
        return v.addTest(t, fn, options...)
3✔
310
}
311

312
// Test: checks that the value contains a special character
313
func (v *StringSchema[T]) ContainsSpecial(options ...TestOption) *StringSchema[T] {
3✔
314
        t := p.Test[*T]{IssueCode: zconst.IssueCodeContainsSpecial}
3✔
315
        fn := func(v *T, ctx Ctx) bool {
9✔
316
                for _, r := range string(*v) {
66✔
317
                        if (r >= '!' && r <= '/') ||
60✔
318
                                (r >= ':' && r <= '@') ||
60✔
319
                                (r >= '[' && r <= '`') ||
60✔
320
                                (r >= '{' && r <= '~') {
63✔
321
                                return true
3✔
322
                        }
3✔
323
                }
324
                return false
3✔
325
        }
326

327
        return v.addTest(t, fn, options...)
3✔
328
}
329

330
// Test: checks that the value is a valid uuid
331
func (v *StringSchema[T]) UUID(options ...TestOption) *StringSchema[T] {
4✔
332
        t := p.Test[*T]{IssueCode: zconst.IssueCodeUUID}
4✔
333
        fn := func(v *T, ctx Ctx) bool {
14✔
334
                return is.UUIDv4(string(*v))
10✔
335
        }
10✔
336

337
        return v.addTest(t, fn, options...)
4✔
338
}
339

340
// Test: checks that value matches to regex
341
func (v *StringSchema[T]) Match(regex *regexp.Regexp, options ...TestOption) *StringSchema[T] {
4✔
342
        t := p.Test[*T]{IssueCode: zconst.IssueCodeMatch, Params: make(map[string]any, 1)}
4✔
343
        t.Params[zconst.IssueCodeMatch] = regex.String()
4✔
344
        fn := func(v *T, ctx Ctx) bool {
13✔
345
                return regex.MatchString(string(*v))
9✔
346
        }
9✔
347

348
        return v.addTest(t, fn, options...)
4✔
349
}
350

351
// Not returns a schema that negates the next validation test.
352
// For example, `z.String().Not().Email()` validates that the string is NOT a valid email.
353
// Note: The negation only applies to the next validation test and is reset afterward.
354
func (v *StringSchema[T]) Not() NotStringSchema[T] {
27✔
355
        v.isNot = true
27✔
356
        return v
27✔
357
}
27✔
358

359
func (v *StringSchema[T]) addTest(t p.Test[*T], fn p.BoolTFunc[*T], options ...TestOption) *StringSchema[T] {
158✔
360
        if v.isNot {
185✔
361
                p.TestNotFuncFromBool(fn, &t)
27✔
362
                t.IssueCode = zconst.NotIssueCode(t.IssueCode)
27✔
363
                v.isNot = false
27✔
364
        } else {
158✔
365
                p.TestFuncFromBool(fn, &t)
131✔
366
        }
131✔
367

368
        for _, opt := range options {
175✔
369
                opt(&t)
17✔
370
        }
17✔
371

372
        v.processors = append(v.processors, &t)
158✔
373
        return v
158✔
374
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc