├── go.sum ├── go.mod ├── LICENSE.txt ├── readme.md ├── tstruct.go └── tstruct_test.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/josharian/tstruct 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Josh Bleecher Snyder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Package tstruct provides template FuncMap helpers to construct struct literals within a Go template. 2 | 3 | In your Go code: 4 | 5 | ```go 6 | type T struct { 7 | S string 8 | N int 9 | M map[string]int 10 | L []float64 11 | } 12 | 13 | m := template.FuncMap{ /* your func map here */ } 14 | err := tstruct.AddFuncMap[T](m) 15 | // handle err 16 | ``` 17 | 18 | This will register FuncMap functions called `T`, `S`, `N`, `M`, and `L`, for the struct name and each of its fields. You can use them to construct and populate a T from a template: 19 | 20 | ``` 21 | {{ template "template-that-renders-T" T 22 | (S "a string") 23 | (L 1.0 2.0) 24 | (M "one map entry" 1) 25 | (M "another map entry" 2) 26 | (M "x" 3 "y" 4) 27 | (N 42) 28 | (L 4.0) 29 | }} 30 | ``` 31 | 32 | And voila: Your template will be called with a struct equal to: 33 | 34 | ```go 35 | T{ 36 | S: "a string", 37 | N: 42, 38 | M: map[string]int{"one map entry": 1, "another map entry": 2, "x": 3, "y": 4}, 39 | L: []float64{1.0, 2.0, 4.0}, 40 | } 41 | ``` 42 | 43 | Note that order is irrelevant, except for slice appends. 44 | 45 | As a special case (matching package flag), you may omit the argument `true` when setting a bool field to true: `(Enabled)` is equivalent to `(Enabled true)`. 46 | 47 | If you have multiple struct types whose fields share a name, the field setters will Just Work, despite having a single name. However, no two struct types may share a name, nor can a struct type and a field share a name. 48 | 49 | To request that tstruct ignore a struct field, add the struct tag `tstruct:"-"` to it. 50 | 51 | To require that a value for struct field be explicitly provided, add the struct tag `tstruct:"+"` to it. 52 | 53 | If you need to construct an unusual type from a template, there's a magic method: `TStructSet`. To use it, declare a type that has that method on a pointer receiver. It can accept any number of args, which will be passed directly from the template args. In the method, set the value according to the args. 54 | 55 | Example: 56 | 57 | ```go 58 | type Repeat string 59 | 60 | func (x *Repeat) TStructSet(s string, count int) { 61 | *x = Repeat(strings.Repeat(s, count)) 62 | } 63 | 64 | type U struct { 65 | S Repeat 66 | } 67 | ``` 68 | 69 | In your template: 70 | 71 | ``` 72 | {{ $lab := U (S "hi " 15) }} 73 | ``` 74 | 75 | That creates a `U`, and populates its `S` field by calling `(*Repeat).TStructSet` with arguments `"hi "` and `15`. 76 | 77 | The conflict with the field named `S` in type `T` is handled automatically. 78 | 79 | --- 80 | 81 | If this is not what you wanted, you might check out https://pkg.go.dev/rsc.io/tmplfunc. 82 | 83 | If this is _almost_ what you wanted, but not quite, tell me about it. :) 84 | -------------------------------------------------------------------------------- /tstruct.go: -------------------------------------------------------------------------------- 1 | // Package tstruct provides template FuncMap helpers to construct struct literals within a Go template. 2 | // 3 | // See also https://pkg.go.dev/rsc.io/tmplfunc. 4 | // 5 | // TODO: Unify docs with README, link to blog post (if I ever write one). 6 | package tstruct 7 | 8 | import ( 9 | "fmt" 10 | "reflect" 11 | "sort" 12 | "strings" 13 | ) 14 | 15 | // AddFuncMap adds constructors for T to base. 16 | // base must not be nil. 17 | // AddFuncMap will return an error if there is a conflict with any existing entries in base. 18 | // AddFuncMap may modify entries in base that were added by a prior call to AddFuncMap. 19 | // If AddFuncMap returns a non-nil error, base will be unmodified. 20 | func AddFuncMap[T any](base map[string]any) error { 21 | if base == nil { 22 | return fmt.Errorf("base FuncMap is nil") 23 | } 24 | var t T 25 | rt := reflect.TypeOf(t) 26 | origrt := rt 27 | if rt.Kind() == reflect.Pointer { 28 | rt = rt.Elem() 29 | } 30 | if rt.Kind() != reflect.Struct { 31 | return fmt.Errorf("non-struct type %v", rt) 32 | } 33 | // Make a copy of base to modify. 34 | // This is safe because all keys are strings and all values are funcs. 35 | fnmap := make(map[string]any) 36 | copyFuncMap(fnmap, base) 37 | // Add struct and field funcs to fnmap. 38 | err := addStructFuncs[T](origrt, fnmap) 39 | if err != nil { 40 | return err 41 | } 42 | // Nothing went wrong; copy our modified FuncMap back onto base. 43 | copyFuncMap(base, fnmap) 44 | return nil 45 | } 46 | 47 | func copyFuncMap(dst, src map[string]any) { 48 | for k, v := range src { 49 | dst[k] = v 50 | } 51 | } 52 | 53 | // addStructFuncs adds funcs to fnmap to construct structs of type rt and to populate rt's fields. 54 | func addStructFuncs[T any](rt reflect.Type, fnmap map[string]any) error { 55 | origrt := rt 56 | if rt.Kind() == reflect.Ptr { 57 | rt = rt.Elem() 58 | } 59 | if rt.Name() == "" { 60 | return fmt.Errorf("anonymous struct (type %v) is not supported", rt) 61 | } 62 | // TODO: Accept namespacing prefix(es)? 63 | 64 | // Make a struct constructor for rt with the same name as the struct. 65 | // It takes as arguments functions that can be applied to modify the struct. 66 | // We generate functions that return such arguments below. 67 | if x, ok := fnmap[rt.Name()]; ok { 68 | match := registeredFuncMatches[T](x, rt) 69 | if !match { 70 | return fmt.Errorf("conflicting FuncMap entries for %s: %T", rt.Name(), x) 71 | } 72 | // We already have a constructor for this struct type. 73 | // Replace it with a more precisely typed one, if possible. 74 | // But if T is reflect.Value, we risk overwriting a more precisely typed function. 75 | var zero T 76 | if reflect.TypeOf(zero) == reflectValueType { 77 | return nil 78 | } 79 | } 80 | 81 | var required fieldsAreUnset 82 | for i := 0; i < rt.NumField(); i++ { 83 | f := rt.Field(i) 84 | if !f.IsExported() || f.Tag.Get("tstruct") != "+" { 85 | continue 86 | } 87 | // Require that this struct field be set. 88 | if required == nil { 89 | required = make(fieldsAreUnset) 90 | } 91 | required[f.Name] = true 92 | } 93 | 94 | fnmap[rt.Name()] = func(args ...applyFn) T { 95 | v := reflect.New(rt).Elem() 96 | // If there are required fields, check whether they are about to be set. 97 | if required != nil { 98 | // clone required 99 | // TODO: when Go 1.21 is out, use maps.Clone 100 | r2 := make(fieldsAreUnset, len(required)) 101 | for k, v := range required { 102 | r2[k] = v 103 | } 104 | rqv := reflect.ValueOf(r2) 105 | // Call apply using our special sentinel map type. 106 | // Each apply function will delete the field name it is responsible for 107 | // from the map, but not do any further work. 108 | for _, apply := range args { 109 | apply(rqv) 110 | } 111 | // Gather all unset required fields. 112 | if len(r2) > 0 { 113 | missing := make([]string, 0, len(r2)) 114 | for k := range r2 { 115 | missing = append(missing, rt.Name()+"."+k) 116 | } 117 | sort.Strings(missing) 118 | panic(fmt.Sprintf("%s required but not provided", strings.Join(missing, ", "))) 119 | } 120 | } 121 | // Now, actually set the fields. 122 | for _, apply := range args { 123 | apply(v) 124 | } 125 | var t T 126 | switch any(t).(type) { 127 | case reflect.Value: 128 | // We want to return a reflect.Value. 129 | // v already is a reflect.Value. 130 | // (We know that; help the compiler.) 131 | return any(v).(T) 132 | } 133 | if origrt.Kind() == reflect.Pointer { 134 | v = v.Addr() 135 | } 136 | // v holds a T. Extract it. 137 | return v.Interface().(T) 138 | } 139 | 140 | // For each struct field, generate a function that modifies that struct field, 141 | // named after the struct field. 142 | // Make args with the same name as each of the struct fields. 143 | for i := 0; i < rt.NumField(); i++ { 144 | f := rt.Field(i) 145 | if !f.IsExported() { 146 | continue 147 | } 148 | if f.Tag.Get("tstruct") == "-" { 149 | // Ignore this struct field. 150 | continue 151 | } 152 | switch f.Type.Kind() { 153 | case reflect.Struct: 154 | // Process this struct's fields as well! 155 | // TODO: avoid panic on recursively defined structs (but really, don't do that) 156 | err := addStructFuncs[reflect.Value](f.Type, fnmap) 157 | if err != nil { 158 | return err 159 | } 160 | case reflect.Slice: 161 | if elem := f.Type.Elem(); elem.Kind() == reflect.Struct { 162 | err := addStructFuncs[reflect.Value](elem, fnmap) 163 | if err != nil { 164 | return err 165 | } 166 | } 167 | case reflect.Map: 168 | for _, elem := range []reflect.Type{f.Type.Key(), f.Type.Elem()} { 169 | if elem.Kind() == reflect.Struct { 170 | err := addStructFuncs[reflect.Value](elem, fnmap) 171 | if err != nil { 172 | return err 173 | } 174 | } 175 | } 176 | } 177 | name := f.Name 178 | // TODO: modify fn name based on field type? E.g. AppendF for a field named F of slice type? 179 | fn, err := genSavedApplyFnForField(f, name) 180 | if err != nil { 181 | return err 182 | } 183 | err = setSavedApplyFn(fnmap, name, rt, fn) 184 | if err != nil { 185 | return err 186 | } 187 | } 188 | return nil 189 | } 190 | 191 | func registeredFuncMatches[T any](x any, rt reflect.Type) bool { 192 | // There's already a registered function with the name we want to use. 193 | // If it is a tstruct constructor for the exact same type as we are 194 | // trying to generate now, that's ok. Otherwise, fail. 195 | _, isTypedCtor := x.(func(args ...applyFn) T) 196 | if isTypedCtor { 197 | // OK 198 | return true 199 | } 200 | // Check whether x is a func(args ...applyFn) T for any T, including possibly reflect.Value. 201 | // If so, call x to get a struct value whose type we can inspect. 202 | xfn := reflect.ValueOf(x) 203 | if xfn.Kind() != reflect.Func { 204 | return false 205 | } 206 | xType := xfn.Type() 207 | if xType.NumIn() != 1 || xType.NumOut() != 1 || !xType.IsVariadic() { 208 | return false 209 | } 210 | in := xType.In(0) 211 | if in.Kind() != reflect.Slice || in.Elem() != applyFnType { 212 | return false 213 | } 214 | out := xfn.Call(nil) 215 | s := out[0] 216 | // If s is a reflect.Value (holding a reflect.Value!), use its contents instead. 217 | if s.Type() == reflectValueType { 218 | s = s.Interface().(reflect.Value) 219 | } 220 | return s.Type() == rt 221 | } 222 | 223 | // fieldsAreUnset is a special sentinel type that applyFn recognizes. 224 | // It is a map from a field name to whether it remains unset. 225 | type fieldsAreUnset map[string]bool 226 | 227 | var fieldsAreUnsetType = reflect.TypeOf(fieldsAreUnset(nil)) 228 | 229 | // didMarkFieldAsSet checks whether this is a request to mark the field name as having been set by an apply function. 230 | // If it returns true, the apply function must stop processing v. 231 | func didMarkFieldAsSet(v reflect.Value, name string) bool { 232 | if v.Type() != fieldsAreUnsetType { 233 | return false 234 | } 235 | // Update the map: This field is no longer unset. 236 | m := v.Interface().(fieldsAreUnset) 237 | delete(m, name) 238 | return true 239 | } 240 | 241 | // genSavedApplyFnForField generates a savedApplyFn for f, to be given name name. 242 | func genSavedApplyFnForField(f reflect.StructField, name string) (savedApplyFn, error) { 243 | method, ok := reflect.PtrTo(f.Type).MethodByName("TStructSet") 244 | if ok { 245 | if method.Type.NumOut() != 0 { 246 | return nil, fmt.Errorf("(*%v).TStructSet (for field %s) must not return values", f.Type.Name(), f.Name) 247 | } 248 | if _, ok := f.Type.MethodByName("TStructSet"); ok { 249 | return nil, fmt.Errorf("(%v).TStructSet (for field %s) must have pointer receiver", f.Type.Name(), f.Name) 250 | } 251 | return func(args ...reflect.Value) applyFn { 252 | return func(v reflect.Value) { 253 | if didMarkFieldAsSet(v, name) { 254 | return 255 | } 256 | x := reflect.New(method.Type.In(0).Elem()) 257 | dvArgs := devirtAll(args) 258 | args = append([]reflect.Value{x}, dvArgs...) 259 | method.Func.Call(args) 260 | convertAndSet(v.FieldByIndex(f.Index), x.Elem()) 261 | } 262 | }, nil 263 | } 264 | 265 | switch f.Type.Kind() { 266 | case reflect.Map: 267 | return func(args ...reflect.Value) applyFn { 268 | return func(dst reflect.Value) { 269 | if didMarkFieldAsSet(dst, name) { 270 | return 271 | } 272 | f := dst.FieldByIndex(f.Index) 273 | if f.IsZero() { 274 | f.Set(reflect.MakeMap(f.Type())) 275 | } 276 | if len(args) == 1 { 277 | // If it is a map arg with appropriate types, copy the elems over. 278 | arg := devirt(args[0]) 279 | typ := arg.Type() 280 | ftyp := f.Type() 281 | if typ.Kind() == reflect.Map && typ.Key().AssignableTo(ftyp.Key()) && typ.Elem().AssignableTo(ftyp.Elem()) { 282 | iter := arg.MapRange() 283 | for iter.Next() { 284 | f.SetMapIndex(iter.Key(), iter.Value()) 285 | } 286 | // success 287 | return 288 | } 289 | } 290 | if len(args)&1 != 0 { 291 | panic(fmt.Sprintf("odd number of args to %v, expected (key, elem) pairs, got %d args", name, len(args))) 292 | } 293 | for i := 0; i < len(args); i += 2 { 294 | k := args[i] 295 | e := args[i+1] 296 | f.SetMapIndex(devirt(k), devirt(e)) 297 | } 298 | } 299 | }, nil 300 | case reflect.Slice: 301 | return func(args ...reflect.Value) applyFn { 302 | return func(dst reflect.Value) { 303 | if didMarkFieldAsSet(dst, name) { 304 | return 305 | } 306 | f := dst.FieldByIndex(f.Index) 307 | for _, arg := range devirtAll(args) { 308 | if arg.Type().AssignableTo(f.Type()) { 309 | f.Set(reflect.AppendSlice(f, arg)) 310 | } else { 311 | f.Set(reflect.Append(f, arg)) 312 | } 313 | } 314 | } 315 | }, nil 316 | // TODO: reflect.Array: Set by index with a func named AtName? Does it even matter? 317 | } 318 | // Everything else: do a plain Set 319 | return func(args ...reflect.Value) applyFn { 320 | return func(dst reflect.Value) { 321 | if didMarkFieldAsSet(dst, name) { 322 | return 323 | } 324 | out := dst.FieldByIndex(f.Index) 325 | var x reflect.Value 326 | switch len(args) { 327 | case 0: 328 | // special case for ergonomics: treat (X) as (X true) when destination has bool type 329 | if out.Type().Kind() == reflect.Bool { 330 | x = reflect.ValueOf(true) 331 | } 332 | case 1: 333 | x = args[0] 334 | } 335 | if !x.IsValid() { 336 | panic("wrong number of args to " + name + ", expected 1") 337 | } 338 | convertAndSet(out, devirt(x)) 339 | } 340 | }, nil 341 | } 342 | 343 | func setSavedApplyFn(fnmap map[string]any, name string, typ reflect.Type, fn savedApplyFn) error { 344 | existing, ok := fnmap[name] 345 | if !ok { 346 | // We are the first ones to use this function name. 347 | fnmap[name] = fn 348 | return nil 349 | } 350 | dispatch, ok := existing.(savedApplyFn) 351 | if !ok { 352 | // Someone has used this name for something other than a savedApplyFn. 353 | // Refuse to overwrite it. 354 | return fmt.Errorf("conflicting FuncMap entries for %s", name) 355 | } 356 | // We previously used this name for a savedApplyFn. 357 | // This happens when two structs share the same field name. 358 | // In that case, replace the function with a new function 359 | // that checks whether we're being applied to the right struct type, 360 | // and if not, dispatches to the previous savedApplyFn. 361 | fnmap[name] = func(args ...reflect.Value) applyFn { 362 | return func(dst reflect.Value) { 363 | if didMarkFieldAsSet(dst, name) { 364 | return 365 | } 366 | if dst.Type() == typ { 367 | // We can handle this type! Do it. 368 | fn(args...)(dst) 369 | return 370 | } 371 | // Dispatch to a previous function, in the hopes 372 | // that it can handle this unknown type. 373 | dispatch(args...)(dst) 374 | } 375 | } 376 | return nil 377 | } 378 | 379 | // A savedApplyFn accepts arguments from a template and saves them to be applied later. 380 | type savedApplyFn = func(args ...reflect.Value) applyFn 381 | 382 | // An applyFn applies previously saved arguments to v. 383 | type applyFn = func(v reflect.Value) 384 | 385 | // TODO: use reflect.TypeFor once Go 1.22 comes out 386 | var ( 387 | applyFnType = reflect.TypeOf(applyFn(nil)) 388 | reflectValueType = reflect.TypeOf(reflect.Value{}) 389 | ) 390 | 391 | // devirt makes x have a concrete type. 392 | func devirt(x reflect.Value) reflect.Value { 393 | if x.Type().Kind() == reflect.Interface { 394 | x = x.Elem() 395 | } 396 | return x 397 | } 398 | 399 | // devirtAll returns a copy of s containing devirtualized values. 400 | func devirtAll(s []reflect.Value) []reflect.Value { 401 | c := make([]reflect.Value, len(s)) 402 | for i, x := range s { 403 | c[i] = devirt(x) 404 | } 405 | return c 406 | } 407 | 408 | func convertAndSet(dst, src reflect.Value) { 409 | dst.Set(src.Convert(dst.Type())) 410 | } 411 | -------------------------------------------------------------------------------- /tstruct_test.go: -------------------------------------------------------------------------------- 1 | package tstruct_test 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "text/template" 9 | 10 | "github.com/josharian/tstruct" 11 | ) 12 | 13 | type Z string 14 | 15 | func (z *Z) TStructSet(x string) { 16 | *z = "z" + Z(x) 17 | } 18 | 19 | type S struct { 20 | URL string 21 | Data map[string]int 22 | List []int 23 | ZStr Z 24 | Sub T 25 | } 26 | 27 | type T struct { 28 | A string 29 | } 30 | 31 | func TestBasic(t *testing.T) { 32 | want := S{ 33 | URL: "x", 34 | Data: map[string]int{"a": 1, "b": 2}, 35 | List: []int{-1, -2}, 36 | ZStr: "zhello", 37 | Sub: T{A: "A"}, 38 | } 39 | const tmpl = ` 40 | {{ yield 41 | (S 42 | (URL "x") 43 | (Data "a" 1) 44 | (Data "b" 2) 45 | (List -1) 46 | (List -2) 47 | (ZStr "hello") 48 | (Sub (T (A "A"))) 49 | ) 50 | }} 51 | ` 52 | testOne(t, want, tmpl) 53 | } 54 | 55 | func TestDevirtualization(t *testing.T) { 56 | m := make(template.FuncMap) 57 | err := tstruct.AddFuncMap[S](m) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | m["yield"] = func(x any) error { 62 | want := S{ 63 | URL: "a", 64 | Data: map[string]int{"a": 1}, 65 | List: []int{1}, 66 | ZStr: "za", 67 | Sub: T{A: "a"}, 68 | } 69 | if !reflect.DeepEqual(x, want) { 70 | t.Fatalf("got %#v, want %#v", x, want) 71 | } 72 | return nil 73 | } 74 | const tmpl = ` 75 | {{ yield 76 | (S 77 | (URL .Str) 78 | (Data .Str .Int) 79 | (List .Int) 80 | (ZStr .Str) 81 | (Sub (T (A .Str))) 82 | ) 83 | }} 84 | ` 85 | p, err := template.New("test").Funcs(m).Parse(tmpl) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | err = p.Execute(io.Discard, map[string]any{"Str": "a", "Int": 1}) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | } 94 | 95 | func TestFieldReuse(t *testing.T) { 96 | type X struct { 97 | F int 98 | } 99 | type Y struct { 100 | F string 101 | } 102 | type W struct { 103 | F Z 104 | } 105 | m := make(template.FuncMap) 106 | err := tstruct.AddFuncMap[X](m) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | err = tstruct.AddFuncMap[Y](m) 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | err = tstruct.AddFuncMap[W](m) 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | calls := 0 119 | m["yield"] = func(x any) error { 120 | calls++ 121 | switch x.(type) { 122 | case X: 123 | want := X{F: 1} 124 | if !reflect.DeepEqual(x, want) { 125 | t.Fatalf("got %#v, want %#v", x, want) 126 | } 127 | case Y: 128 | want := Y{F: "a"} 129 | if !reflect.DeepEqual(x, want) { 130 | t.Fatalf("got %#v, want %#v", x, want) 131 | } 132 | case W: 133 | want := W{F: "za"} 134 | if !reflect.DeepEqual(x, want) { 135 | t.Fatalf("got %#v, want %#v", x, want) 136 | } 137 | default: 138 | t.Fatalf("unexpected type %T", x) 139 | } 140 | return nil 141 | } 142 | const tmpl = `{{ yield (X (F 1)) }} {{ yield (Y (F "a")) }} {{ yield (W (F "a")) }}` 143 | p, err := template.New("test").Funcs(m).Parse(tmpl) 144 | if err != nil { 145 | t.Fatal(err) 146 | } 147 | err = p.Execute(io.Discard, nil) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | if calls != 3 { 152 | t.Fatalf("got %d calls, want 3", calls) 153 | } 154 | } 155 | 156 | type WrapS struct { 157 | Inner S 158 | } 159 | 160 | type ( 161 | sFn = func(...func(reflect.Value)) S 162 | wsFn = func(...func(reflect.Value)) WrapS 163 | rvFn = func(...func(reflect.Value)) reflect.Value 164 | ) 165 | 166 | func TestFieldReuseOuterInner(t *testing.T) { 167 | m := make(template.FuncMap) 168 | err := tstruct.AddFuncMap[WrapS](m) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | err = tstruct.AddFuncMap[S](m) 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | wantType[sFn](t, m["S"]) 177 | wantType[wsFn](t, m["WrapS"]) 178 | } 179 | 180 | func TestFieldReuseInnerOuter(t *testing.T) { 181 | m := make(template.FuncMap) 182 | err := tstruct.AddFuncMap[S](m) 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | err = tstruct.AddFuncMap[WrapS](m) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | wantType[sFn](t, m["S"]) 191 | wantType[wsFn](t, m["WrapS"]) 192 | } 193 | 194 | func wantType[T any](t *testing.T, got any) { 195 | t.Helper() 196 | z, ok := got.(T) 197 | if !ok { 198 | t.Fatalf("expected %T, got %T", z, got) 199 | } 200 | } 201 | 202 | func TestCollisionDetection(t *testing.T) { 203 | m := make(template.FuncMap) 204 | m["S"] = func(x any) error { return nil } 205 | err := tstruct.AddFuncMap[S](m) 206 | if err == nil { 207 | t.Fatal("expected error") 208 | } 209 | } 210 | 211 | func TestSliceOfStructs(t *testing.T) { 212 | type Sub struct { 213 | X int 214 | } 215 | type T struct { 216 | X []Sub 217 | } 218 | const tmpl = `{{ yield (T (X (Sub (X 1))) (X (Sub (X 2)))) }}` 219 | want := T{X: []Sub{{X: 1}, {X: 2}}} 220 | testOne(t, want, tmpl) 221 | } 222 | 223 | func TestAnonymousStructField(t *testing.T) { 224 | type T struct { 225 | X struct { 226 | A int 227 | } 228 | } 229 | m := make(template.FuncMap) 230 | err := tstruct.AddFuncMap[T](m) 231 | if err == nil { 232 | t.Fatalf("expected error, got %#v", m) 233 | } 234 | } 235 | 236 | func TestNonStruct(t *testing.T) { 237 | type T []int 238 | m := make(template.FuncMap) 239 | err := tstruct.AddFuncMap[T](m) 240 | if err == nil { 241 | t.Fatalf("expected error, got %#v", m) 242 | } 243 | } 244 | 245 | func TestAppendMany(t *testing.T) { 246 | type T struct { 247 | X []int 248 | } 249 | want := T{X: []int{1, 2, 3, 4}} 250 | const tmpl = `{{ yield (T (X 1 2 3) (X 4)) }}` 251 | testOne(t, want, tmpl) 252 | } 253 | 254 | func TestConvert(t *testing.T) { 255 | type Int int 256 | type T struct { 257 | X Int 258 | } 259 | want := T{X: 1} 260 | const tmpl = `{{ yield (T (X 1)) }}` 261 | testOne(t, want, tmpl) 262 | } 263 | 264 | func TestMapMany(t *testing.T) { 265 | type T struct { 266 | M map[string]int 267 | } 268 | want := T{M: map[string]int{"a": 1, "b": 2, "c": 3}} 269 | const tmpl = `{{ yield (T (M "a" 1 "b" 2) (M "c" 3)) }}` 270 | testOne(t, want, tmpl) 271 | } 272 | 273 | func TestInterfaceField(t *testing.T) { 274 | type T struct { 275 | I any 276 | } 277 | testOne(t, T{I: 1}, `{{ yield (T (I 1)) }}`) 278 | testOne(t, T{I: "a"}, `{{ yield (T (I "a")) }}`) 279 | testOne(t, T{I: T{I: 1.0}}, `{{ yield (T (I (T (I 1.0)))) }}`) 280 | } 281 | 282 | func testOne[T any](t *testing.T, want T, tmpl string, dots ...any) { 283 | err := testRunOne[T](t, want, tmpl, dots...) 284 | if err != nil { 285 | t.Fatal(err) 286 | } 287 | } 288 | 289 | func testOneWantErrStrs[T any](t *testing.T, want T, tmpl string, substrs []string, dots ...any) { 290 | err := testRunOne[T](t, want, tmpl, dots...) 291 | if err == nil { 292 | t.Fatal("expected error") 293 | } 294 | for _, substr := range substrs { 295 | if !strings.Contains(err.Error(), substr) { 296 | t.Errorf("expected error to contain %q, got %q", substr, err) 297 | } 298 | } 299 | } 300 | 301 | func testRunOne[T any](t *testing.T, want T, tmpl string, dots ...any) error { 302 | t.Helper() 303 | m := make(template.FuncMap) 304 | err := tstruct.AddFuncMap[T](m) 305 | if err != nil { 306 | t.Fatal(err) 307 | } 308 | m["yield"] = func(x any) error { 309 | if !reflect.DeepEqual(x, want) { 310 | t.Fatalf("got %#v, want %#v", x, want) 311 | } 312 | return nil 313 | } 314 | p, err := template.New("test").Funcs(m).Parse(tmpl) 315 | if err != nil { 316 | t.Fatal(err) 317 | } 318 | var dot any 319 | if len(dots) == 1 { 320 | dot = dots[0] 321 | } 322 | return p.Execute(io.Discard, dot) 323 | } 324 | 325 | func TestRepeatedSliceStruct(t *testing.T) { 326 | type A struct { 327 | I int 328 | } 329 | type T struct { 330 | AA []A 331 | } 332 | type U struct { 333 | AA []A 334 | } 335 | // Check that it is possible to add T and U to a single FuncMap. 336 | m := make(template.FuncMap) 337 | err := tstruct.AddFuncMap[T](m) 338 | if err != nil { 339 | t.Fatal(err) 340 | } 341 | err = tstruct.AddFuncMap[U](m) 342 | if err != nil { 343 | t.Fatal(err) 344 | } 345 | // Check that it behaves correctly. 346 | type V struct { 347 | ET T 348 | EU U 349 | } 350 | want := V{ 351 | ET: T{AA: []A{{I: 1}, {I: 2}}}, 352 | EU: U{AA: []A{{I: 1}, {I: 2}}}, 353 | } 354 | const tmpl = `{{ yield 355 | (V 356 | (ET (T (AA (A (I 1)) (A (I 2))))) 357 | (EU (U (AA (A (I 1)) (A (I 2))))) 358 | ) 359 | }}` 360 | testOne(t, want, tmpl) 361 | } 362 | 363 | func TestStructFieldNameConflict(t *testing.T) { 364 | type T struct{} 365 | type S struct { 366 | T T 367 | } 368 | m := make(template.FuncMap) 369 | err := tstruct.AddFuncMap[S](m) 370 | if err == nil { 371 | t.Fatalf("expected error, got %#v", m) 372 | } 373 | } 374 | 375 | func TestIgnoreStructField(t *testing.T) { 376 | type T struct{} 377 | type S struct { 378 | X int 379 | T T `tstruct:"-"` 380 | } 381 | m := make(template.FuncMap) 382 | err := tstruct.AddFuncMap[S](m) 383 | if err != nil { 384 | t.Fatal(err) 385 | } 386 | testOne(t, S{X: 1}, `{{ yield (S (X 1)) }}`) 387 | } 388 | 389 | func TestBringASliceToASliceFight(t *testing.T) { 390 | type T struct { 391 | X []int 392 | } 393 | m := make(template.FuncMap) 394 | err := tstruct.AddFuncMap[T](m) 395 | if err != nil { 396 | t.Fatal(err) 397 | } 398 | testOne(t, T{X: []int{1, 2, 3}}, `{{ yield (T (X .Ints)) }}`, map[string]any{"Ints": []int{1, 2, 3}}) 399 | testOne(t, T{X: []int{0, 1, 2, 3}}, `{{ yield (T (X 0 .Ints)) }}`, map[string]any{"Ints": []int{1, 2, 3}}) 400 | testOne(t, T{X: []int{0, 1, 2, 3, 4, 1, 2, 3}}, `{{ yield (T (X 0 .Ints 4 .Ints)) }}`, map[string]any{"Ints": []int{1, 2, 3}}) 401 | } 402 | 403 | func TestDirectMapsWork(t *testing.T) { 404 | type T struct { 405 | X map[string]any 406 | } 407 | m := make(template.FuncMap) 408 | err := tstruct.AddFuncMap[T](m) 409 | if err != nil { 410 | t.Fatal(err) 411 | } 412 | want := map[string]any{"A": 1, "B": 2} 413 | testOne(t, T{X: want}, `{{ yield (T (X .M)) }}`, map[string]any{"M": want}) 414 | testOne(t, T{X: want}, `{{ yield (T (X .M)) }}`, map[string]any{"M": map[string]int{"A": 1, "B": 2}}) 415 | } 416 | 417 | func TestBoolTrue(t *testing.T) { 418 | type T struct { 419 | X bool 420 | } 421 | m := make(template.FuncMap) 422 | err := tstruct.AddFuncMap[T](m) 423 | if err != nil { 424 | t.Fatal(err) 425 | } 426 | testOne(t, T{X: false}, `{{ yield (T (X false)) }}`, nil) 427 | testOne(t, T{X: true}, `{{ yield (T (X true)) }}`, nil) 428 | testOne(t, T{X: true}, `{{ yield (T (X)) }}`, nil) 429 | } 430 | 431 | type R struct { 432 | NotReq string 433 | ReqInt int `tstruct:"+"` 434 | ReqSlice []int `tstruct:"+"` 435 | ReqMap map[string]int `tstruct:"+"` 436 | ReqStruct S `tstruct:"+"` 437 | ReqZStr Z `tstruct:"+"` 438 | } 439 | 440 | func TestRequired(t *testing.T) { 441 | // Check that providing required fields works. 442 | want := R{ 443 | ReqInt: 1, 444 | ReqSlice: []int{1}, 445 | ReqMap: map[string]int{"a": 1}, 446 | ReqStruct: S{URL: "x"}, 447 | ReqZStr: "zhello", 448 | } 449 | const tmpl = ` 450 | {{ yield 451 | (R 452 | (ReqInt 1) 453 | (ReqSlice 1) 454 | (ReqMap "a" 1) 455 | (ReqStruct (S (URL "x"))) 456 | (ReqZStr "hello") 457 | ) 458 | }} 459 | ` 460 | testOne(t, want, tmpl) 461 | } 462 | 463 | func TestRequiredZeroValueIsOK(t *testing.T) { 464 | // Check that providing required fields works. 465 | want := R{ 466 | ReqZStr: "z", 467 | ReqMap: map[string]int{}, 468 | } 469 | const tmpl = ` 470 | {{ yield 471 | (R 472 | (ReqInt 0) 473 | (ReqSlice) 474 | (ReqMap) 475 | (ReqStruct (S)) 476 | (ReqZStr "") 477 | ) 478 | }} 479 | ` 480 | testOne(t, want, tmpl) 481 | } 482 | 483 | func TestRequiredMissing(t *testing.T) { 484 | // Check that we catch and report all missing required fields. 485 | want := R{ 486 | ReqInt: 0, 487 | ReqSlice: []int{1}, 488 | ReqMap: map[string]int{"a": 1}, 489 | ReqStruct: S{URL: "x"}, 490 | ReqZStr: "zhello", 491 | } 492 | const tmpl = ` 493 | {{ yield 494 | (R 495 | ) 496 | }} 497 | ` 498 | testOneWantErrStrs(t, want, tmpl, []string{"required", "R.ReqInt", "R.ReqSlice", "R.ReqMap", "R.ReqStruct", "R.ReqZStr"}) 499 | } 500 | 501 | func TestFieldReuseWithRequire(t *testing.T) { 502 | // Test that we can reuse a field name if one of the uses is required, without interference. 503 | type X struct { 504 | F int 505 | } 506 | type Y struct { 507 | F string `tstruct:"+"` 508 | } 509 | 510 | m := make(template.FuncMap) 511 | err := tstruct.AddFuncMap[X](m) 512 | if err != nil { 513 | t.Fatal(err) 514 | } 515 | err = tstruct.AddFuncMap[Y](m) 516 | if err != nil { 517 | t.Fatal(err) 518 | } 519 | 520 | calls := 0 521 | m["yield"] = func(x any) error { 522 | calls++ 523 | switch x.(type) { 524 | case X: 525 | want := X{} 526 | if !reflect.DeepEqual(x, want) { 527 | t.Fatalf("got %#v, want %#v", x, want) 528 | } 529 | default: 530 | t.Fatalf("unexpected type %T", x) 531 | } 532 | return nil 533 | } 534 | const tmpl = `{{ yield (X) }} {{ yield (Y) }}` 535 | p, err := template.New("test").Funcs(m).Parse(tmpl) 536 | if err != nil { 537 | t.Fatal(err) 538 | } 539 | err = p.Execute(io.Discard, nil) 540 | // ensure X succeeded 541 | if calls != 1 { 542 | t.Fatalf("got %d calls, want 1", calls) 543 | } 544 | // ensure Y failed, with the right error 545 | if err == nil { 546 | t.Fatal("expected error, got none") 547 | } 548 | if !strings.Contains(err.Error(), "required") { 549 | t.Fatalf("expected error to contain %q, got %q", "required", err) 550 | } 551 | } 552 | 553 | func TestRequiredTrackingNoSharedState(t *testing.T) { 554 | // Test that we don't share state between each evaluation of a struct with required fields. 555 | // 556 | // We have a required fields. 557 | // We will evaluate two templates: one with the field, and one without. 558 | // If we share state, the second evaluation will succeed, 559 | // because the first evaluation will have marked the field as present. 560 | type X struct { 561 | F int `tstruct:"+"` 562 | } 563 | m := make(template.FuncMap) 564 | err := tstruct.AddFuncMap[X](m) 565 | if err != nil { 566 | t.Fatal(err) 567 | } 568 | st := template.New("test").Funcs(m) 569 | // This should succeed: required fields are present. 570 | t0, err := st.New("first").Parse(`{{ (X (F 1)) }}`) 571 | if err != nil { 572 | t.Fatal(err) 573 | } 574 | err = t0.Execute(io.Discard, nil) 575 | if err != nil { 576 | t.Fatal(err) 577 | } 578 | // This should fail: required fields are missing. 579 | t1, err := st.New("second").Parse(`{{ (X) }}`) 580 | if err != nil { 581 | t.Fatal(err) 582 | } 583 | err = t1.Execute(io.Discard, nil) 584 | if err == nil { 585 | t.Fatal("expected error about missing required field F, got none") 586 | } 587 | } 588 | 589 | func TestPtr(t *testing.T) { 590 | want := &T{ 591 | A: "a", 592 | } 593 | const tmpl = ` 594 | {{ yield 595 | (T 596 | (A "a") 597 | ) 598 | }} 599 | ` 600 | testOne(t, want, tmpl) 601 | } 602 | 603 | type Ptr struct { 604 | P *T 605 | } 606 | --------------------------------------------------------------------------------