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
10 changes: 10 additions & 0 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ func New(exec Executor, compiler Compiler) *DB {

// Create inserts a new model into the database.
func (db *DB) Create(m Model) error {
if v, ok := m.(fmt.Validator); ok {
if err := v.Validate(); err != nil {
return err
}
}
if err := validate(ActionCreate, m); err != nil {
return err
}
Expand Down Expand Up @@ -54,6 +59,11 @@ func (db *DB) Create(m Model) error {
// Providing zero conditions is a compile-time error — there is no variadic
// fallback — preventing accidental full-table UPDATE statements.
func (db *DB) Update(m Model, cond Condition, rest ...Condition) error {
if v, ok := m.(fmt.Validator); ok {
if err := v.Validate(); err != nil {
return err
}
}
if err := validate(ActionUpdate, m); err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions docs/ARQUITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ type Fielder interface {
- `Name string` — column name (snake_case)
- `Type fmt.FieldType` — `FieldText`, `FieldInt`, `FieldFloat`, `FieldBool`, `FieldBlob`, `FieldStruct`
- `PK bool`, `Unique bool`, `NotNull bool`, `AutoInc bool` — schema constraints
- `Input string` — hint from `form:` struct tag (read by `tinywasm/form`)
- `JSON string` — hint from `json:` struct tag (read by `tinywasm/json`)
- `OmitEmpty bool` — hint from `json:",omitempty"` struct tag (read by `tinywasm/json`)
- `Permitted Permitted` — validation rules from `validate:` struct tag (read by `fmt.ValidateFielder`)

> `Pointers()` are only called by the Executor logic for the operations that require them.

Expand Down
File renamed without changes.
17 changes: 8 additions & 9 deletions docs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ type Fielder interface {
| `Unique bool` | `db:"unique"` | |
| `NotNull bool` | `db:"not_null"` | |
| `AutoInc bool` | `db:"autoincrement"` | Numeric fields only |
| `Input string` | `form:"email"` | Hint for form rendering; `form:"-"` = skip |
| `JSON string` | `json:"name"` | Hint for JSON codec; `json:"-"` = skip |
| `OmitEmpty bool` | `json:",omitempty"` | Propagated from `json` tag |
| `Permitted fmt.Permitted` | `validate:"..."` | Validation rules for characters and bounds |
| FK reference | `db:"ref=table"` or `db:"ref=table:column"` | stored in `FieldExt.Ref` + `FieldExt.RefColumn` |
| Ignore field | `db:"-"` | Silently excluded from `Schema()`, `Pointers()` |

Expand Down Expand Up @@ -107,19 +107,18 @@ Use a single `//go:generate` at the project root — **not** per struct:
- `ReadOneT(qb *orm.QB, model *T) (*T, error)`
- `ReadAllT(qb *orm.QB) ([]*T, error)`

### `form:` and `json:` tags
### `validate:` and `json:` tags

`ormc` reads struct tags and propagates them to `fmt.Field`:

| Tag | Field | Generated |
|---|---|---|
| `form:"email"` | `Input = "email"` | `Input: "email"` |
| `form:"-"` | `Input = "-"` | `Input: "-"` (form skips it) |
| `json:"name"` | `JSON = "name"` | `JSON: "name"` |
| `json:"bio,omitempty"` | `JSON = "bio,omitempty"` | `JSON: "bio,omitempty"` |
| `json:"-"` | `JSON = "-"` | `JSON: "-"` (json skips it) |
| `validate:"required"` | `NotNull = true` | `NotNull: true` |
| `validate:"email"` | `Format = "email"` | Injects `form.ValidateEmail` call in `Validate()` |
| `validate:"min=2"` | `Permitted.Minimum = 2` | `Permitted: {Minimum: 2}` |
| `json:"bio,omitempty"` | `OmitEmpty = true` | `OmitEmpty: true` |

No `form` or `json` dependencies are imported or generated. The downstream `form` and `json` packages read `Field.Input` / `Field.JSON` autonomously.
`ormc` generates a composite `Validate()` method that invokes `fmt.ValidateFielder(m)` plus any format validators requested by the `validate:` tag.

### `// ormc:formonly` directive

Expand Down
2 changes: 1 addition & 1 deletion docs/img/badges.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
182 changes: 166 additions & 16 deletions ormc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"

. "github.com/tinywasm/fmt"
)
Expand All @@ -25,8 +26,16 @@ type FieldInfo struct {
RefColumn string
IsPK bool
GoType string
Input string
JSON string
OmitEmpty bool
// Permitted config — populated from validate:"..." tag
Letters bool
Tilde bool
Numbers bool
Spaces bool
Extra []rune
Minimum int
Maximum int
Format string // "email", "phone", etc. (triggers validator call generation)
}

// SliceFieldInfo records a slice-of-struct field found in a parent struct.
Expand Down Expand Up @@ -157,18 +166,18 @@ func (o *Ormc) ParseStruct(structName string, goFile string) (StructInfo, error)
}

dbTag := ""
formTag := ""
jsonTag := ""
validateTag := ""
if field.Tag != nil {
tagVal := Convert(field.Tag.Value).TrimPrefix("`").TrimSuffix("`").String()
parts := Convert(tagVal).Split(" ")
for _, p := range parts {
if HasPrefix(p, "db:\"") {
dbTag = Convert(p).TrimPrefix(`db:"`).TrimSuffix(`"`).String()
} else if HasPrefix(p, "form:\"") {
formTag = Convert(p).TrimPrefix(`form:"`).TrimSuffix(`"`).String()
} else if HasPrefix(p, "json:\"") {
jsonTag = Convert(p).TrimPrefix(`json:"`).TrimSuffix(`"`).String()
} else if HasPrefix(p, "validate:\"") {
validateTag = Convert(p).TrimPrefix(`validate:"`).TrimSuffix(`"`).String()
}
}
}
Expand Down Expand Up @@ -285,7 +294,17 @@ func (o *Ormc) ParseStruct(structName string, goFile string) (StructInfo, error)
}
}

info.Fields = append(info.Fields, FieldInfo{
omitEmpty := false
if jsonTag != "" {
parts := Convert(jsonTag).Split(",")
for _, p := range parts {
if p == "omitempty" {
omitEmpty = true
}
}
}

fi := FieldInfo{
Name: fieldName,
ColumnName: colName,
Type: fieldType,
Expand All @@ -297,14 +316,58 @@ func (o *Ormc) ParseStruct(structName string, goFile string) (StructInfo, error)
RefColumn: refCol,
IsPK: fieldIsPK,
GoType: typeStr,
Input: formTag,
JSON: jsonTag,
})
OmitEmpty: omitEmpty,
}

if validateTag != "" {
parseValidateTag(validateTag, &fi)
}

info.Fields = append(info.Fields, fi)
}

return info, nil
}

// parseValidateTag maps validate:"..." rules to FieldInfo Permitted fields.
func parseValidateTag(tag string, fi *FieldInfo) {
parts := Convert(tag).Split(",")
for _, v := range parts {
switch {
case v == "required":
fi.NotNull = true
case v == "email":
fi.Format = "email"
case v == "phone":
fi.Format = "phone"
case v == "ip":
fi.Format = "ip"
case v == "rut":
fi.Format = "rut"
case v == "date":
fi.Format = "date"
case v == "name":
fi.Letters = true
fi.Tilde = true
fi.Spaces = true
case v == "letters":
fi.Letters = true
case v == "numbers":
fi.Numbers = true
case v == "tilde":
fi.Tilde = true
case v == "spaces":
fi.Spaces = true
case HasPrefix(v, "min="):
n, _ := Convert(v).TrimPrefix("min=").Int64()
fi.Minimum = int(n)
case HasPrefix(v, "max="):
n, _ := Convert(v).TrimPrefix("max=").Int64()
fi.Maximum = int(n)
}
}
}

// GenerateForStruct reads the Go File and generates the ORM implementations for a given struct name.
func (o *Ormc) GenerateForStruct(structName string, goFile string) error {
info, err := o.ParseStruct(structName, goFile)
Expand All @@ -317,6 +380,64 @@ func (o *Ormc) GenerateForStruct(structName string, goFile string) error {
return o.GenerateForFile([]StructInfo{info}, goFile)
}

func capitalize(s string) string {
if s == "" {
return ""
}
return strings.ToUpper(s[0:1]) + s[1:]
}

func writePermittedFields(buf *Conv, f FieldInfo) {
// Use nested Permitted literal
hasPerm := f.Letters || f.Tilde || f.Numbers || f.Spaces ||
len(f.Extra) > 0 || f.Minimum > 0 || f.Maximum > 0

if !hasPerm {
return
}

buf.Write(", Permitted: fmt.Permitted{")
parts := []string{}
if f.Letters {
parts = append(parts, "Letters: true")
}
if f.Tilde {
parts = append(parts, "Tilde: true")
}
if f.Numbers {
parts = append(parts, "Numbers: true")
}
if f.Spaces {
parts = append(parts, "Spaces: true")
}
if f.Minimum > 0 {
parts = append(parts, Sprintf("Minimum: %d", f.Minimum))
}
if f.Maximum > 0 {
parts = append(parts, Sprintf("Maximum: %d", f.Maximum))
}
if len(f.Extra) > 0 {
buf2 := "Extra: []rune{"
for i, r := range f.Extra {
if i > 0 {
buf2 += ", "
}
buf2 += Sprintf("'%s'", string(r))
}
buf2 += "}"
parts = append(parts, buf2)
}

// Join parts
for i, p := range parts {
if i > 0 {
buf.Write(", ")
}
buf.Write(p)
}
buf.Write("}")
}

// GenerateForFile writes ORM implementations for all infos into one file.
func (o *Ormc) GenerateForFile(infos []StructInfo, sourceFile string) error {
if len(infos) == 0 {
Expand All @@ -329,10 +450,15 @@ func (o *Ormc) GenerateForFile(infos []StructInfo, sourceFile string) error {
buf.Write(Sprintf("package %s\n\n", infos[0].PackageName))

hasModel := false
hasFormat := false
for _, info := range infos {
if !info.FormOnly {
hasModel = true
break
}
for _, f := range info.Fields {
if f.Format != "" {
hasFormat = true
}
}
}

Expand All @@ -341,6 +467,9 @@ func (o *Ormc) GenerateForFile(infos []StructInfo, sourceFile string) error {
if hasModel {
buf.Write("\t\"github.com/tinywasm/orm\"\n")
}
if hasFormat {
buf.Write("\t\"github.com/tinywasm/form\"\n")
}
buf.Write(")\n\n")

for _, info := range infos {
Expand Down Expand Up @@ -386,12 +515,10 @@ func (o *Ormc) GenerateForFile(infos []StructInfo, sourceFile string) error {
if f.AutoInc {
buf.Write(", AutoInc: true")
}
if f.Input != "" {
buf.Write(Sprintf(", Input: \"%s\"", f.Input))
}
if f.JSON != "" {
buf.Write(Sprintf(", JSON: \"%s\"", f.JSON))
if f.OmitEmpty {
buf.Write(", OmitEmpty: true")
}
writePermittedFields(buf, f)
buf.Write("},\n")
}
buf.Write("\t}\n\n")
Expand All @@ -406,6 +533,29 @@ func (o *Ormc) GenerateForFile(infos []StructInfo, sourceFile string) error {
buf.Write("\t}\n")
buf.Write("}\n\n")

hasValidation := false
for _, f := range info.Fields {
if f.NotNull || f.Letters || f.Numbers || f.Tilde || f.Spaces ||
len(f.Extra) > 0 || f.Minimum > 0 || f.Maximum > 0 || f.Format != "" {
hasValidation = true
break
}
}

if hasValidation {
buf.Write(Sprintf("func (m *%s) Validate() error {\n", info.Name))
buf.Write("\tif err := fmt.ValidateFielder(m); err != nil { return err }\n")
for _, f := range info.Fields {
if f.Format != "" {
// E.g. "email" -> "ValidateEmail"
validatorName := "form.Validate" + capitalize(f.Format)
buf.Write(Sprintf("\tif err := %s(m.%s); err != nil { return err }\n", validatorName, f.Name))
}
}
buf.Write("\treturn nil\n")
buf.Write("}\n\n")
}

if !info.FormOnly {
// Metadata Descriptors
buf.Write(Sprintf("var %s_ = struct {\n", info.Name))
Expand Down Expand Up @@ -565,7 +715,7 @@ func (o *Ormc) Run() error {
// Pass 4: sync dependencies
if _, err := os.Stat(filepath.Join(o.rootDir, "go.mod")); err == nil {
o.log("Syncing dependencies...")
if err := o.exec("go", "get", "github.com/tinywasm/fmt", "github.com/tinywasm/orm"); err != nil {
if err := o.exec("go", "get", "github.com/tinywasm/fmt", "github.com/tinywasm/orm", "github.com/tinywasm/form"); err != nil {
return Err(err, "failed to get dependencies")
}
if err := o.exec("go", "mod", "tidy"); err != nil {
Expand Down
Loading