Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map function #378

Open
arran4 opened this issue Aug 7, 2023 · 0 comments
Open

Map function #378

arran4 opened this issue Aug 7, 2023 · 0 comments

Comments

@arran4
Copy link

arran4 commented Aug 7, 2023

As generics don't seem to be supported by text/template yet I produced the following: (Or does anyone know what other library I should be looking at?)

Would something like this be accepted into the project?

package main

import (
	"errors"
	"fmt"
	"os"
	"reflect"
	"text/template"
)

var (
	ErrInputFuncMustTake0or1Arguments     = errors.New("expected second parameter function to take 0 or 1 parameters")
	ErrExpectedFirstParameterToBeSlice    = errors.New("expected first parameter to be an slice")
	ErrExpected2ndArgumentToBeFunction    = errors.New("expected second parameter to be a function")
	ErrExpectedSecondReturnToBeError      = errors.New("expected second return type to be assignable to error")
	ErrExpectedSecondArgumentToBeFunction = errors.New("expected second return of function f to take 1 or 2 parameters instead")
	ErrExpected2ReturnTypes               = errors.New("expected return with 1 or 2 arguments of types (any, error?)")
)

func MapTemplateFunc(array any, f any) (any, error) {
	av := reflect.ValueOf(array)
	if av.Kind() != reflect.Slice {
		return array, fmt.Errorf("%w not %s", ErrExpectedFirstParameterToBeSlice, av.Kind())
	}
	fv := reflect.ValueOf(f)
	if fv.Kind() != reflect.Func {
		return array, ErrExpected2ndArgumentToBeFunction
	}
	var fvfpt reflect.Type
	switch fv.Type().NumIn() {
	case 0:
	case 1:
		fvfpt = fv.Type().In(0)
	default:
		return array, ErrInputFuncMustTake0or1Arguments
	}
	var fvfrt reflect.Type
	switch fv.Type().NumOut() {
	case 1:
		fvfrt = fv.Type().Out(0)
	case 2:
		fvsrt := fv.Type().Out(1)
		if !fvsrt.AssignableTo(reflect.TypeOf(error(nil))) {
			return array, fmt.Errorf("%w instead got: %s", ErrExpectedSecondReturnToBeError, fvsrt)
		}
	default:
		return array, fmt.Errorf("%w got: %d", ErrExpectedSecondArgumentToBeFunction, fv.Type().NumOut())
	}
	l := av.Len()
	ra := make([]reflect.Value, l)
	var newType reflect.Type = fvfrt
	toan := reflect.TypeOf(any(nil))
	for i := 0; i < l; i++ {
		var r []reflect.Value
		switch fv.Type().NumIn() {
		case 1:
			if fvfpt != nil {
				ev := av.Index(i)
				if !ev.Type().AssignableTo(fvfpt) {
					return nil, fmt.Errorf("item %d not assignable to: %s", i, fvfpt)
				}
				r = fv.Call([]reflect.Value{ev})
				break
			}
			fallthrough
		case 0:
			r = fv.Call([]reflect.Value{})
		default:
			return array, ErrInputFuncMustTake0or1Arguments
		}
		if r == nil {
			continue
		}
		if len(r) != 1 && len(r) != 2 {
			return array, fmt.Errorf("f execution number %d returned: %d results %w", i, len(r), ErrExpected2ReturnTypes)
		}
		if len(r) == 2 && !r[1].IsNil() {
			return nil, fmt.Errorf("f execution number %d returned: %w", i, r[1].Interface().(error))
		}
		rt := reflect.TypeOf(r[0])
		if newType == nil {
			newType = rt
		} else if rt != newType && !rt.AssignableTo(newType) {
			rt = toan
		}
		ra[i] = r[0]
	}
	if newType == nil {
		return ra, nil
	}
	nra := reflect.MakeSlice(reflect.SliceOf(newType), l, l)
	for i, e := range ra {
		nra.Index(i).Set(e)
	}
	return nra.Interface(), nil
}

func main() {
	// Create a template.
	funcs := map[string]any{
		"map": MapTemplateFunc,
		"inc": func(i int) int {
			return i + 1
		},
		"odd": func(i int) bool {
			return i%2 == 1
		},
	}
	tmpl := template.Must(template.New("").Funcs(funcs).Parse(`
        {{ map $.Data $.Funcs.inc }}
        {{ map $.Data $.Funcs.odd }}
    `))

	// Create some data to be used in the template.
	data := struct {
		Data  []int
		Funcs map[string]any
	}{
		Data:  []int{1, 2, 3, 4},
		Funcs: funcs,
	}

	// Execute the template with the data.
	err := tmpl.Execute(os.Stdout, data)
	if err != nil {
		fmt.Println(err)
	}
}

It would be unreasonable for functions like this to match some of the clauses here:
We followed these principles to decide which functions to add and how to implement them:

  • Use template functions to build layout. The following
    types of operations are within the domain of template functions:
    • Formatting
    • Layout
    • Simple type conversions
    • Utilities that assist in handling common formatting and layout needs (e.g. arithmetic)
  • Template functions should not return errors unless there is no way to print
    a sensible value. For example, converting a string to an integer should not
    produce an error if conversion fails. Instead, it should display a default
    value.
  • Simple math is necessary for grid layouts, pagers, and so on. Complex math
    (anything other than arithmetic) should be done outside of templates.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant