internal/pkg/dsl/tmpl.go (141 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License; // you may not use this file except in compliance with the Elastic License. package dsl import ( "bytes" "encoding/json" "errors" "fmt" "strings" "github.com/gofrs/uuid" ) const kPrefix = "TMPL." const kTokenSz = len(kPrefix) + 36 // len of uuid string var ( ErrTokenUndefined = errors.New("bound token not defined") ErrTokenNotFound = errors.New("named token not found") ErrNotResolved = errors.New("template not resolved") ) type Tmpl struct { tmap map[string]Token sseq []sliceT bcnt int } type sliceT struct { name string data []byte } func NewTmpl() *Tmpl { return &Tmpl{ tmap: make(map[string]Token), } } type Token string func newToken() Token { u := uuid.Must(uuid.NewV4()) t := fmt.Sprintf("%s%s", kPrefix, u.String()) if len(t) != kTokenSz { panic("Size misalignment") } return Token(t) } func (t *Tmpl) Bind(name string) Token { token := newToken() t.tmap[name] = token return token } func (t *Tmpl) Resolve(n *Node) error { d, err := json.Marshal(n) if err != nil { return err } var sliceSeq []sliceT // Reverse name->token map rmap := make(map[Token]string, len(t.tmap)) for name, token := range t.tmap { rmap[token] = name } // O(n) Scan d for each token; src := string(d) var sum int matches := make(map[Token]struct{}) for v := strings.Index(src, kPrefix); v != -1; v = strings.Index(src, kPrefix) { var slice sliceT if v > 0 && src[v-1] == '"' && len(src) >= v+kTokenSz+1 && src[v+kTokenSz] == '"' { token := Token(src[v : v+kTokenSz]) // Do we know about this token? if name, ok := rmap[token]; ok { matches[token] = struct{}{} slice.name = name slice.data = []byte(src[:v-1]) src = src[v+kTokenSz+1:] } } // If we did not find a match, append up to index; this allows other strings with kPrefix to work. if slice.name == "" { slice.data = []byte(src[:v]) src = src[v:] } sum += len(src) sliceSeq = append(sliceSeq, slice) } // Append slice for any remainder if len(src) > 0 { sum += len(src) sliceSeq = append(sliceSeq, sliceT{ data: []byte(src), }) } if len(matches) != len(rmap) { return ErrTokenUndefined } t.sseq = sliceSeq t.bcnt = sum return nil } func (t *Tmpl) MustResolve(n *Node) *Tmpl { err := t.Resolve(n) if err != nil { panic(err) } return t } // RenderOne is a convenience function to avoid map when only one token func (t *Tmpl) RenderOne(name string, v interface{}) ([]byte, error) { d, err := json.Marshal(v) if err != nil { return nil, err } m := map[string][]byte{name: d} return t.render(m, len(d)) } func (t *Tmpl) Render(m map[string]interface{}) ([]byte, error) { // Marshal all targets, get byte count marshalMap := make(map[string][]byte, len(m)) var sum int for name, v := range m { d, err := json.Marshal(v) if err != nil { return nil, err } sum += len(d) marshalMap[name] = d } return t.render(marshalMap, sum) } func (t *Tmpl) MustRender(m map[string]interface{}) []byte { b, err := t.Render(m) if err != nil { panic(err) } return b } func (t *Tmpl) render(m map[string][]byte, sum int) ([]byte, error) { if t.sseq == nil { return nil, ErrNotResolved } // Allocate buffer for result var buf bytes.Buffer buf.Grow(sum + t.bcnt) // O(n) Iterate through sequences, render as expected for _, s := range t.sseq { buf.Write(s.data) if s.name != "" { d, ok := m[s.name] if !ok { return nil, ErrTokenNotFound } buf.Write(d) } } return buf.Bytes(), nil }