@@ -17,17 +17,21 @@ import (
17
17
18
18
"github.com/claceio/clace/internal/app/apptype"
19
19
"github.com/claceio/clace/internal/app/starlark_type"
20
+ "github.com/claceio/clace/internal/types"
20
21
"github.com/go-chi/chi"
21
22
"go.starlark.net/starlark"
23
+ "go.starlark.net/starlarkstruct"
22
24
)
23
25
24
- //go:embed *.go.html static /*
26
+ //go:embed *.go.html astatic /*
25
27
var embedHtml embed.FS
26
28
27
29
// Action represents a single action that is exposed by the App. Actions
28
30
// provide a way to trigger app operations, with an auto-generated form UI
29
31
// and an API interface
30
32
type Action struct {
33
+ * types.Logger
34
+ isDev bool
31
35
name string
32
36
description string
33
37
path string
@@ -41,7 +45,7 @@ type Action struct {
41
45
}
42
46
43
47
// NewAction creates a new action
44
- func NewAction (name , description , apath string , run , suggest starlark.Callable ,
48
+ func NewAction (logger * types. Logger , isDev bool , name , description , apath string , run , suggest starlark.Callable ,
45
49
params []apptype.AppParam , paramValuesStr map [string ]string , paramDict starlark.StringDict , appPath string ) (* Action , error ) {
46
50
tmpl , err := template .New ("form" ).ParseFS (embedHtml , "*.go.html" )
47
51
if err != nil {
@@ -52,7 +56,16 @@ func NewAction(name, description, apath string, run, suggest starlark.Callable,
52
56
return a .Index - b .Index
53
57
})
54
58
59
+ subLogger := logger .With ().Str ("action" , name ).Logger ()
60
+ appLogger := types.Logger {Logger : & subLogger }
61
+
62
+ pagePath := path .Join (appPath , apath )
63
+ if pagePath == "/" {
64
+ pagePath = ""
65
+ }
66
+
55
67
return & Action {
68
+ Logger : & appLogger ,
56
69
name : name ,
57
70
description : description ,
58
71
path : apath ,
@@ -62,28 +75,212 @@ func NewAction(name, description, apath string, run, suggest starlark.Callable,
62
75
paramValuesStr : paramValuesStr ,
63
76
paramDict : paramDict ,
64
77
template : tmpl ,
65
- pagePath : path . Join ( appPath , apath ) ,
78
+ pagePath : pagePath ,
66
79
}, nil
67
80
}
68
81
69
82
func (a * Action ) BuildRouter () (* chi.Mux , error ) {
70
- fSys , err := fs .Sub (embedHtml , "static " )
83
+ fSys , err := fs .Sub (embedHtml , "astatic " )
71
84
if err != nil {
72
85
return nil , err
73
86
}
74
87
staticServer := http .FileServer (http .FS (fSys ))
75
88
76
89
r := chi .NewRouter ()
77
- r .Post ("/" , a .runHandler )
90
+ r .Post ("/" , a .runAction )
78
91
r .Get ("/" , a .getForm )
79
- r .Handle ("/static /*" , http .StripPrefix (path .Join (a .pagePath , "/static /" ), staticServer ))
92
+ r .Handle ("/astatic /*" , http .StripPrefix (path .Join (a .pagePath , "/astatic /" ), staticServer ))
80
93
return r , nil
81
94
}
82
95
83
- func (a * Action ) runHandler (w http.ResponseWriter , r * http.Request ) {
84
- // Parse the form
85
- // Validate the form
86
- // Run the action
96
+ func (a * Action ) runAction (w http.ResponseWriter , r * http.Request ) {
97
+ thread := & starlark.Thread {
98
+ Name : a .name ,
99
+ Print : func (_ * starlark.Thread , msg string ) { fmt .Println (msg ) },
100
+ }
101
+
102
+ // Save the request context in the starlark thread local
103
+ thread .SetLocal (types .TL_CONTEXT , r .Context ())
104
+ //isHtmxRequest := r.Header.Get("HX-Request") == "true" && !(r.Header.Get("HX-Boosted") == "true")
105
+
106
+ r .ParseForm ()
107
+ var err error
108
+ dryRun := false
109
+ dryRunStr := r .Form .Get ("dry-run" )
110
+ if dryRunStr != "" {
111
+ dryRun , err = strconv .ParseBool (dryRunStr )
112
+ if err != nil {
113
+ http .Error (w , fmt .Sprintf ("invalid value for dry-run: %s" , dryRunStr ), http .StatusBadRequest )
114
+ return
115
+ }
116
+ }
117
+
118
+ deferredCleanup := func () error {
119
+ // Check for any deferred cleanups
120
+ err = RunDeferredCleanup (thread )
121
+ if err != nil {
122
+ a .Error ().Err (err ).Msg ("error cleaning up plugins" )
123
+ http .Error (w , err .Error (), http .StatusInternalServerError )
124
+ return err
125
+ }
126
+ return nil
127
+ }
128
+
129
+ defer deferredCleanup ()
130
+
131
+ args := starlark.StringDict {}
132
+ // Make a copy of the app level param dict
133
+ for k , v := range a .paramDict {
134
+ args [k ] = v
135
+ }
136
+
137
+ // Update args with submitted form values
138
+ for _ , param := range a .params {
139
+ formValue := r .Form .Get (param .Name )
140
+ if formValue == "" {
141
+ if param .Type == starlark_type .BOOLEAN {
142
+ // Form does not submit unchecked checkboxes, set to false
143
+ args [param .Name ] = starlark .Bool (false )
144
+ }
145
+ } else {
146
+ newVal , err := apptype .ParamStringToType (param .Name , param .Type , formValue )
147
+ if err != nil {
148
+ http .Error (w , err .Error (), http .StatusBadRequest )
149
+ return
150
+ }
151
+ args [param .Name ] = newVal
152
+ }
153
+ }
154
+
155
+ argsValue := Args {members : args }
156
+
157
+ // Call the handler function
158
+ var ret starlark.Value
159
+ ret , err = starlark .Call (thread , a .run , starlark.Tuple {starlark .Bool (dryRun ), & argsValue }, nil )
160
+ if err != nil {
161
+ a .Error ().Err (err ).Msg ("error calling action run handler" )
162
+
163
+ firstFrame := ""
164
+ if evalErr , ok := err .(* starlark.EvalError ); ok {
165
+ // Iterate through the CallFrame stack for debugging information
166
+ for i , frame := range evalErr .CallStack {
167
+ a .Warn ().Msgf ("Function: %s, Position: %s\n " , frame .Name , frame .Pos )
168
+ if i == 0 {
169
+ firstFrame = fmt .Sprintf ("Function %s, Position %s" , frame .Name , frame .Pos )
170
+ }
171
+ }
172
+ }
173
+
174
+ msg := err .Error ()
175
+ if firstFrame != "" && a .isDev {
176
+ msg = msg + " : " + firstFrame
177
+ }
178
+
179
+ // No err handler defined, abort
180
+ http .Error (w , msg , http .StatusInternalServerError )
181
+ return
182
+ }
183
+
184
+ var valuesMap []map [string ]any
185
+ var valuesStr []string
186
+ var message string
187
+ var paramErrors map [string ]any
188
+
189
+ resultStruct , ok := ret .(* starlarkstruct.Struct )
190
+ if ok {
191
+ message , err = apptype .GetOptionalStringAttr (resultStruct , "message" )
192
+ if err != nil {
193
+ http .Error (w , fmt .Sprintf ("error getting result attr message: %s" , err ), http .StatusInternalServerError )
194
+ return
195
+ }
196
+
197
+ valuesMap , err = apptype .GetListMapAttr (resultStruct , "values" , true )
198
+ if err != nil {
199
+ valuesStr , err = apptype .GetListStringAttr (resultStruct , "values" , true )
200
+ if err != nil {
201
+ http .Error (w , fmt .Sprintf ("error getting result values, not a list of string or list of maps: %s" , err ), http .StatusInternalServerError )
202
+ return
203
+ }
204
+ }
205
+
206
+ paramErrors , err = apptype .GetDictAttr (resultStruct , "param_errors" , true )
207
+ if err != nil {
208
+ http .Error (w , fmt .Sprintf ("error getting result attr paramErrors: %s" , err ), http .StatusInternalServerError )
209
+ return
210
+ }
211
+ } else {
212
+ // Not a result struct
213
+ message = ret .String ()
214
+ }
215
+
216
+ a .Info ().Msgf ("action result message: %s valuesStr %s valuesMap %s paramErrors %s" , message , valuesStr , valuesMap , paramErrors )
217
+
218
+ if deferredCleanup () != nil {
219
+ return
220
+ }
221
+
222
+ if err != nil {
223
+ http .Error (w , err .Error (), http .StatusInternalServerError )
224
+ return
225
+ }
226
+
227
+ // Render the result message
228
+ err = a .template .ExecuteTemplate (w , "message" , message )
229
+ if err != nil {
230
+ http .Error (w , err .Error (), http .StatusInternalServerError )
231
+ }
232
+
233
+ // Render the param error messages if any, using HTMX OOB
234
+ for paramName , paramError := range paramErrors {
235
+ tv := struct {
236
+ Name string
237
+ Message string
238
+ }{
239
+ Name : paramName ,
240
+ Message : fmt .Sprintf ("%s" , paramError ),
241
+ }
242
+ err = a .template .ExecuteTemplate (w , "paramError" , tv )
243
+ if err != nil {
244
+ http .Error (w , err .Error (), http .StatusInternalServerError )
245
+ }
246
+ }
247
+
248
+ if len (valuesStr ) > 0 {
249
+ // Render the result values, using HTMX OOB
250
+ err = a .template .ExecuteTemplate (w , "result-textarea" , valuesStr )
251
+ if err != nil {
252
+ http .Error (w , err .Error (), http .StatusInternalServerError )
253
+ }
254
+ }
255
+
256
+ }
257
+
258
+ func RunDeferredCleanup (thread * starlark.Thread ) error {
259
+ deferMap := thread .Local (types .TL_DEFER_MAP )
260
+ if deferMap == nil {
261
+ return nil
262
+ }
263
+
264
+ strictFailures := []string {}
265
+ for pluginName , pluginMap := range deferMap .(map [string ]map [string ]apptype.DeferEntry ) {
266
+ for key , entry := range pluginMap {
267
+ err := entry .Func ()
268
+ if err != nil {
269
+ fmt .Printf ("error cleaning up %s %s: %s\n " , pluginName , key , err )
270
+ }
271
+ if entry .Strict {
272
+ strictFailures = append (strictFailures , fmt .Sprintf ("%s:%s" , pluginName , key ))
273
+ }
274
+ }
275
+ }
276
+
277
+ thread .SetLocal (types .TL_DEFER_MAP , nil ) // reset the defer map
278
+
279
+ if len (strictFailures ) > 0 {
280
+ return fmt .Errorf ("resource has not be closed, check handler code: %s" , strings .Join (strictFailures , ", " ))
281
+ }
282
+
283
+ return nil
87
284
}
88
285
89
286
type ParamDef struct {
0 commit comments