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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ func (u *User) Validate(action byte) error {
}
```

Usage:

```go
err := model.ValidateFields(model.ActionUpdate, user)
```

### Using the Codec

Generated `EncodeFields` and `DecodeFields` methods follow the typed codec pattern:
Expand Down
2 changes: 0 additions & 2 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package model
type FieldWriter interface {
String(name, val string)
Int(name string, val int64)
Uint(name string, val uint64)
Float(name string, val float64)
Bool(name string, val bool)
Bytes(name string, val []byte)
Expand Down Expand Up @@ -42,7 +41,6 @@ type Encodable interface {
type FieldReader interface {
String(name string) (string, bool)
Int(name string) (int64, bool)
Uint(name string) (uint64, bool)
Float(name string) (float64, bool)
Bool(name string) (bool, bool)
Bytes(name string) ([]byte, bool)
Expand Down
2 changes: 0 additions & 2 deletions docs/API_CODEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ Receives typed field values. Concrete implementations (like JSON or JS-value) re
type FieldWriter interface {
String(name, val string)
Int(name string, val int64)
Uint(name string, val uint64)
Float(name string, val float64)
Bool(name string, val bool)
Bytes(name string, val []byte)
Expand Down Expand Up @@ -79,7 +78,6 @@ Delivers typed field values by name. Implementations avoid building internal map
type FieldReader interface {
String(name string) (string, bool)
Int(name string) (int64, bool)
Uint(name string) (uint64, bool)
Float(name string) (float64, bool)
Bool(name string) (bool, bool)
Bytes(name string) ([]byte, bool)
Expand Down
21 changes: 17 additions & 4 deletions docs/API_FIELD.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,15 +299,28 @@ type SafeFields interface {

Generic function that iterates through a `Fielder`'s schema and pointers to perform full validation based on the action. It handles `FieldText` (calling `Field.Validate`) and `FieldStruct` (recursive validation).

#### Action Constants

The following `byte` constants should be used for the `action` parameter:

| Constant | Value | Description |
|----------|-------|-------------|
| `ActionCreate` | `'c'` | Model is being created (skips PK+AutoInc) |
| `ActionRead` | `'r'` | Model is being read (used for RBAC/tool convention) |
| `ActionUpdate` | `'u'` | Model is being updated (requires PK) |
| `ActionDelete` | `'d'` | Model is being deleted (only requires PK, skips other validation) |

#### Validation Matrix

| Action | PK + AutoInc | PK without AutoInc | NotNull | Permitted |
|--------|--------------|--------------------|---------|-----------|
| `'c'` create | skip (DB assigns) | required | required | applies |
| `'u'` update | required | required | required | applies |
| `'d'` delete | required | required | skip | skip |
| `ActionCreate` | skip (DB assigns) | required | required | applies |
| `ActionUpdate` | required | required | required | applies |
| `ActionDelete` | required | required | skip | skip |
| other/unknown | required | required | required | applies |

```go
err := model.ValidateFields('u', myFielder)
err := model.ValidateFields(model.ActionUpdate, myFielder)
```

## Conversion and Reading Helpers
Expand Down
File renamed without changes.
8 changes: 8 additions & 0 deletions docs/WHY_64BIT_ONLY.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,11 @@ truncation bugs. The storage layer agrees: SQLite integers are 64-bit and Postgr
silent `int64 → int` narrowing already exists there. Keep `FieldIntSlice` for small
enumerations (role lists, option ids); values beyond 32 bits belong in scalar `int64`
fields or `FieldBlob`.

## Removal of `Uint`

The codec interfaces (`FieldWriter` and `FieldReader`) originally included `Uint` methods.
These were removed to simplify the API and reduce binary size. The single ecosystem-wide
caller (`binary.Message`'s `uint32` correlation ID) was migrated losslessly to `Int`.
Genuine 64-bit unsigned values (like hashes) that might exceed `int64` should be
represented as `FieldText` (hex) or `FieldBlob` (8 bytes).
30 changes: 24 additions & 6 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,36 @@ func (f Field) hasPermittedRules() bool {
len(f.NotAllowed) > 0 || f.StartWith != nil
}

// ValidateFields validates all fields of a Fielder based on the action ('c', 'u', 'd').
// CRUD action bytes — the single source of truth for the ecosystem-wide
// action convention used by Validator, ValidateFields, and downstream
// libraries (crudp HTTP mapping, mcp Tool.Action / RBAC, form).
//
// Deliberately plain byte constants (not a named type) so every existing
// `action byte` signature accepts them unchanged.
//
// Not to be confused with orm.Action (an int enum describing query
// execution plans) — that is a different, orm-internal concept.
const (
ActionCreate byte = 'c'
ActionRead byte = 'r'
ActionUpdate byte = 'u'
ActionDelete byte = 'd'
)

// ValidateFields validates all fields of a Fielder based on the action.
// For each FieldText field, reads the string value and calls Field.Validate.
// For non-text fields with NotNull, checks against zero value.
//
// Common actions are ActionCreate, ActionUpdate, and ActionDelete.
//
// 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' {
// ActionDelete: only PK required, skip everything else
if action == ActionDelete {
if field.IsPK() {
switch field.Type {
case FieldText, FieldRaw:
Expand All @@ -182,16 +200,16 @@ func ValidateFields(action byte, f Fielder) error {
continue
}

// 'c' create: skip PK+AutoInc (DB assigns it)
if action == 'c' && field.IsPK() && field.IsAutoInc() {
// ActionCreate: skip PK+AutoInc (DB assigns it)
if action == ActionCreate && field.IsPK() && field.IsAutoInc() {
continue
}

switch field.Type {
case FieldText, FieldRaw:
val, _ := ReadStringPtr(ptrs[i])

// PK always required (in 'c' without AutoInc, in 'u', and any other)
// PK always required (in ActionCreate without AutoInc, in ActionUpdate, and any other)
if field.IsPK() && val == "" {
return fmt.Err(field.Name, "required")
}
Expand Down
21 changes: 0 additions & 21 deletions tests/codec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,6 @@ func (m *mockFieldWriter) Int(name string, val int64) {
m.buf.WriteByte(';')
}

func (m *mockFieldWriter) Uint(name string, val uint64) {
m.buf.WriteString(name)
m.buf.WriteByte('=')
m.conv.ResetBuffer(fmt.BuffWork)
m.conv.WrIntBase(fmt.BuffWork, int64(val), 10, false)
m.buf.Write(m.conv.GetBytes(fmt.BuffWork))
m.buf.WriteByte(';')
}

func (m *mockFieldWriter) Float(name string, val float64) {
m.buf.WriteString(name)
m.buf.WriteByte('=')
Expand Down Expand Up @@ -181,18 +172,6 @@ func (r *mockFieldReader) Int(name string) (int64, bool) {
return v, true
}

func (r *mockFieldReader) Uint(name string) (uint64, bool) {
val, ok := r.find(name)
if !ok {
return 0, false
}
c := fmt.GetConv()
c.LoadBytes(val)
v, _ := c.Uint64()
c.PutConv()
return v, true
}

func (r *mockFieldReader) Float(name string) (float64, bool) {
val, ok := r.find(name)
if !ok {
Expand Down
62 changes: 34 additions & 28 deletions tests/field_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,9 @@ type fullMock struct {
Int int
Int32 int32
Int64 int64
Uint uint
Uint32 uint32
Uint64 uint64
U uint
U32 uint32
U64 uint64
Float float64
Float32 float32
Bool bool
Expand All @@ -286,9 +286,9 @@ func (m *fullMock) Schema() []Field {
{Name: "int", Type: FieldInt, NotNull: true},
{Name: "int32", Type: FieldInt},
{Name: "int64", Type: FieldInt},
{Name: "uint", Type: FieldInt},
{Name: "uint32", Type: FieldInt},
{Name: "uint64", Type: FieldInt},
{Name: "u", Type: FieldInt},
{Name: "u32", Type: FieldInt},
{Name: "u64", Type: FieldInt},
{Name: "float", Type: FieldFloat, NotNull: true},
{Name: "float32", Type: FieldFloat},
{Name: "bool", Type: FieldBool, NotNull: true},
Expand All @@ -299,7 +299,7 @@ func (m *fullMock) Schema() []Field {
}

func (m *fullMock) Pointers() []any {
return []any{&m.Text, &m.Int, &m.Int32, &m.Int64, &m.Uint, &m.Uint32, &m.Uint64, &m.Float, &m.Float32, &m.Bool, &m.Blob, &m.Raw, m.Nested}
return []any{&m.Text, &m.Int, &m.Int32, &m.Int64, &m.U, &m.U32, &m.U64, &m.Float, &m.Float32, &m.Bool, &m.Blob, &m.Raw, m.Nested}
}

type mockUser struct {
Expand All @@ -316,6 +316,12 @@ func (m *mockUser) Schema() []Field {
func (m *mockUser) Pointers() []any { return []any{&m.id, &m.name} }
func (m *mockUser) Validate(action byte) error { return ValidateFields(action, m) }

func TestActionConstantValues(t *testing.T) {
if ActionCreate != 'c' || ActionRead != 'r' || ActionUpdate != 'u' || ActionDelete != 'd' {
t.Fatal("action constants must keep their CRUD byte values — ecosystem wire/RBAC contract")
}
}

func TestValidateFieldsRecursive(t *testing.T) {
m := &fullMock{
Text: "hello",
Expand All @@ -327,20 +333,20 @@ func TestValidateFieldsRecursive(t *testing.T) {
Nested: &mockUser{id: "u1", name: "Alice"},
}

if err := ValidateFields('u', m); err != nil {
if err := ValidateFields(ActionUpdate, m); err != nil {
t.Errorf("expected success, got %v", err)
}

// Fail nested
m.Nested.name = ""
if err := ValidateFields('u', m); err == nil {
if err := ValidateFields(ActionUpdate, m); err == nil {
t.Error("expected failure in nested struct")
}

// Fail other types
m.Nested.name = "Alice"
m.Int = 0 // NotNull
if err := ValidateFields('u', m); err == nil {
if err := ValidateFields(ActionUpdate, m); err == nil {
t.Error("expected failure for int zero")
}
}
Expand All @@ -351,9 +357,9 @@ func TestReadValuesAllTypes(t *testing.T) {
Int: 10,
Int32: 32,
Int64: 64,
Uint: 100,
Uint32: 320,
Uint64: 640,
U: 100,
U32: 320,
U64: 640,
Float: 1.1,
Float32: 2.2,
Bool: true,
Expand Down Expand Up @@ -527,7 +533,7 @@ func TestValidateFieldsWithOnlyFielder(t *testing.T) {
ptrs := []any{sub}

// Validate it through the manualFielder helper
if err := ValidateFields('u', &manualFielder{schema, ptrs}); err != nil {
if err := ValidateFields(ActionUpdate, &manualFielder{schema, ptrs}); err != nil {
t.Errorf("expected success, got %v", err)
}
}
Expand Down Expand Up @@ -562,34 +568,34 @@ func TestValidateFieldsActions(t *testing.T) {
f := getFielder(m)

// PK+AutoInc should be skipped in 'c'
if err := ValidateFields('c', f); err != nil {
t.Errorf("expected success in 'c' with zero ID, got %v", err)
if err := ValidateFields(ActionCreate, f); err != nil {
t.Errorf("expected success in ActionCreate with zero ID, got %v", err)
}

// NotNull still applies
m.Name = ""
if err := ValidateFields('c', f); err == nil {
t.Error("expected failure in 'c' with empty Name")
if err := ValidateFields(ActionCreate, f); err == nil {
t.Error("expected failure in ActionCreate with empty Name")
}
m.Name = "Alice"
m.Raw = ""
if err := ValidateFields('c', f); err == nil {
t.Error("expected failure in 'c' with empty Raw")
if err := ValidateFields(ActionCreate, f); err == nil {
t.Error("expected failure in ActionCreate with empty Raw")
}
})

t.Run("Update 'u'", func(t *testing.T) {
m := &userActionMock{ID: 1, Name: "Alice", Email: "a@b.com", Raw: "{}", Version: 1}
f := getFielder(m)

if err := ValidateFields('u', f); err != nil {
t.Errorf("expected success in 'u', got %v", err)
if err := ValidateFields(ActionUpdate, f); err != nil {
t.Errorf("expected success in ActionUpdate, got %v", err)
}

// PK is required in 'u'
m.ID = 0
if err := ValidateFields('u', f); err == nil {
t.Error("expected failure in 'u' with zero ID")
if err := ValidateFields(ActionUpdate, f); err == nil {
t.Error("expected failure in ActionUpdate with zero ID")
}
})

Expand All @@ -598,14 +604,14 @@ func TestValidateFieldsActions(t *testing.T) {
f := getFielder(m)

// Only PK matters in 'd'
if err := ValidateFields('d', f); err != nil {
t.Errorf("expected success in 'd' with only PK, got %v", err)
if err := ValidateFields(ActionDelete, f); err != nil {
t.Errorf("expected success in ActionDelete with only PK, got %v", err)
}

// PK missing
m.ID = 0
if err := ValidateFields('d', f); err == nil {
t.Error("expected failure in 'd' with missing PK")
if err := ValidateFields(ActionDelete, f); err == nil {
t.Error("expected failure in ActionDelete with missing PK")
}
})

Expand Down