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 }