go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/data/text/templateproto/render.go (about)

     1  // Copyright 2016 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 templateproto
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"reflect"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"go.chromium.org/luci/common/data/stringset"
    26  )
    27  
    28  // MustNewValue creates a new *Value wrapping v, and panics if v is a bad type
    29  func MustNewValue(v any) *Value {
    30  	ret, err := NewValue(v)
    31  	if err != nil {
    32  		panic(err)
    33  	}
    34  	return ret
    35  }
    36  
    37  // NewValue creates a new *Value wrapping v.
    38  //
    39  // Allowed types are:
    40  //   - Any of the explicit *Value_Int - style types
    41  //   - nil -> Null
    42  //   - string -> String
    43  //   - []byte -> Bytes
    44  //   - int, int8, int16, int32, int64 -> Integer
    45  //   - uint, uint8, uint16, uint32, uint64 -> Unsigned
    46  //   - float32, float64 -> Float
    47  //   - bool -> Boolean
    48  //   - map[string]any -> Object
    49  //   - []any -> Array
    50  func NewValue(v any) (*Value, error) {
    51  	switch x := v.(type) {
    52  	case isValue_Value:
    53  		return &Value{Value: x}, nil
    54  	case nil:
    55  		return &Value{Value: &Value_Null{}}, nil
    56  	case int8, int16, int32, int64, int:
    57  		return &Value{Value: &Value_Int{reflect.ValueOf(v).Int()}}, nil
    58  	case uint8, uint16, uint32, uint64, uint:
    59  		return &Value{Value: &Value_Uint{reflect.ValueOf(v).Uint()}}, nil
    60  	case float32, float64:
    61  		return &Value{Value: &Value_Float{reflect.ValueOf(v).Float()}}, nil
    62  	case string:
    63  		return &Value{Value: &Value_Str{x}}, nil
    64  	case []byte:
    65  		return &Value{Value: &Value_Bytes{x}}, nil
    66  	case bool:
    67  		return &Value{Value: &Value_Bool{x}}, nil
    68  	case map[string]any:
    69  		ret, err := json.Marshal(x)
    70  		if err != nil {
    71  			return nil, err
    72  		}
    73  		return &Value{Value: &Value_Object{string(ret)}}, nil
    74  	case []any:
    75  		ret, err := json.Marshal(x)
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  		return &Value{Value: &Value_Array{string(ret)}}, nil
    80  	}
    81  	return nil, fmt.Errorf("unknown type %T", v)
    82  }
    83  
    84  // LiteralMap is a type for literal in-line param substitutions, or when you
    85  // know statically that the params correspond to correct Value types.
    86  type LiteralMap map[string]any
    87  
    88  // Convert converts this to a parameter map that can be used with
    89  // Template.Render.
    90  func (m LiteralMap) Convert() (map[string]*Value, error) {
    91  	ret := make(map[string]*Value, len(m))
    92  	for k, v := range m {
    93  		v, err := NewValue(v)
    94  		if err != nil {
    95  			return nil, fmt.Errorf("key %q: %s", k, err)
    96  		}
    97  		ret[k] = v
    98  	}
    99  	return ret, nil
   100  }
   101  
   102  // RenderL renders this template with a LiteralMap, calling its Convert method
   103  // and passing the result to Render.
   104  func (t *File_Template) RenderL(m LiteralMap) (string, error) {
   105  	pm, err := m.Convert()
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  	return t.Render(pm)
   110  }
   111  
   112  // Render turns the Template into a JSON document, filled with the given
   113  // parameters. It does not validate that the output is valid JSON, but if you
   114  // called Normalize on this Template already, then it WILL be valid JSON.
   115  func (t *File_Template) Render(params map[string]*Value) (string, error) {
   116  	sSet := stringset.New(len(params))
   117  	replacementSlice := make([]string, 0, len(t.Param)*2)
   118  	for k, param := range t.Param {
   119  		replacementSlice = append(replacementSlice, k)
   120  		if newVal, ok := params[k]; ok {
   121  			if err := param.Accepts(newVal); err != nil {
   122  				return "", fmt.Errorf("param %q: %s", k, err)
   123  			}
   124  			sSet.Add(k)
   125  			replacementSlice = append(replacementSlice, newVal.JSONRender())
   126  		} else if param.Default != nil {
   127  			replacementSlice = append(replacementSlice, param.Default.JSONRender())
   128  		} else {
   129  			return "", fmt.Errorf("param %q: missing", k)
   130  		}
   131  	}
   132  	if len(params) != sSet.Len() {
   133  		unknown := make([]string, 0, len(params))
   134  		for k := range params {
   135  			if !sSet.Has(k) {
   136  				unknown = append(unknown, k)
   137  			}
   138  		}
   139  		sort.Strings(unknown)
   140  		return "", fmt.Errorf("unknown parameters: %q", unknown)
   141  	}
   142  
   143  	r := strings.NewReplacer(replacementSlice...)
   144  
   145  	return r.Replace(t.Body), nil
   146  }
   147  
   148  func (v *Value) schemaType() isSchema_Schema {
   149  	switch v.Value.(type) {
   150  	case *Value_Str:
   151  		return (*Schema_Str)(nil)
   152  	case *Value_Bytes:
   153  		return (*Schema_Bytes)(nil)
   154  	case *Value_Int:
   155  		return (*Schema_Int)(nil)
   156  	case *Value_Uint:
   157  		return (*Schema_Uint)(nil)
   158  	case *Value_Float:
   159  		return (*Schema_Float)(nil)
   160  	case *Value_Bool:
   161  		return (*Schema_Bool)(nil)
   162  	case *Value_Object:
   163  		return (*Schema_Object)(nil)
   164  	case *Value_Array:
   165  		return (*Schema_Array)(nil)
   166  	}
   167  	panic(fmt.Errorf("unknown type %T", v.Value))
   168  }
   169  
   170  func schemaTypeStr(s isSchema_Schema) string {
   171  	switch s.(type) {
   172  	case *Schema_Str:
   173  		return "str"
   174  	case *Schema_Bytes:
   175  		return "bytes"
   176  	case *Schema_Int:
   177  		return "int"
   178  	case *Schema_Uint:
   179  		return "uint"
   180  	case *Schema_Float:
   181  		return "float"
   182  	case *Schema_Bool:
   183  		return "bool"
   184  	case *Schema_Enum:
   185  		return "enum"
   186  	case *Schema_Object:
   187  		return "object"
   188  	case *Schema_Array:
   189  		return "array"
   190  	}
   191  	panic(fmt.Errorf("unknown type %T", s))
   192  }
   193  
   194  // JSONRender returns the to-be-injected string rendering of v.
   195  func (v *Value) JSONRender() string {
   196  	return v.Value.(interface {
   197  		JSONRender() string
   198  	}).JSONRender()
   199  }
   200  
   201  // JSONRender returns a rendering of this string as JSON, e.g. go value "foo"
   202  // renders as `"foo"`.
   203  func (v *Value_Str) JSONRender() string {
   204  	ret, err := json.Marshal(v.Str)
   205  	if err != nil {
   206  		panic(err)
   207  	}
   208  	return string(ret)
   209  }
   210  
   211  // JSONRender returns a rendering of these bytes as JSON, e.g. go value
   212  // []byte("foo") renders as `"Zm9v"`.
   213  func (v *Value_Bytes) JSONRender() string {
   214  	ret, err := json.Marshal(v.Bytes)
   215  	if err != nil {
   216  		panic(err)
   217  	}
   218  	return string(ret)
   219  }
   220  
   221  // JSONRender returns a rendering of this int as JSON, e.g. go value 100
   222  // renders as `100`. If the absolute value is > 2**53, this will render it as
   223  // a string.
   224  //
   225  // Integers render as strings to avoid encoding issues in JSON, which only
   226  // supports double-precision floating point numbers.
   227  func (v *Value_Int) JSONRender() string {
   228  	num := strconv.FormatInt(v.Int, 10)
   229  	abs := v.Int
   230  	if abs < 0 {
   231  		abs = -abs
   232  	}
   233  	if abs < (1 << 53) {
   234  		return num
   235  	}
   236  	return fmt.Sprintf(`"%s"`, num)
   237  }
   238  
   239  // JSONRender returns a rendering of this uint as JSON, e.g. go value 100
   240  // renders as `"100"`.
   241  //
   242  // Unsigns render as strings to avoid encoding issues in JSON, which only
   243  // supports double-precision floating point numbers.
   244  func (v *Value_Uint) JSONRender() string {
   245  	num := strconv.FormatUint(v.Uint, 10)
   246  	if v.Uint < (1 << 53) {
   247  		return num
   248  	}
   249  	return fmt.Sprintf(`"%s"`, num)
   250  }
   251  
   252  // JSONRender returns a rendering of this float as JSON, e.g. go value 1.23
   253  // renders as `1.23`.
   254  func (v *Value_Float) JSONRender() string {
   255  	ret, err := json.Marshal(v.Float)
   256  	if err != nil {
   257  		panic(err)
   258  	}
   259  	return string(ret)
   260  }
   261  
   262  // JSONRender returns a rendering of this bool as JSON, e.g. go value true
   263  // renders as `true`.
   264  func (v *Value_Bool) JSONRender() string {
   265  	if v.Bool {
   266  		return "true"
   267  	}
   268  	return "false"
   269  }
   270  
   271  // JSONRender returns a rendering of this JSON object as JSON. This is a direct
   272  // return of the JSON encoded string; no validation is done. To check that the
   273  // contained string is valid, use the Valid() method.
   274  func (v *Value_Object) JSONRender() string {
   275  	return v.Object
   276  }
   277  
   278  // JSONRender returns a rendering of this JSON array as JSON. This is a direct
   279  // return of the JSON encoded string; no validation is done. To check that the
   280  // contained string is valid, use the Valid() method.
   281  func (v *Value_Array) JSONRender() string {
   282  	return v.Array
   283  }
   284  
   285  // JSONRender returns a rendering of null. This always returns `null`.
   286  func (v *Value_Null) JSONRender() string {
   287  	return "null"
   288  }