blob: 1c45228e5dca551030f4fba74cc2d783a8df8598 [file] [log] [blame]
// Copyright 2016 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 templateproto
import (
"encoding/json"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"go.chromium.org/luci/common/data/stringset"
)
// MustNewValue creates a new *Value wrapping v, and panics if v is a bad type
func MustNewValue(v any) *Value {
ret, err := NewValue(v)
if err != nil {
panic(err)
}
return ret
}
// NewValue creates a new *Value wrapping v.
//
// Allowed types are:
// - Any of the explicit *Value_Int - style types
// - nil -> Null
// - string -> String
// - []byte -> Bytes
// - int, int8, int16, int32, int64 -> Integer
// - uint, uint8, uint16, uint32, uint64 -> Unsigned
// - float32, float64 -> Float
// - bool -> Boolean
// - map[string]any -> Object
// - []any -> Array
func NewValue(v any) (*Value, error) {
switch x := v.(type) {
case isValue_Value:
return &Value{Value: x}, nil
case nil:
return &Value{Value: &Value_Null{}}, nil
case int8, int16, int32, int64, int:
return &Value{Value: &Value_Int{reflect.ValueOf(v).Int()}}, nil
case uint8, uint16, uint32, uint64, uint:
return &Value{Value: &Value_Uint{reflect.ValueOf(v).Uint()}}, nil
case float32, float64:
return &Value{Value: &Value_Float{reflect.ValueOf(v).Float()}}, nil
case string:
return &Value{Value: &Value_Str{x}}, nil
case []byte:
return &Value{Value: &Value_Bytes{x}}, nil
case bool:
return &Value{Value: &Value_Bool{x}}, nil
case map[string]any:
ret, err := json.Marshal(x)
if err != nil {
return nil, err
}
return &Value{Value: &Value_Object{string(ret)}}, nil
case []any:
ret, err := json.Marshal(x)
if err != nil {
return nil, err
}
return &Value{Value: &Value_Array{string(ret)}}, nil
}
return nil, fmt.Errorf("unknown type %T", v)
}
// LiteralMap is a type for literal in-line param substitutions, or when you
// know statically that the params correspond to correct Value types.
type LiteralMap map[string]any
// Convert converts this to a parameter map that can be used with
// Template.Render.
func (m LiteralMap) Convert() (map[string]*Value, error) {
ret := make(map[string]*Value, len(m))
for k, v := range m {
v, err := NewValue(v)
if err != nil {
return nil, fmt.Errorf("key %q: %s", k, err)
}
ret[k] = v
}
return ret, nil
}
// RenderL renders this template with a LiteralMap, calling its Convert method
// and passing the result to Render.
func (t *File_Template) RenderL(m LiteralMap) (string, error) {
pm, err := m.Convert()
if err != nil {
return "", err
}
return t.Render(pm)
}
// Render turns the Template into a JSON document, filled with the given
// parameters. It does not validate that the output is valid JSON, but if you
// called Normalize on this Template already, then it WILL be valid JSON.
func (t *File_Template) Render(params map[string]*Value) (string, error) {
sSet := stringset.New(len(params))
replacementSlice := make([]string, 0, len(t.Param)*2)
for k, param := range t.Param {
replacementSlice = append(replacementSlice, k)
if newVal, ok := params[k]; ok {
if err := param.Accepts(newVal); err != nil {
return "", fmt.Errorf("param %q: %s", k, err)
}
sSet.Add(k)
replacementSlice = append(replacementSlice, newVal.JSONRender())
} else if param.Default != nil {
replacementSlice = append(replacementSlice, param.Default.JSONRender())
} else {
return "", fmt.Errorf("param %q: missing", k)
}
}
if len(params) != sSet.Len() {
unknown := make([]string, 0, len(params))
for k := range params {
if !sSet.Has(k) {
unknown = append(unknown, k)
}
}
sort.Strings(unknown)
return "", fmt.Errorf("unknown parameters: %q", unknown)
}
r := strings.NewReplacer(replacementSlice...)
return r.Replace(t.Body), nil
}
func (v *Value) schemaType() isSchema_Schema {
switch v.Value.(type) {
case *Value_Str:
return (*Schema_Str)(nil)
case *Value_Bytes:
return (*Schema_Bytes)(nil)
case *Value_Int:
return (*Schema_Int)(nil)
case *Value_Uint:
return (*Schema_Uint)(nil)
case *Value_Float:
return (*Schema_Float)(nil)
case *Value_Bool:
return (*Schema_Bool)(nil)
case *Value_Object:
return (*Schema_Object)(nil)
case *Value_Array:
return (*Schema_Array)(nil)
}
panic(fmt.Errorf("unknown type %T", v.Value))
}
func schemaTypeStr(s isSchema_Schema) string {
switch s.(type) {
case *Schema_Str:
return "str"
case *Schema_Bytes:
return "bytes"
case *Schema_Int:
return "int"
case *Schema_Uint:
return "uint"
case *Schema_Float:
return "float"
case *Schema_Bool:
return "bool"
case *Schema_Enum:
return "enum"
case *Schema_Object:
return "object"
case *Schema_Array:
return "array"
}
panic(fmt.Errorf("unknown type %T", s))
}
// JSONRender returns the to-be-injected string rendering of v.
func (v *Value) JSONRender() string {
return v.Value.(interface {
JSONRender() string
}).JSONRender()
}
// JSONRender returns a rendering of this string as JSON, e.g. go value "foo"
// renders as `"foo"`.
func (v *Value_Str) JSONRender() string {
ret, err := json.Marshal(v.Str)
if err != nil {
panic(err)
}
return string(ret)
}
// JSONRender returns a rendering of these bytes as JSON, e.g. go value
// []byte("foo") renders as `"Zm9v"`.
func (v *Value_Bytes) JSONRender() string {
ret, err := json.Marshal(v.Bytes)
if err != nil {
panic(err)
}
return string(ret)
}
// JSONRender returns a rendering of this int as JSON, e.g. go value 100
// renders as `100`. If the absolute value is > 2**53, this will render it as
// a string.
//
// Integers render as strings to avoid encoding issues in JSON, which only
// supports double-precision floating point numbers.
func (v *Value_Int) JSONRender() string {
num := strconv.FormatInt(v.Int, 10)
abs := v.Int
if abs < 0 {
abs = -abs
}
if abs < (1 << 53) {
return num
}
return fmt.Sprintf(`"%s"`, num)
}
// JSONRender returns a rendering of this uint as JSON, e.g. go value 100
// renders as `"100"`.
//
// Unsigns render as strings to avoid encoding issues in JSON, which only
// supports double-precision floating point numbers.
func (v *Value_Uint) JSONRender() string {
num := strconv.FormatUint(v.Uint, 10)
if v.Uint < (1 << 53) {
return num
}
return fmt.Sprintf(`"%s"`, num)
}
// JSONRender returns a rendering of this float as JSON, e.g. go value 1.23
// renders as `1.23`.
func (v *Value_Float) JSONRender() string {
ret, err := json.Marshal(v.Float)
if err != nil {
panic(err)
}
return string(ret)
}
// JSONRender returns a rendering of this bool as JSON, e.g. go value true
// renders as `true`.
func (v *Value_Bool) JSONRender() string {
if v.Bool {
return "true"
}
return "false"
}
// JSONRender returns a rendering of this JSON object as JSON. This is a direct
// return of the JSON encoded string; no validation is done. To check that the
// contained string is valid, use the Valid() method.
func (v *Value_Object) JSONRender() string {
return v.Object
}
// JSONRender returns a rendering of this JSON array as JSON. This is a direct
// return of the JSON encoded string; no validation is done. To check that the
// contained string is valid, use the Valid() method.
func (v *Value_Array) JSONRender() string {
return v.Array
}
// JSONRender returns a rendering of null. This always returns `null`.
func (v *Value_Null) JSONRender() string {
return "null"
}