Skip to content

Commit 71c0e05

Browse files
authored
Merge pull request #1731 from breml/generate-database-marshal-json
generate-database: Add support for marshal to JSON
2 parents c12b0f6 + eec5c50 commit 71c0e05

File tree

9 files changed

+156
-18
lines changed

9 files changed

+156
-18
lines changed

cmd/generate-database/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Tag | Description
115115
`primary=yes` | Assigns column associated with the field to be sufficient for returning a row from the table. Will default to `Name` if unspecified. Fields with this key will be included in the default 'ORDER BY' clause.
116116
`omit=<Stmt Types>` | Omits a given field from consideration for the comma separated list of statement types (`create`, `objects-by-Name`, `update`).
117117
`ignore=yes` | Outright ignore the struct field as though it does not exist.
118-
`marshal=yes` | Marshal/Unmarshal data into the field. The column must be a TEXT column and the type must implement both `Marshal` and `Unmarshal`. This works for entity tables only, and not for association or mapping tables.
118+
`marshal=<yes/json>` | Marshal/Unmarshal data into the field. The column must be a TEXT column. If `marshal=yes`, then the type must implement both `Marshal` and `Unmarshal`. If `marshal=json`, the type is marshaled to JSON using the standard library ([json.Marshal](https://pkg.go.dev/encoding/json#Marshal)). This works for entity tables only, and not for association or mapping tables.
119119

120120
### Go Function Generation
121121

cmd/generate-database/db/lex.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,12 @@ func destFunc(slice string, typ string, fields []*Field) string {
9797
}
9898

9999
unmarshal := func(declVarName string, field *Field) {
100-
writeLine(fmt.Sprintf("err = unmarshal(%s, &%s.%s)", declVarName, varName, field.Name))
100+
unmarshalFunc := "unmarshal"
101+
if field.Config.Get("marshal") == "json" {
102+
unmarshalFunc = "unmarshalJSON"
103+
}
104+
105+
writeLine(fmt.Sprintf("err = %s(%s, &%s.%s)", unmarshalFunc, declVarName, varName, field.Name))
101106
checkErr()
102107
}
103108

@@ -106,7 +111,7 @@ func destFunc(slice string, typ string, fields []*Field) string {
106111
declVarNames := make([]string, 0, len(fields))
107112
for i, field := range fields {
108113
var arg string
109-
if util.IsTrue(field.Config.Get("marshal")) {
114+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
110115
declVarName := fmt.Sprintf("%sStr", lex.Minuscule(field.Name))
111116
declVarNames = append(declVarNames, declVarName)
112117
declVars[declVarName] = field

cmd/generate-database/db/mapping.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ func (m *Mapping) FieldParamsMarshal(fields []*Field) string {
240240
name = lex.Minuscule(m.Name) + field.Name
241241
}
242242

243-
if util.IsTrue(field.Config.Get("marshal")) {
243+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
244244
name = fmt.Sprintf("marshaled%s", field.Name)
245245
}
246246

cmd/generate-database/db/method.go

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,13 @@ func (m *Method) getMany(buf *file.Buffer) error {
231231
var args string
232232
for _, name := range filter {
233233
for _, field := range mapping.Fields {
234-
if name == field.Name && util.IsTrue(field.Config.Get("marshal")) {
235-
buf.L("marshaledFilter%s, err := marshal(filter.%s)", name, name)
234+
if name == field.Name && util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
235+
marshalFunc := "marshal"
236+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
237+
marshalFunc = "marshalJSON"
238+
}
239+
240+
buf.L("marshaledFilter%s, err := %s(filter.%s)", name, marshalFunc, name)
236241
m.ifErrNotNil(buf, true, "nil", "err")
237242
args += fmt.Sprintf("marshaledFilter%s,", name)
238243
} else if name == field.Name {
@@ -563,8 +568,13 @@ func (m *Method) id(buf *file.Buffer) error {
563568
m.ifErrNotNil(buf, true, "-1", fmt.Sprintf(`fmt.Errorf("Failed to get \"%s\" prepared statement: %%w", err)`, stmtCodeVar(m.entity, "ID")))
564569

565570
for _, field := range nk {
566-
if util.IsTrue(field.Config.Get("marshal")) {
567-
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
571+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
572+
marshalFunc := "marshal"
573+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
574+
marshalFunc = "marshalJSON"
575+
}
576+
577+
buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
568578
m.ifErrNotNil(buf, true, "-1", "err")
569579
}
570580
}
@@ -608,8 +618,13 @@ func (m *Method) exists(buf *file.Buffer) error {
608618
m.ifErrNotNil(buf, true, "false", fmt.Sprintf(`fmt.Errorf("Failed to get \"%s\" prepared statement: %%w", err)`, stmtCodeVar(m.entity, "ID")))
609619

610620
for _, field := range nk {
611-
if util.IsTrue(field.Config.Get("marshal")) {
612-
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
621+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
622+
marshalFunc := "marshal"
623+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
624+
marshalFunc = "marshalJSON"
625+
}
626+
627+
buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
613628
m.ifErrNotNil(buf, true, "false", "err")
614629
}
615630
}
@@ -726,8 +741,13 @@ func (m *Method) create(buf *file.Buffer, replace bool) error {
726741

727742
buf.L("// Populate the statement arguments. ")
728743
for i, field := range fields {
729-
if util.IsTrue(field.Config.Get("marshal")) {
730-
buf.L("marshaled%s, err := marshal(object.%s)", field.Name, field.Name)
744+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
745+
marshalFunc := "marshal"
746+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
747+
marshalFunc = "marshalJSON"
748+
}
749+
750+
buf.L("marshaled%s, err := %s(object.%s)", field.Name, marshalFunc, field.Name)
731751
m.ifErrNotNil(buf, true, "-1", "err")
732752
buf.L("args[%d] = marshaled%s", i, field.Name)
733753
} else {
@@ -880,8 +900,13 @@ func (m *Method) rename(buf *file.Buffer) error {
880900
m.ifErrNotNil(buf, true, fmt.Sprintf(`fmt.Errorf("Failed to get \"%s\" prepared statement: %%w", err)`, stmtCodeVar(m.entity, "rename")))
881901

882902
for _, field := range nk {
883-
if util.IsTrue(field.Config.Get("marshal")) {
884-
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
903+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
904+
marshalFunc := "marshal"
905+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
906+
marshalFunc = "marshalJSON"
907+
}
908+
909+
buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
885910
m.ifErrNotNil(buf, true, "err")
886911
}
887912
}
@@ -999,8 +1024,13 @@ func (m *Method) update(buf *file.Buffer) error {
9991024
fields := updateMapping.ColumnFields("ID") // This exclude the ID column, which is autogenerated.
10001025
params := make([]string, len(fields))
10011026
for i, field := range fields {
1002-
if util.IsTrue(field.Config.Get("marshal")) {
1003-
buf.L("marshaled%s, err := marshal(object.%s)", field.Name, field.Name)
1027+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
1028+
marshalFunc := "marshal"
1029+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
1030+
marshalFunc = "marshalJSON"
1031+
}
1032+
1033+
buf.L("marshaled%s, err := %s(object.%s)", field.Name, marshalFunc, field.Name)
10041034
m.ifErrNotNil(buf, true, "err")
10051035
params[i] = fmt.Sprintf("marshaled%s", field.Name)
10061036
} else {
@@ -1111,8 +1141,13 @@ func (m *Method) delete(buf *file.Buffer, deleteOne bool) error {
11111141
buf.L("stmt, err := Stmt(db, %s)", stmtCodeVar(m.entity, "delete", FieldNames(activeFilters)...))
11121142

11131143
for _, field := range activeFilters {
1114-
if util.IsTrue(field.Config.Get("marshal")) {
1115-
buf.L("marshaled%s, err := marshal(%s)", field.Name, lex.Minuscule(field.Name))
1144+
if util.IsNeitherFalseNorEmpty(field.Config.Get("marshal")) {
1145+
marshalFunc := "marshal"
1146+
if strings.ToLower(field.Config.Get("marshal")) == "json" {
1147+
marshalFunc = "marshalJSON"
1148+
}
1149+
1150+
buf.L("marshaled%s, err := %s(%s)", field.Name, marshalFunc, lex.Minuscule(field.Name))
11161151
m.ifErrNotNil(buf, true, "err")
11171152
}
11181153
}

cmd/generate-database/db/parse.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strings"
1515

1616
"github.com/lxc/incus/v6/cmd/generate-database/lex"
17+
"github.com/lxc/incus/v6/shared/util"
1718
)
1819

1920
// FiltersFromStmt parses all filtering statement defined for the given entity. It
@@ -374,6 +375,11 @@ func parseField(f *types.Var, structTag string, kind string, pkgName string) (*F
374375
if err != nil {
375376
return nil, fmt.Errorf("Parse 'db' structure tag: %w", err)
376377
}
378+
379+
err = validateFieldConfig(config)
380+
if err != nil {
381+
return nil, fmt.Errorf("Invalid struct tag for field %q: %v", name, err)
382+
}
377383
}
378384

379385
// Ignore fields that are marked with `db:"omit"`.
@@ -440,3 +446,58 @@ func parseType(x types.Type, pkgName string) string {
440446
return ""
441447
}
442448
}
449+
450+
func validateFieldConfig(config url.Values) error {
451+
for tag, values := range config {
452+
switch tag {
453+
case
454+
"sql",
455+
"coalesce",
456+
"join",
457+
"leftjoin",
458+
"joinon",
459+
"omit":
460+
461+
_, err := exactlyOneValue(tag, values)
462+
return err
463+
464+
case
465+
"order",
466+
"primary",
467+
"ignore":
468+
469+
value, err := exactlyOneValue(tag, values)
470+
if err != nil {
471+
return err
472+
}
473+
474+
if !util.IsTrue(value) && !util.IsFalse(value) {
475+
return fmt.Errorf("Unexpected value %q for %q tag", value, tag)
476+
}
477+
478+
case "marshal":
479+
value, err := exactlyOneValue(tag, values)
480+
if err != nil {
481+
return err
482+
}
483+
484+
if !util.IsTrue(value) && !util.IsFalse(value) && strings.ToLower(value) != "json" {
485+
return fmt.Errorf("Unexpected value %q for %q tag", value, tag)
486+
}
487+
}
488+
}
489+
490+
return nil
491+
}
492+
493+
func exactlyOneValue(tag string, values []string) (string, error) {
494+
if len(values) == 0 {
495+
return "", fmt.Errorf("Missing value for %q tag", tag)
496+
}
497+
498+
if len(values) > 1 {
499+
return "", fmt.Errorf("More than one value for %q tag", tag)
500+
}
501+
502+
return values[0], nil
503+
}

cmd/generate-database/file/boilerplate/boilerplate.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package boilerplate
33
import (
44
"context"
55
"database/sql"
6+
"encoding/json"
67
"errors"
78
"fmt"
89
)
@@ -127,6 +128,19 @@ func unmarshal(data string, v any) error {
127128
return unmarshaler.UnmarshalDB(data)
128129
}
129130

131+
func marshalJSON(v any) (string, error) {
132+
marshalled, err := json.Marshal(v)
133+
if err != nil {
134+
return "", err
135+
}
136+
137+
return string(marshalled), nil
138+
}
139+
140+
func unmarshalJSON(data string, v any) error {
141+
return json.Unmarshal([]byte(data), v)
142+
}
143+
130144
// dest is a function that is expected to return the objects to pass to the
131145
// 'dest' argument of sql.Rows.Scan(). It is invoked by SelectObjects once per
132146
// yielded row, and it will be passed the index of the row being scanned.

cmd/generate-database/file/boilerplate/boilerplate_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ func Test(t *testing.T) {
1010
_ = defaultMapErr
1111
_ = marshal
1212
_ = unmarshal
13+
_ = marshalJSON
14+
_ = unmarshalJSON
1315
_ = selectObjects
1416
_ = scan
1517
}

internal/server/db/cluster/mapper_boilerplate.go

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

shared/util/boolean.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ func IsTrue(value string) bool {
1010
return slices.Contains([]string{"true", "1", "yes", "on"}, strings.ToLower(value))
1111
}
1212

13+
// IsNeitherFalseNorEmpty is true, if value is neither false nor empty,
14+
// which is the contrary to IsFalseOrEmpty.
15+
func IsNeitherFalseNorEmpty(value string) bool {
16+
isFalseOrEmtpy := IsFalseOrEmpty(value)
17+
return !isFalseOrEmtpy
18+
}
19+
1320
// IsTrueOrEmpty returns true if value is empty or if IsTrue() returns true.
1421
func IsTrueOrEmpty(value string) bool {
1522
return value == "" || IsTrue(value)

0 commit comments

Comments
 (0)