go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/lucicfg/templates.go (about) 1 // Copyright 2019 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package lucicfg 16 17 import ( 18 "bytes" 19 "crypto/sha256" 20 "fmt" 21 "hash/fnv" 22 "text/template" 23 24 "go.starlark.net/starlark" 25 26 "go.chromium.org/luci/starlark/builtins" 27 ) 28 29 type templateValue struct { 30 tmpl *template.Template 31 hash uint32 32 } 33 34 // String returns the string representation of the value. 35 func (t *templateValue) String() string { return "template(...)" } 36 37 // Type returns a short string describing the value's type. 38 func (t *templateValue) Type() string { return "template" } 39 40 // Freeze does nothing since templateValue is already immutable. 41 func (t *templateValue) Freeze() {} 42 43 // Truth returns the truth value of an object. 44 func (t *templateValue) Truth() starlark.Bool { return starlark.True } 45 46 // Hash returns a function of x such that Equals(x, y) => Hash(x) == Hash(y). 47 func (t *templateValue) Hash() (uint32, error) { return t.hash, nil } 48 49 // AttrNames returns all .<attr> of this object. 50 func (t *templateValue) AttrNames() []string { 51 return []string{"render"} 52 } 53 54 // Attr returns a .<name> attribute of this object or nil. 55 func (t *templateValue) Attr(name string) (starlark.Value, error) { 56 switch name { 57 case "render": 58 return templateRenderBuiltin.BindReceiver(t), nil 59 default: 60 return nil, nil 61 } 62 } 63 64 // render implements template rendering using given value as input. 65 func (t *templateValue) render(data any) (string, error) { 66 buf := bytes.Buffer{} 67 if err := t.tmpl.Execute(&buf, data); err != nil { 68 return "", err 69 } 70 return buf.String(), nil 71 } 72 73 // Implementation of template.render(**dict) builtin. 74 var templateRenderBuiltin = starlark.NewBuiltin("render", func(_ *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 75 if len(args) != 0 { 76 return nil, fmt.Errorf("render: expecting only keyword arguments, got %d positional", len(args)) 77 } 78 79 // Convert kwargs to a real dict and then to a go nested map reusing to_json 80 // machinery. 81 data := starlark.NewDict(len(kwargs)) 82 for _, tup := range kwargs { 83 if len(tup) != 2 { 84 panic(fmt.Sprintf("impossible kwarg with len %d", len(tup))) 85 } 86 if err := data.SetKey(tup[0], tup[1]); err != nil { 87 panic(fmt.Sprintf("impossible bad kwarg %s %s", tup[0], tup[1])) 88 } 89 } 90 obj, err := builtins.ToGoNative(data) 91 if err != nil { 92 return nil, fmt.Errorf("render: %s", err) 93 } 94 95 out, err := fn.Receiver().(*templateValue).render(obj) 96 if err != nil { 97 return nil, fmt.Errorf("render: %s", err) 98 } 99 return starlark.String(out), nil 100 }) 101 102 //////////////////////////////////////////////////////////////////////////////// 103 104 type templateCache struct { 105 cache map[string]*templateValue // SHA256 of body => parsed template 106 } 107 108 func (tc *templateCache) get(body string) (starlark.Value, error) { 109 blob := []byte(body) 110 hash := sha256.Sum256(blob) 111 cacheKey := string(hash[:]) 112 if t, ok := tc.cache[cacheKey]; ok { 113 return t, nil 114 } 115 116 tmpl, err := template.New("<str>").Parse(body) 117 if err != nil { 118 return nil, err // note: the error is already prefixed by "template: ..." 119 } 120 val := &templateValue{tmpl: tmpl} 121 122 fh := fnv.New32a() 123 fh.Write(blob) 124 val.hash = fh.Sum32() 125 126 if tc.cache == nil { 127 tc.cache = make(map[string]*templateValue, 1) 128 } 129 tc.cache[cacheKey] = val 130 return val, nil 131 } 132 133 // See //internal/strutil.star for where this is used. 134 func init() { 135 declNative("template", func(call nativeCall) (starlark.Value, error) { 136 var body starlark.String 137 if err := call.unpack(1, &body); err != nil { 138 return nil, err 139 } 140 return call.State.templates.get(body.GoString()) 141 }) 142 }