| // Copyright 2019 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package lucicfg |
| |
| import ( |
| "bytes" |
| "crypto/sha256" |
| "fmt" |
| "hash/fnv" |
| "text/template" |
| |
| "go.starlark.net/starlark" |
| |
| "go.chromium.org/luci/starlark/builtins" |
| ) |
| |
| type templateValue struct { |
| tmpl *template.Template |
| hash uint32 |
| } |
| |
| // String returns the string representation of the value. |
| func (t *templateValue) String() string { return "template(...)" } |
| |
| // Type returns a short string describing the value's type. |
| func (t *templateValue) Type() string { return "template" } |
| |
| // Freeze does nothing since templateValue is already immutable. |
| func (t *templateValue) Freeze() {} |
| |
| // Truth returns the truth value of an object. |
| func (t *templateValue) Truth() starlark.Bool { return starlark.True } |
| |
| // Hash returns a function of x such that Equals(x, y) => Hash(x) == Hash(y). |
| func (t *templateValue) Hash() (uint32, error) { return t.hash, nil } |
| |
| // AttrNames returns all .<attr> of this object. |
| func (t *templateValue) AttrNames() []string { |
| return []string{"render"} |
| } |
| |
| // Attr returns a .<name> attribute of this object or nil. |
| func (t *templateValue) Attr(name string) (starlark.Value, error) { |
| switch name { |
| case "render": |
| return templateRenderBuiltin.BindReceiver(t), nil |
| default: |
| return nil, nil |
| } |
| } |
| |
| // render implements template rendering using given value as input. |
| func (t *templateValue) render(data interface{}) (string, error) { |
| buf := bytes.Buffer{} |
| if err := t.tmpl.Execute(&buf, data); err != nil { |
| return "", err |
| } |
| return buf.String(), nil |
| } |
| |
| // Implementation of template.render(**dict) builtin. |
| var templateRenderBuiltin = starlark.NewBuiltin("render", func(_ *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { |
| if len(args) != 0 { |
| return nil, fmt.Errorf("render: expecting only keyword arguments, got %d positional", len(args)) |
| } |
| |
| // Convert kwargs to a real dict and then to a go nested map reusing to_json |
| // machinery. |
| data := starlark.NewDict(len(kwargs)) |
| for _, tup := range kwargs { |
| if len(tup) != 2 { |
| panic(fmt.Sprintf("impossible kwarg with len %d", len(tup))) |
| } |
| if err := data.SetKey(tup[0], tup[1]); err != nil { |
| panic(fmt.Sprintf("impossible bad kwarg %s %s", tup[0], tup[1])) |
| } |
| } |
| obj, err := builtins.ToGoNative(data) |
| if err != nil { |
| return nil, fmt.Errorf("render: %s", err) |
| } |
| |
| out, err := fn.Receiver().(*templateValue).render(obj) |
| if err != nil { |
| return nil, fmt.Errorf("render: %s", err) |
| } |
| return starlark.String(out), nil |
| }) |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| type templateCache struct { |
| cache map[string]*templateValue // SHA256 of body => parsed template |
| } |
| |
| func (tc *templateCache) get(body string) (starlark.Value, error) { |
| blob := []byte(body) |
| hash := sha256.Sum256(blob) |
| cacheKey := string(hash[:]) |
| if t, ok := tc.cache[cacheKey]; ok { |
| return t, nil |
| } |
| |
| tmpl, err := template.New("<str>").Parse(body) |
| if err != nil { |
| return nil, err // note: the error is already prefixed by "template: ..." |
| } |
| val := &templateValue{tmpl: tmpl} |
| |
| fh := fnv.New32a() |
| fh.Write(blob) |
| val.hash = fh.Sum32() |
| |
| if tc.cache == nil { |
| tc.cache = make(map[string]*templateValue, 1) |
| } |
| tc.cache[cacheKey] = val |
| return val, nil |
| } |
| |
| // See //internal/strutil.star for where this is used. |
| func init() { |
| declNative("template", func(call nativeCall) (starlark.Value, error) { |
| var body starlark.String |
| if err := call.unpack(1, &body); err != nil { |
| return nil, err |
| } |
| return call.State.templates.get(body.GoString()) |
| }) |
| } |