-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfield.go
More file actions
286 lines (265 loc) · 7.4 KB
/
field.go
File metadata and controls
286 lines (265 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package fmt
// FieldType represents the abstract storage type of a struct field.
type FieldType int
const (
FieldText FieldType = iota // Any string
FieldInt // Any integer
FieldFloat // Any float
FieldBool // Boolean
FieldBlob // Binary data ([]byte)
FieldStruct // Nested struct (implements Fielder)
)
var fieldTypeNames = []string{"text", "int", "float", "bool", "blob", "struct"}
func (ft FieldType) String() string {
if int(ft) >= 0 && int(ft) < len(fieldTypeNames) {
return fieldTypeNames[ft]
}
return "unknown"
}
// Field describes a single field in a struct's schema.
// It provides type metadata, constraint flags, and validation rules
// used by database (orm), transport (json), UI (form), and validation layers.
//
// Validation rules are embedded via Permitted. When a field has validation
// configured, Field.Validate(value) checks the value against all rules.
// Fields without validation rules pass any value.
type Field struct {
Name string
Type FieldType
PK bool
Unique bool
NotNull bool
AutoInc bool
OmitEmpty bool // omit from JSON when zero value
Permitted // embedded: validation rules (characters, min/max)
}
// Validate checks a string value against this field's constraints.
// Checks NotNull first, then delegates to embedded Permitted.
func (f Field) Validate(value string) error {
if f.NotNull && value == "" {
return Err(f.Name, "required")
}
if value == "" {
return nil // empty + not required = ok
}
// Only run Permitted validation if any rule is configured
if f.hasPermittedRules() {
return f.Permitted.Validate(f.Name, value)
}
return nil
}
// hasPermittedRules returns true if any Permitted field is non-zero.
func (f Field) hasPermittedRules() bool {
return f.Letters || f.Tilde || f.Numbers || f.Spaces ||
f.BreakLine || f.Tab || len(f.Extra) > 0 ||
len(f.NotAllowed) > 0 || f.Minimum > 0 || f.Maximum > 0 ||
f.StartWith != nil
}
// Fielder describes any type that can expose its schema and
// writable pointers. It is the shared contract between orm (database),
// form (UI), and json layers, replacing runtime reflection.
//
// Implementations are generated by code generators — never written by hand.
//
// Contract:
// - Schema() and Pointers() MUST return slices of the same length.
// - The i-th element in each slice corresponds to the same struct field.
// - Pointers() returns pointers to fields for reading (dereference) and writing (scan/sync).
type Fielder interface {
Schema() []Field
Pointers() []any
}
// Validator is implemented by types that can self-validate.
// Generated by ormc: func (m *X) Validate(action byte) error { if err := fmt.ValidateFields(action, m); err != nil { return err }; return form.ValidateStructFormats(m) }
// Used by form, json.Decode, and orm pre-insert to enforce data integrity.
type Validator interface {
Validate(action byte) error
}
// Model describes a resource with a schema and a name.
// Standard interface used by DB, Form, and API layers.
type Model interface {
Fielder
ModelName() string
}
// SafeFields combines schema access with validation.
// Handlers that receive user input should accept SafeFields
// to enforce compile-time validation guarantees.
type SafeFields interface {
Fielder
Validator
}
// ValidateFields validates all fields of a Fielder based on the action ('c', 'u', 'd').
// For each FieldText field, reads the string value and calls Field.Validate.
// For non-text fields with NotNull, checks against zero value.
//
// This is the single validation entry point — ormc-generated Validate()
// methods call this function first.
func ValidateFields(action byte, f Fielder) error {
schema := f.Schema()
ptrs := f.Pointers()
for i, field := range schema {
// 'd' delete: only PK required, skip everything else
if action == 'd' {
if field.PK {
switch field.Type {
case FieldText:
val, _ := ReadStringPtr(ptrs[i])
if val == "" {
return Err(field.Name, "required")
}
default:
if isZeroPtr(ptrs[i], field.Type) {
return Err(field.Name, "required")
}
}
}
continue
}
// 'c' create: skip PK+AutoInc (DB assigns it)
if action == 'c' && field.PK && field.AutoInc {
continue
}
switch field.Type {
case FieldText:
val, _ := ReadStringPtr(ptrs[i])
// PK always required (in 'c' without AutoInc, in 'u', and any other)
if field.PK && val == "" {
return Err(field.Name, "required")
}
if err := field.Validate(val); err != nil {
return err
}
case FieldStruct:
// Recursive validation for nested structs
if validator, ok := ptrs[i].(Validator); ok {
if err := validator.Validate(action); err != nil {
return err
}
} else if fielder, ok := ptrs[i].(Fielder); ok {
if err := ValidateFields(action, fielder); err != nil {
return err
}
}
default:
// PK always required
if field.PK && isZeroPtr(ptrs[i], field.Type) {
return Err(field.Name, "required")
}
// Non-text fields: only check NotNull (zero value check)
if field.NotNull && isZeroPtr(ptrs[i], field.Type) {
return Err(field.Name, "required")
}
}
}
return nil
}
// isZeroPtr checks if a pointer points to a zero value.
func isZeroPtr(ptr any, ft FieldType) bool {
switch ft {
case FieldInt:
switch p := ptr.(type) {
case *int64:
return *p == 0
case *int:
return *p == 0
case *int32:
return *p == 0
case *uint:
return *p == 0
case *uint32:
return *p == 0
case *uint64:
return *p == 0
}
case FieldFloat:
switch p := ptr.(type) {
case *float64:
return *p == 0
case *float32:
return *p == 0
}
case FieldBool:
if p, ok := ptr.(*bool); ok {
return !*p
}
case FieldBlob:
if p, ok := ptr.(*[]byte); ok {
return len(*p) == 0
}
}
return false
}
// ReadValues reads field values through Pointers by dereferencing based on FieldType.
// Used by consumers that need []any (e.g., orm for SQL args).
// Hot-path consumers (json, form) should read through Pointers directly to avoid boxing.
func ReadValues(schema []Field, ptrs []any) []any {
vals := make([]any, len(schema))
for i, f := range schema {
if ptrs[i] == nil {
continue
}
switch f.Type {
case FieldText:
if p, ok := ptrs[i].(*string); ok && p != nil {
vals[i] = *p
}
case FieldInt:
switch p := ptrs[i].(type) {
case *int64:
if p != nil {
vals[i] = *p
}
case *int:
if p != nil {
vals[i] = *p
}
case *int32:
if p != nil {
vals[i] = *p
}
case *uint:
if p != nil {
vals[i] = *p
}
case *uint32:
if p != nil {
vals[i] = *p
}
case *uint64:
if p != nil {
vals[i] = *p
}
}
case FieldFloat:
switch p := ptrs[i].(type) {
case *float64:
if p != nil {
vals[i] = *p
}
case *float32:
if p != nil {
vals[i] = *p
}
}
case FieldBool:
if p, ok := ptrs[i].(*bool); ok && p != nil {
vals[i] = *p
}
case FieldBlob:
if p, ok := ptrs[i].(*[]byte); ok && p != nil {
vals[i] = *p
}
case FieldStruct:
vals[i] = ptrs[i] // pointer to nested struct IS the Fielder
}
}
return vals
}
// ReadStringPtr reads a string from a typed pointer.
// Returns the string value and true if the pointer is *string, or ("", false) otherwise.
func ReadStringPtr(ptr any) (string, bool) {
if p, ok := ptr.(*string); ok && p != nil {
return *p, true
}
return "", false
}