Skip to content

Commit

Permalink
Merge pull request #5 from go-andiamo/tokens
Browse files Browse the repository at this point in the history
Add TokenOption
  • Loading branch information
marrow16 committed Jul 15, 2023
2 parents b07828e + f9f9c99 commit 418bf44
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 51 deletions.
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,63 @@ if err != nil {
} else {
fmt.Printf("%#v", args) // prints: []interface {}{"some name", "unknown", time.Date{...}}
}
```
```

### Tokens
Sometimes you may have a common lexicon of table names, columns and/or arg names defined as consts.
These can be used in templates using token replace notation (`{{token}}`) in the template string and transposed by providing a `sqlnt.TokenOption` implementation...
```go
package main

import (
"database/sql"
"fmt"
"github.com/go-andiamo/sqlnt"
"time"
)

func main() {
insertQueue := sqlnt.MustCreateNamedTemplate(InsertStatement, &Lexicon{TableNameQueues}).
DefaultValue(ParamNameCreatedAt, nowFunc).DefaultValue(ParamNameStatus, "unknown")
insertTopic := sqlnt.MustCreateNamedTemplate(InsertStatement, &Lexicon{TableNameTopics}).
DefaultValue(ParamNameCreatedAt, nowFunc).DefaultValue(ParamNameStatus, "unknown")

statement, args := insertQueue.MustStatementAndArgs(sql.Named(ParamNameName, "foo"))
fmt.Printf("statement: %s\n args: %#v\n", statement, args)

statement, args = insertTopic.MustStatementAndArgs(sql.Named(ParamNameName, "foo"))
fmt.Printf("statement: %s\n args: %#v\n", statement, args)
}

const (
InsertStatement = "INSERT INTO {{table}} ({{baseCols}}) VALUES ({{insertArgs}})"
TableNameQueues = "queues"
TableNameTopics = "topics"
BaseCols = "name,status,created_at"
ParamNameName = "name"
ParamNameStatus = "status"
ParamNameCreatedAt = "createdAt"
)

var nowFunc = func(name string) any {
return time.Now()
}

var commonLexiconMap = map[string]string{
"baseCols": BaseCols,
"insertArgs": ":" + ParamNameName + ",:" + ParamNameStatus + ",:" + ParamNameCreatedAt,
}

type Lexicon struct {
TableName string
}

// Replace implements sqlnt.TokenOption.Replace
func (l *Lexicon) Replace(token string) (string, bool) {
if token == "table" {
return l.TableName, true
}
r, ok := commonLexiconMap[token]
return r, ok
}
```
30 changes: 28 additions & 2 deletions build_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ package sqlnt

import (
"fmt"
"regexp"
"strconv"
"strings"
)

func (n *namedTemplate) buildArgs() error {
if err := n.replaceTokens(); err != nil {
return err
}
var builder strings.Builder
n.argsCount = 0
lastPos := 0
runes := []rune(n.originalStatement)
rlen := len(runes)
purge := func(pos int) {
if lastPos != -1 && pos > lastPos {
if pos > lastPos {
builder.WriteString(string(runes[lastPos:pos]))
}
}
Expand Down Expand Up @@ -59,8 +63,30 @@ func (n *namedTemplate) buildArgs() error {
return nil
}

var tokenRegexp = regexp.MustCompile(`\{\{([^}]*)}}`)

func (n *namedTemplate) replaceTokens() error {
errs := make([]string, 0)
n.originalStatement = tokenRegexp.ReplaceAllStringFunc(n.originalStatement, func(s string) string {
token := s[2 : len(s)-2]
for _, tr := range n.tokenOptions {
if r, ok := tr.Replace(token); ok {
return r
}
}
errs = append(errs, token)
return ""
})
if len(errs) == 1 {
return fmt.Errorf("unknown token: %s", errs[0])
} else if len(errs) > 0 {
return fmt.Errorf("unknown tokens: %s", strings.Join(errs, ", "))
}
return nil
}

func isNameRune(r rune) bool {
return r == '_' || r == '-' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
return r == '_' || r == '-' || r == '.' || (r >= '0' && r <= '9') || (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')
}

func (n *namedTemplate) addNamedArg(name string, omissible bool) string {
Expand Down
9 changes: 9 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,14 @@ Example:
"b": "b value",
}
_, _ = db.Exec(tmp.Statement(), tmp.MustArgs(args)...)
Or directly using template:
var tmp = sqlnt.MustCreateNamedTemplate(`INSERT INTO table (col_a, col_b) VALUES(:a, :b)`, nil)
args := map[string]any{
"a": "a value",
"b": "b value",
}
_, _ = tmp.Exec(db, args)
*/
package sqlnt
50 changes: 38 additions & 12 deletions named_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,39 +90,65 @@ type namedTemplate struct {
argsCount int
usePositionalTags bool
argTag string
tokenOptions []TokenOption
}

// NewNamedTemplate creates a new NamedTemplate
//
// Returns an error if the supplied template cannot be parsed for arg names
func NewNamedTemplate(statement string, option Option) (NamedTemplate, error) {
if option == nil {
option = DefaultsOption
// # Returns an error if the supplied template cannot be parsed for arg names
//
// Multiple options can be specified - each must be either a sqlnt.Option or sqlnt.TokenOption
func NewNamedTemplate(statement string, options ...any) (NamedTemplate, error) {
opt, tokenOptions, err := getOptions(options...)
if err != nil {
return nil, err
}
result := newNamedTemplate(statement, option.UsePositionalTags(), option.ArgTag())
if err := result.buildArgs(); err != nil {
result := newNamedTemplate(statement, opt.UsePositionalTags(), opt.ArgTag(), tokenOptions)
if err = result.buildArgs(); err != nil {
return nil, err
}
return result, nil
}

func getOptions(options ...any) (Option, []TokenOption, error) {
opt := DefaultsOption
tokenOptions := make([]TokenOption, 0)
for _, o := range options {
if o != nil {
o1, ok1 := o.(Option)
o2, ok2 := o.(TokenOption)
if !ok1 && !ok2 {
return nil, nil, errors.New("invalid option")
}
if ok1 {
opt = o1
}
if ok2 {
tokenOptions = append(tokenOptions, o2)
}
}
}
return opt, tokenOptions, nil
}

// MustCreateNamedTemplate creates a new NamedTemplate
//
// is the same as NewNamedTemplate, except panics in case of error
func MustCreateNamedTemplate(statement string, option Option) NamedTemplate {
nt, err := NewNamedTemplate(statement, option)
func MustCreateNamedTemplate(statement string, options ...any) NamedTemplate {
nt, err := NewNamedTemplate(statement, options...)
if err != nil {
panic(err)
}
return nt
}

func newNamedTemplate(statement string, usePositionalTags bool, argTag string) *namedTemplate {
func newNamedTemplate(statement string, usePositionalTags bool, argTag string, tokenOptions []TokenOption) *namedTemplate {
return &namedTemplate{
originalStatement: statement,
args: map[string]*namedArg{},
usePositionalTags: usePositionalTags,
argTag: argTag,
tokenOptions: tokenOptions,
}
}

Expand Down Expand Up @@ -323,7 +349,7 @@ func (n *namedTemplate) Clone(option Option) NamedTemplate {
// no material change, just copy everything...
return n.copy()
} else {
r := newNamedTemplate(n.originalStatement, option.UsePositionalTags(), option.ArgTag())
r := newNamedTemplate(n.originalStatement, option.UsePositionalTags(), option.ArgTag(), n.tokenOptions)
_ = r.buildArgs()
for name, arg := range n.args {
if rarg, ok := r.args[name]; ok {
Expand All @@ -339,7 +365,7 @@ func (n *namedTemplate) Clone(option Option) NamedTemplate {
//
// Returns an error if the supplied statement portion cannot be parsed for arg names
func (n *namedTemplate) Append(portion string) (NamedTemplate, error) {
result := newNamedTemplate(n.originalStatement+portion, n.usePositionalTags, n.argTag)
result := newNamedTemplate(n.originalStatement+portion, n.usePositionalTags, n.argTag, n.tokenOptions)
if err := result.buildArgs(); err != nil {
return nil, err
}
Expand All @@ -362,7 +388,7 @@ func (n *namedTemplate) MustAppend(portion string) NamedTemplate {
}

func (n *namedTemplate) copy() *namedTemplate {
r := newNamedTemplate(n.originalStatement, n.usePositionalTags, n.argTag)
r := newNamedTemplate(n.originalStatement, n.usePositionalTags, n.argTag, n.tokenOptions)
r.statement = n.statement
r.argsCount = n.argsCount
r.usePositionalTags = n.usePositionalTags
Expand Down
Loading

0 comments on commit 418bf44

Please sign in to comment.