@@ -6,31 +6,30 @@ import (
6
6
"fmt"
7
7
"github.com/klothoplatform/klotho/pkg/graph"
8
8
"github.com/klothoplatform/klotho/pkg/multierr"
9
- "github.com/mitchellh/hashstructure/v2"
10
9
"github.com/pkg/errors"
11
10
"io"
12
11
"io/fs"
13
12
"reflect"
14
13
"regexp"
15
14
"sort"
16
- "strings"
17
15
)
18
16
19
17
type (
20
18
varNamer interface {
21
19
VariableName () string
22
20
}
23
21
24
- resource struct {
25
- hash string
26
- element any
27
- }
28
-
29
22
templatesCompiler struct {
30
- templates fs.FS
31
- resourceGraph * graph.Directed [resource ]
32
- resources map [any ]resource
23
+ // templates is the fs.FS where we read all of our `<struct>/factory.ts` files
24
+ templates fs.FS
25
+ // resourceGraph is the graph of resources to render
26
+ resourceGraph * graph.Directed [graph.Identifiable ]
27
+ // templatesByStructName is a cache from struct name (e.g. "CloudwatchLogs") to the template for that struct.
33
28
templatesByStructName map [string ]ResourceCreationTemplate
29
+ // resourceVarNames is a set of all variable names
30
+ resourceVarNames map [string ]struct {}
31
+ // resourceVarNamesById is a map from resource id to the variable name for that resource
32
+ resourceVarNamesById map [string ]string
34
33
}
35
34
)
36
35
@@ -41,69 +40,52 @@ var (
41
40
nonIdentifierChars = regexp .MustCompile (`\W` )
42
41
)
43
42
44
- func CreateTemplatesCompiler () * templatesCompiler {
43
+ func CreateTemplatesCompiler (resources * graph. Directed [graph. Identifiable ] ) * templatesCompiler {
45
44
subTemplates , err := fs .Sub (standardTemplates , "templates" )
46
45
if err != nil {
47
46
panic (err ) // unexpected, since standardTemplates is statically built into klotho
48
47
}
49
48
return & templatesCompiler {
50
49
templates : subTemplates ,
51
- resourceGraph : graph .NewDirected [resource ](),
52
- resources : make (map [any ]resource ),
50
+ resourceGraph : resources ,
53
51
templatesByStructName : make (map [string ]ResourceCreationTemplate ),
52
+ resourceVarNames : make (map [string ]struct {}),
53
+ resourceVarNamesById : make (map [string ]string ),
54
54
}
55
55
}
56
56
57
- func (tc templatesCompiler ) AddResource (v any ) {
58
- if _ , exists := tc .resources [v ]; exists {
59
- return
60
-
61
- }
62
- res := resource {
63
- hash : fmt .Sprintf (`%x` , len (tc .resources )),
64
- element : v ,
65
- }
66
- tc .resources [v ] = res
67
- tc .resourceGraph .AddVertex (res )
68
- for _ , child := range getStructValues (v ) {
69
- if reflect .TypeOf (child ).Kind () == reflect .Struct {
70
- tc .AddResource (child )
71
- childRes := tc .getResource (child )
72
- tc .resourceGraph .AddEdge (res .Id (), childRes .Id ())
73
- }
74
- }
75
- }
76
-
77
- func (tc templatesCompiler ) getResource (v any ) resource {
78
- childRes , childExists := tc .resources [v ]
79
- if ! childExists {
80
- panic (fmt .Sprintf (`compiler has inconsistent state: no resource for %v` , v ))
81
- }
82
- return childRes
83
- }
84
-
85
57
func (tc templatesCompiler ) RenderBody (out io.Writer ) error {
86
- // TODO: for now, assume a nice little tree
87
- // TODO: need a stable sorting of outputs!
88
-
89
58
errs := multierr.Error {}
90
- for _ , res := range tc .resourceGraph .Roots () {
91
- err := tc .renderResource (out , res .element )
59
+ vertexIds , err := tc .resourceGraph .VertexIdsInTopologicalOrder ()
60
+ if err != nil {
61
+ return err
62
+ }
63
+ reverseInPlace (vertexIds )
64
+ for _ , id := range vertexIds {
65
+ resource := tc .resourceGraph .GetVertex (id )
66
+ err := tc .renderResource (out , resource )
92
67
errs .Append (err )
93
68
}
94
69
return errs .ErrOrNil ()
95
70
}
96
71
97
72
func (tc templatesCompiler ) RenderImports (out io.Writer ) error {
98
- // TODO: for now, assume a nice little tree
73
+ errs := multierr. Error {}
99
74
100
75
allImports := make (map [string ]struct {})
101
- for _ , res := range tc .resources {
102
- tmpl := tc .GetTemplate (res .element )
76
+ for _ , res := range tc .resourceGraph .GetAllVertices () {
77
+ tmpl , err := tc .GetTemplate (res )
78
+ if err != nil {
79
+ errs .Append (err )
80
+ continue
81
+ }
103
82
for statement , _ := range tmpl .imports {
104
83
allImports [statement ] = struct {}{}
105
84
}
106
85
}
86
+ if err := errs .ErrOrNil (); err != nil {
87
+ return err
88
+ }
107
89
108
90
sortedImports := make ([]string , 0 , len (allImports ))
109
91
for statement , _ := range allImports {
@@ -123,67 +105,81 @@ func (tc templatesCompiler) RenderImports(out io.Writer) error {
123
105
return nil
124
106
}
125
107
126
- func (tc templatesCompiler ) renderResource (out io.Writer , res any ) error {
108
+ func (tc templatesCompiler ) renderResource (out io.Writer , resource graph. Identifiable ) error {
127
109
// TODO: for now, assume a nice little tree
128
110
errs := multierr.Error {}
129
111
130
112
inputArgs := make (map [string ]string )
131
- for fieldName , child := range getStructValues (res ) {
132
- childType := reflect .TypeOf (child ) // todo cache in the resource?
113
+ for fieldName , child := range getStructValues (resource ) {
114
+ childType := reflect .TypeOf (child )
133
115
switch childType .Kind () {
134
116
case reflect .String :
135
117
inputArgs [fieldName ] = quoteTsString (child .(string ))
136
- case reflect .Struct :
137
- errs .Append (tc .renderResource (out , child ))
138
- inputArgs [fieldName ] = tc .getResource (child ).VariableName ()
118
+ case reflect .Struct , reflect .Pointer :
119
+ if child , ok := child .(graph.Identifiable ); ok {
120
+ inputArgs [fieldName ] = tc .getVarName (child )
121
+ } else {
122
+ errs .Append (errors .Errorf (`child struct of %v is not of a known type: %v` , resource , child ))
123
+ }
139
124
default :
140
- errs .Append (errors .Errorf (`unrecognized input type for %v [%s]: %v` , res , fieldName , child ))
125
+ errs .Append (errors .Errorf (`unrecognized input type for %v [%s]: %v` , resource , fieldName , child ))
141
126
}
142
127
}
143
128
144
- varName := tc .getResource (res ).VariableName ()
129
+ varName := tc .getVarName (resource )
130
+ tmpl , err := tc .GetTemplate (resource )
131
+ if err != nil {
132
+ return err
133
+ }
134
+
145
135
fmt .Fprintf (out , `const %s = ` , varName )
146
- errs .Append (tc . GetTemplate ( res ) .RenderCreate (out , inputArgs ))
136
+ errs .Append (tmpl .RenderCreate (out , inputArgs ))
147
137
out .Write ([]byte (";\n " ))
148
138
149
139
return errs .ErrOrNil ()
140
+ }
150
141
142
+ // getVarName gets a unique but nice-looking variable for the given item.
143
+ //
144
+ // It does this by first calculating an ideal variable name, which is a camel-cased ${structName}${Id}. For example, if
145
+ // you had an object CoolResource{id: "foo-bar"}, the ideal variable name is coolResourceFooBar.
146
+ //
147
+ // If that ideal variable name hasn't been used yet, this function returns it. If it has been used, we append `_${i}` to
148
+ // it, where ${i} is the lowest positive integer that would give us a new, unique variable name. This isn't expected
149
+ // to happen often, if at all, since ids are globally unique.
150
+ func (tc templatesCompiler ) getVarName (v graph.Identifiable ) string {
151
+ if name , alreadyResolved := tc .resourceVarNamesById [v .Id ()]; alreadyResolved {
152
+ return name
153
+ }
154
+
155
+ // Generate something like "lambdaFoo", where Lambda is the name of the struct and "foo" is the id
156
+ desiredName := lowercaseFirst (structName (v )) + toUpperCamel (v .Id ())
157
+ resolvedName := desiredName
158
+ for i := 1 ; ; i ++ {
159
+ _ , varNameTaken := tc .resourceVarNames [resolvedName ]
160
+ if varNameTaken {
161
+ resolvedName = fmt .Sprintf ("%s_%d" , desiredName , i )
162
+ } else {
163
+ break
164
+ }
165
+ }
166
+ tc .resourceVarNames [resolvedName ] = struct {}{}
167
+ tc .resourceVarNamesById [v .Id ()] = resolvedName
168
+ return resolvedName
151
169
}
152
170
153
- func (tc templatesCompiler ) GetTemplate (v any ) ResourceCreationTemplate {
154
- // TODO cache into the resource
155
- vType := reflect .TypeOf (v )
156
- typeName := vType .Name ()
171
+ func (tc templatesCompiler ) GetTemplate (v graph.Identifiable ) (ResourceCreationTemplate , error ) {
172
+ typeName := structName (v )
157
173
existing , ok := tc .templatesByStructName [typeName ]
158
174
if ok {
159
- return existing
175
+ return existing , nil
160
176
}
161
177
templateName := camelToSnake (typeName )
162
178
contents , err := fs .ReadFile (tc .templates , templateName + `/factory.ts` )
163
179
if err != nil {
164
- // Shouldn't ever happen; would mean an error in how we set up our structs
165
- panic (err )
180
+ return ResourceCreationTemplate {}, err
166
181
}
167
182
template := ParseResourceCreationTemplate (contents )
168
183
tc .templatesByStructName [typeName ] = template
169
- return template
170
- }
171
-
172
- func (r resource ) Id () string {
173
- if r .hash == "" {
174
- h , err := hashstructure .Hash (r .element , hashstructure .FormatV2 , nil )
175
- if err != nil {
176
- // Shouldn't ever happen; would mean an error in how we set up our structs
177
- panic (err )
178
- }
179
- r .hash = fmt .Sprintf ("%x" , h )
180
- }
181
- return r .hash
182
- }
183
-
184
- func (r resource ) VariableName () string {
185
- name := fmt .Sprintf (`%s_%s` , reflect .TypeOf (r .element ).Name (), r .Id ())
186
- firstChar := name [:1 ]
187
- rest := name [1 :]
188
- return strings .ToLower (firstChar ) + rest
184
+ return template , nil
189
185
}
0 commit comments