diff --git a/poet/code.go b/poet/code.go index 52c465b..cd36fb2 100644 --- a/poet/code.go +++ b/poet/code.go @@ -1,9 +1,19 @@ package poet import ( + "fmt" + "regexp" "strings" ) +const ( + replaceTypeLiteral rune = 'L' + replaceTypeString rune = 'S' + replaceTypeType rune = 'T' +) + +var stringFormatRegex = regexp.MustCompile(`\${1,2}\d*[LST]`) + type Code struct { RawCode string IsFlow bool @@ -15,8 +25,56 @@ type Code struct { Statements []Code } +func stringify(raw any) string { + if raw == nil { + return "null" + } + + return fmt.Sprintf("%v", raw) +} + func formatRawCode(ctx *Context, rawCode string, arguments []any) string { - return "" + matchIndex := 0 + + return stringFormatRegex.ReplaceAllStringFunc(rawCode, func(match string) string { + // if the pattern is escaped + if strings.HasPrefix(match, "$$") { + return match[1:] + } + + argumentIndex, replaceType := 0, rune(match[len(match)-1]) + for i := 1; i < len(match)-1; i++ { + argumentIndex = (argumentIndex * 10) + int(match[i]-'0') + } + + argumentIndex -= 1 + if argumentIndex < 0 { + argumentIndex = matchIndex + } + + if argumentIndex > len(arguments)-1 { + // tried to access an argument that is not there - TODO allow errors to be returned from formatting + return match + } + + replacement := match + switch replaceType { + case replaceTypeLiteral: + replacement = stringify(arguments[argumentIndex]) + case replaceTypeString: + replacement = fmt.Sprintf("%q", stringify(arguments[argumentIndex])) + case replaceTypeType: + // it is unlikely that a user will ever want to include the generic constraint in the formatted + // value - in the future could make this configurable through extended replace type codes + replacement = arguments[argumentIndex].(TypeName).Format(ctx, ExcludeConstraints) + } + + if len(match) == 2 { + matchIndex += 1 + } + + return replacement + }) } func formatStatements(ctx *Context, statements []Code) string { @@ -47,7 +105,7 @@ func (c *Code) Format(ctx *Context) string { if c.IsFlow { // Control flow statement - sb.WriteString(c.RawCode) + sb.WriteString(formatRawCode(ctx, c.RawCode, c.Arguments)) sb.WriteString(" {\n") sb.WriteString(ctx.indent(formatStatements(ctx, c.Statements))) sb.WriteString("}") @@ -57,7 +115,7 @@ func (c *Code) Format(ctx *Context) string { if c.RawCode != "" && !c.IsFlow { // Simple statement - sb.WriteString(c.RawCode) + sb.WriteString(formatRawCode(ctx, c.RawCode, c.Arguments)) if !strings.HasSuffix(c.RawCode, ";") { sb.WriteRune(';') } diff --git a/poet/code_test.go b/poet/code_test.go new file mode 100644 index 0000000..c71cb96 --- /dev/null +++ b/poet/code_test.go @@ -0,0 +1,104 @@ +package poet + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCodeBuilder_StringFormatting(t *testing.T) { + tests := []struct { + name string + code string + expected string + args []any + }{ + { + name: "simple replacement", + code: "$T $L = $S", + expected: `String test = "a value"`, + args: []any{String, "test", "a value"}, + }, + { + name: "complex replacement", + code: "$T $3L = $2S + $3S", + expected: `String test = "a value" + "test"`, + args: []any{String, "a value", "test"}, + }, + { + name: "nil values", + code: "$1L $1S", + expected: `null "null"`, + args: []any{nil}, + }, + { + name: "bool values", + code: "$1L $1S", + expected: `true "true"`, + args: []any{true}, + }, + { + name: "int values", + code: "$1L $1S", + expected: `1 "1"`, + args: []any{1}, + }, + { + name: "float values", + code: "$1L $1S", + expected: `1.1 "1.1"`, + args: []any{1.1}, + }, + { + name: "string values", + code: "$1L $1S", + expected: `hello "hello"`, + args: []any{"hello"}, + }, + { + name: "quoted strings", + code: "$1L $1S", + expected: `"hello" "\"hello\""`, + args: []any{`"hello"`}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + + ctx := NewContext("io.github.tandemdude") + + code := NewCodeBuilder(). + AddStatement(tt.code, tt.args...). + Build() + + assert.Equal(tt.expected+";\n", code.Format(ctx)) + }) + } +} + +func TestCodeBuilder_MultipleStatements(t *testing.T) { + assert := assert.New(t) + + ctx := NewContext("io.github.tandemdude") + + code := NewCodeBuilder(). + AddStatement("$L $S", false, true). + AddStatement("$L $S", true, false). + Build() + + assert.Equal("false \"true\";\ntrue \"false\";\n", code.Format(ctx)) +} + +func TestCodeBuilder_NoStatements(t *testing.T) { + assert := assert.New(t) + + ctx := NewContext("io.github.tandemdude") + + code := NewCodeBuilder().Build() + + assert.Equal("", code.Format(ctx)) +}