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 }