github.com/nevalang/neva@v0.23.1-0.20240507185603-7696a9bb8dda/internal/runtime/funcs/printf.go (about)

     1  package funcs
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/nevalang/neva/internal/runtime"
    10  )
    11  
    12  type printf struct{}
    13  
    14  func (p printf) Create(io runtime.FuncIO, _ runtime.Msg) (func(ctx context.Context), error) {
    15  	tplIn, err := io.In.Port("tpl")
    16  	if err != nil {
    17  		return nil, err
    18  	}
    19  
    20  	argsIn, ok := io.In["args"]
    21  	if !ok {
    22  		return nil, fmt.Errorf("missing required input port 'args'")
    23  	}
    24  
    25  	argsOut, ok := io.Out["args"]
    26  	if !ok {
    27  		return nil, fmt.Errorf("missing required output port 'args'")
    28  	}
    29  
    30  	if len(argsIn) != len(argsOut) {
    31  		return nil, fmt.Errorf("input and output ports 'args' must have the same length")
    32  	}
    33  
    34  	errOut, err := io.Out.Port("err")
    35  	if err != nil {
    36  		return nil, err
    37  	}
    38  
    39  	return p.handle(tplIn, argsIn, errOut, argsOut)
    40  }
    41  
    42  func (printf) handle(
    43  	tplIn chan runtime.Msg,
    44  	argsIn []chan runtime.Msg,
    45  	errOut chan runtime.Msg,
    46  	argsOuts []chan runtime.Msg,
    47  ) (func(ctx context.Context), error) {
    48  	return func(ctx context.Context) {
    49  		var (
    50  			tpl  runtime.Msg
    51  			args = make([]runtime.Msg, len(argsIn))
    52  		)
    53  
    54  		for {
    55  			select {
    56  			case <-ctx.Done():
    57  				return
    58  			case tpl = <-tplIn:
    59  			}
    60  
    61  			for i, argIn := range argsIn {
    62  				select {
    63  				case <-ctx.Done():
    64  					return
    65  				case arg := <-argIn:
    66  					args[i] = arg
    67  				}
    68  			}
    69  
    70  			res, err := format(tpl.Str(), args)
    71  			if err != nil {
    72  				select {
    73  				case <-ctx.Done():
    74  					return
    75  				case errOut <- errorFromString(err.Error()):
    76  				}
    77  				continue
    78  			}
    79  
    80  			if _, err := fmt.Print(res); err != nil {
    81  				select {
    82  				case <-ctx.Done():
    83  					return
    84  				case errOut <- errorFromString(err.Error()):
    85  				}
    86  				continue
    87  			}
    88  
    89  			for i, argOut := range argsOuts {
    90  				select {
    91  				case <-ctx.Done():
    92  					return
    93  				case argOut <- args[i]:
    94  				}
    95  			}
    96  		}
    97  	}, nil
    98  }
    99  
   100  func format(tpl string, args []runtime.Msg) (string, error) {
   101  	// Use a map to keep track of which arguments have been used
   102  	usedArgs := make(map[int]bool)
   103  
   104  	// Builder to construct the final result
   105  	var result strings.Builder
   106  	result.Grow(len(tpl)) // Optimistically assume no increase in length
   107  
   108  	// Scan through the template to find and replace placeholders
   109  	i := 0
   110  	for i < len(tpl) {
   111  		if tpl[i] == '$' {
   112  			// Attempt to read an argument index after the '$'
   113  			j := i + 1
   114  			argIndexStr := ""
   115  			for j < len(tpl) && tpl[j] >= '0' && tpl[j] <= '9' {
   116  				argIndexStr += string(tpl[j])
   117  				j++
   118  			}
   119  
   120  			if argIndexStr != "" {
   121  				argIndex, err := strconv.Atoi(argIndexStr)
   122  				if err != nil {
   123  					// Handle the error if the conversion fails
   124  					return "", fmt.Errorf("invalid placeholder %s: %v", argIndexStr, err)
   125  				}
   126  
   127  				if argIndex < 0 || argIndex >= len(args) {
   128  					return "", fmt.Errorf("template refers to arg %d, but only %d args given", argIndex, len(args))
   129  				}
   130  
   131  				// Mark this arg as used
   132  				usedArgs[argIndex] = true
   133  
   134  				// Replace the placeholder with the argument's string representation
   135  				result.WriteString(args[argIndex].String())
   136  
   137  				// Move past the current placeholder in the template
   138  				i = j
   139  				continue
   140  			}
   141  		}
   142  
   143  		// If not processing a placeholder, just copy the current character
   144  		result.WriteByte(tpl[i])
   145  		i++
   146  	}
   147  
   148  	// Check if all arguments were used
   149  	if len(usedArgs) != len(args) {
   150  		return "", fmt.Errorf("not all arguments were used in the template")
   151  	}
   152  
   153  	return result.String(), nil
   154  }