Skip to content

Commit 0b0dcf0

Browse files
davfsatandemdude
andauthored
feat(poet): implement string formatting for code generation (#38)
* feat(poet): implement string formatting for code generation Signed-off-by: davfsa <davfsa@gmail.com> * chore: cleanup parsing of replacement string after doing regex Signed-off-by: davfsa <davfsa@gmail.com> * chore: improve code and quoting Signed-off-by: davfsa <davfsa@gmail.com> * chore: cleanup code Signed-off-by: davfsa <davfsa@gmail.com> * feat: add tests Signed-off-by: davfsa <davfsa@gmail.com> * chore: tweaks * feat: allow escaping, fix tests --------- Signed-off-by: davfsa <davfsa@gmail.com> Co-authored-by: tandemdude <43570299+tandemdude@users.noreply.github.com>
1 parent 72e0321 commit 0b0dcf0

2 files changed

Lines changed: 165 additions & 3 deletions

File tree

poet/code.go

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package poet
22

33
import (
4+
"fmt"
5+
"regexp"
46
"strings"
57
)
68

9+
const (
10+
replaceTypeLiteral rune = 'L'
11+
replaceTypeString rune = 'S'
12+
replaceTypeType rune = 'T'
13+
)
14+
15+
var stringFormatRegex = regexp.MustCompile(`\${1,2}\d*[LST]`)
16+
717
type Code struct {
818
RawCode string
919
IsFlow bool
@@ -15,8 +25,56 @@ type Code struct {
1525
Statements []Code
1626
}
1727

28+
func stringify(raw any) string {
29+
if raw == nil {
30+
return "null"
31+
}
32+
33+
return fmt.Sprintf("%v", raw)
34+
}
35+
1836
func formatRawCode(ctx *Context, rawCode string, arguments []any) string {
19-
return ""
37+
matchIndex := 0
38+
39+
return stringFormatRegex.ReplaceAllStringFunc(rawCode, func(match string) string {
40+
// if the pattern is escaped
41+
if strings.HasPrefix(match, "$$") {
42+
return match[1:]
43+
}
44+
45+
argumentIndex, replaceType := 0, rune(match[len(match)-1])
46+
for i := 1; i < len(match)-1; i++ {
47+
argumentIndex = (argumentIndex * 10) + int(match[i]-'0')
48+
}
49+
50+
argumentIndex -= 1
51+
if argumentIndex < 0 {
52+
argumentIndex = matchIndex
53+
}
54+
55+
if argumentIndex > len(arguments)-1 {
56+
// tried to access an argument that is not there - TODO allow errors to be returned from formatting
57+
return match
58+
}
59+
60+
replacement := match
61+
switch replaceType {
62+
case replaceTypeLiteral:
63+
replacement = stringify(arguments[argumentIndex])
64+
case replaceTypeString:
65+
replacement = fmt.Sprintf("%q", stringify(arguments[argumentIndex]))
66+
case replaceTypeType:
67+
// it is unlikely that a user will ever want to include the generic constraint in the formatted
68+
// value - in the future could make this configurable through extended replace type codes
69+
replacement = arguments[argumentIndex].(TypeName).Format(ctx, ExcludeConstraints)
70+
}
71+
72+
if len(match) == 2 {
73+
matchIndex += 1
74+
}
75+
76+
return replacement
77+
})
2078
}
2179

2280
func formatStatements(ctx *Context, statements []Code) string {
@@ -47,7 +105,7 @@ func (c *Code) Format(ctx *Context) string {
47105

48106
if c.IsFlow {
49107
// Control flow statement
50-
sb.WriteString(c.RawCode)
108+
sb.WriteString(formatRawCode(ctx, c.RawCode, c.Arguments))
51109
sb.WriteString(" {\n")
52110
sb.WriteString(ctx.indent(formatStatements(ctx, c.Statements)))
53111
sb.WriteString("}")
@@ -57,7 +115,7 @@ func (c *Code) Format(ctx *Context) string {
57115

58116
if c.RawCode != "" && !c.IsFlow {
59117
// Simple statement
60-
sb.WriteString(c.RawCode)
118+
sb.WriteString(formatRawCode(ctx, c.RawCode, c.Arguments))
61119
if !strings.HasSuffix(c.RawCode, ";") {
62120
sb.WriteRune(';')
63121
}

poet/code_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package poet
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestCodeBuilder_StringFormatting(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
code string
13+
expected string
14+
args []any
15+
}{
16+
{
17+
name: "simple replacement",
18+
code: "$T $L = $S",
19+
expected: `String test = "a value"`,
20+
args: []any{String, "test", "a value"},
21+
},
22+
{
23+
name: "complex replacement",
24+
code: "$T $3L = $2S + $3S",
25+
expected: `String test = "a value" + "test"`,
26+
args: []any{String, "a value", "test"},
27+
},
28+
{
29+
name: "nil values",
30+
code: "$1L $1S",
31+
expected: `null "null"`,
32+
args: []any{nil},
33+
},
34+
{
35+
name: "bool values",
36+
code: "$1L $1S",
37+
expected: `true "true"`,
38+
args: []any{true},
39+
},
40+
{
41+
name: "int values",
42+
code: "$1L $1S",
43+
expected: `1 "1"`,
44+
args: []any{1},
45+
},
46+
{
47+
name: "float values",
48+
code: "$1L $1S",
49+
expected: `1.1 "1.1"`,
50+
args: []any{1.1},
51+
},
52+
{
53+
name: "string values",
54+
code: "$1L $1S",
55+
expected: `hello "hello"`,
56+
args: []any{"hello"},
57+
},
58+
{
59+
name: "quoted strings",
60+
code: "$1L $1S",
61+
expected: `"hello" "\"hello\""`,
62+
args: []any{`"hello"`},
63+
},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
t.Parallel()
69+
70+
assert := assert.New(t)
71+
72+
ctx := NewContext("io.github.tandemdude")
73+
74+
code := NewCodeBuilder().
75+
AddStatement(tt.code, tt.args...).
76+
Build()
77+
78+
assert.Equal(tt.expected+";\n", code.Format(ctx))
79+
})
80+
}
81+
}
82+
83+
func TestCodeBuilder_MultipleStatements(t *testing.T) {
84+
assert := assert.New(t)
85+
86+
ctx := NewContext("io.github.tandemdude")
87+
88+
code := NewCodeBuilder().
89+
AddStatement("$L $S", false, true).
90+
AddStatement("$L $S", true, false).
91+
Build()
92+
93+
assert.Equal("false \"true\";\ntrue \"false\";\n", code.Format(ctx))
94+
}
95+
96+
func TestCodeBuilder_NoStatements(t *testing.T) {
97+
assert := assert.New(t)
98+
99+
ctx := NewContext("io.github.tandemdude")
100+
101+
code := NewCodeBuilder().Build()
102+
103+
assert.Equal("", code.Format(ctx))
104+
}

0 commit comments

Comments
 (0)