Skip to content

Commit 28234e2

Browse files
authored
Refactor the notifications schema definition (#1077)
* applet: Expose Starlark globals and main file for embedders * Refactor the notifications schema definition Notifications are still a work in progress. With this change, each notification needs to specify a `builder` function.
1 parent 515acac commit 28234e2

File tree

7 files changed

+89
-55
lines changed

7 files changed

+89
-55
lines changed

runtime/applet.go

+46-30
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,14 @@ type AppletOption func(*Applet) error
5454
type ThreadInitializer func(thread *starlark.Thread) *starlark.Thread
5555

5656
type Applet struct {
57-
ID string
57+
ID string
58+
Globals map[string]starlark.StringDict
59+
MainFile string
5860

5961
loader ModuleLoader
6062
initializers []ThreadInitializer
6163
loadedPaths map[string]bool
6264

63-
globals map[string]starlark.StringDict
64-
65-
mainFile string
6665
mainFun *starlark.Function
6766
schemaFile string
6867

@@ -130,7 +129,7 @@ func NewApplet(id string, src []byte, opts ...AppletOption) (*Applet, error) {
130129
func NewAppletFromFS(id string, fsys fs.FS, opts ...AppletOption) (*Applet, error) {
131130
a := &Applet{
132131
ID: id,
133-
globals: make(map[string]starlark.StringDict),
132+
Globals: make(map[string]starlark.StringDict),
134133
loadedPaths: make(map[string]bool),
135134
}
136135

@@ -153,23 +152,18 @@ func (a *Applet) Run(ctx context.Context) (roots []render.Root, err error) {
153152
return a.RunWithConfig(ctx, nil)
154153
}
155154

156-
// RunWithConfig exceutes the applet's main function, passing it configuration as a
157-
// starlark dict. It returns the render roots that are returned by the applet.
158-
func (a *Applet) RunWithConfig(ctx context.Context, config map[string]string) (roots []render.Root, err error) {
159-
var args starlark.Tuple
160-
if a.mainFun.NumParams() > 0 {
161-
starlarkConfig := AppletConfig(config)
162-
args = starlark.Tuple{starlarkConfig}
163-
}
164-
165-
returnValue, err := a.Call(ctx, a.mainFun, args...)
166-
if err != nil {
167-
return nil, err
168-
}
155+
// ExtractRoots extracts render roots from a Starlark value. It expects the value
156+
// to be either a single render root or a list of render roots.
157+
//
158+
// It's used internally by RunWithConfig to extract the roots returned by the applet.
159+
func ExtractRoots(val starlark.Value) ([]render.Root, error) {
160+
var roots []render.Root
169161

170-
if returnRoot, ok := returnValue.(render_runtime.Rootable); ok {
162+
if val == starlark.None {
163+
// no roots returned
164+
} else if returnRoot, ok := val.(render_runtime.Rootable); ok {
171165
roots = []render.Root{returnRoot.AsRenderRoot()}
172-
} else if returnList, ok := returnValue.(*starlark.List); ok {
166+
} else if returnList, ok := val.(*starlark.List); ok {
173167
roots = make([]render.Root, returnList.Len())
174168
iter := returnList.Iterate()
175169
defer iter.Done()
@@ -188,7 +182,29 @@ func (a *Applet) RunWithConfig(ctx context.Context, config map[string]string) (r
188182
i++
189183
}
190184
} else {
191-
return nil, fmt.Errorf("expected app implementation to return Root(s) but found: %s", returnValue.Type())
185+
return nil, fmt.Errorf("expected app implementation to return Root(s) but found: %s", val.Type())
186+
}
187+
188+
return roots, nil
189+
}
190+
191+
// RunWithConfig exceutes the applet's main function, passing it configuration as a
192+
// starlark dict. It returns the render roots that are returned by the applet.
193+
func (a *Applet) RunWithConfig(ctx context.Context, config map[string]string) (roots []render.Root, err error) {
194+
var args starlark.Tuple
195+
if a.mainFun.NumParams() > 0 {
196+
starlarkConfig := AppletConfig(config)
197+
args = starlark.Tuple{starlarkConfig}
198+
}
199+
200+
returnValue, err := a.Call(ctx, a.mainFun, args...)
201+
if err != nil {
202+
return nil, err
203+
}
204+
205+
roots, err = ExtractRoots(returnValue)
206+
if err != nil {
207+
return nil, err
192208
}
193209

194210
return roots, nil
@@ -220,7 +236,7 @@ func (app *Applet) CallSchemaHandler(ctx context.Context, handlerName, parameter
220236
return options, nil
221237

222238
case schema.ReturnSchema:
223-
sch, err := schema.FromStarlark(resultVal, app.globals[app.schemaFile])
239+
sch, err := schema.FromStarlark(resultVal, app.Globals[app.schemaFile])
224240
if err != nil {
225241
return "", err
226242
}
@@ -253,7 +269,7 @@ func (app *Applet) RunTests(t *testing.T) {
253269
return thread
254270
})
255271

256-
for file, globals := range app.globals {
272+
for file, globals := range app.Globals {
257273
for name, global := range globals {
258274
if !strings.HasPrefix(name, "test_") {
259275
continue
@@ -347,7 +363,7 @@ func (a *Applet) ensureLoaded(fsys fs.FS, pathToLoad string, currentlyLoading ..
347363

348364
// normalize path so that it can be used as a key
349365
pathToLoad = path.Clean(pathToLoad)
350-
if _, ok := a.globals[pathToLoad]; ok {
366+
if _, ok := a.Globals[pathToLoad]; ok {
351367
// already loaded, good to go
352368
return nil
353369
}
@@ -390,7 +406,7 @@ func (a *Applet) ensureLoaded(fsys fs.FS, pathToLoad string, currentlyLoading ..
390406
return nil, err
391407
}
392408

393-
if g, ok := a.globals[modulePath]; !ok {
409+
if g, ok := a.Globals[modulePath]; !ok {
394410
return nil, fmt.Errorf("module %s not loaded", modulePath)
395411
} else {
396412
return g, nil
@@ -416,17 +432,17 @@ func (a *Applet) ensureLoaded(fsys fs.FS, pathToLoad string, currentlyLoading ..
416432
if err != nil {
417433
return fmt.Errorf("starlark.ExecFile: %v", err)
418434
}
419-
a.globals[pathToLoad] = globals
435+
a.Globals[pathToLoad] = globals
420436

421437
// if the file is in the root directory, check for the main function
422438
// and schema function
423439
mainFun, _ := globals["main"].(*starlark.Function)
424440
if mainFun != nil {
425-
if a.mainFile != "" {
426-
return fmt.Errorf("multiple files with a main() function:\n- %s\n- %s", pathToLoad, a.mainFile)
441+
if a.MainFile != "" {
442+
return fmt.Errorf("multiple files with a main() function:\n- %s\n- %s", pathToLoad, a.MainFile)
427443
}
428444

429-
a.mainFile = pathToLoad
445+
a.MainFile = pathToLoad
430446
a.mainFun = mainFun
431447
}
432448

@@ -454,7 +470,7 @@ func (a *Applet) ensureLoaded(fsys fs.FS, pathToLoad string, currentlyLoading ..
454470
}
455471

456472
default:
457-
a.globals[pathToLoad] = starlark.StringDict{
473+
a.Globals[pathToLoad] = starlark.StringDict{
458474
"file": &file.File{
459475
FS: fsys,
460476
Path: pathToLoad,

runtime/render_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def main():
161161
app, err := NewApplet(filename, []byte(src))
162162
assert.NoError(t, err)
163163

164-
b := app.globals["test_box.star"]["b"]
164+
b := app.Globals["test_box.star"]["b"]
165165
assert.IsType(t, &render_runtime.Box{}, b)
166166

167167
widget := b.(*render_runtime.Box).AsRenderWidget()
@@ -196,7 +196,7 @@ def main():
196196
app, err := NewApplet(filename, []byte(src))
197197
assert.NoError(t, err)
198198

199-
txt := app.globals["test_text.star"]["t"]
199+
txt := app.Globals["test_text.star"]["t"]
200200
assert.IsType(t, &render_runtime.Text{}, txt)
201201

202202
widget := txt.(*render_runtime.Text).AsRenderWidget()
@@ -240,7 +240,7 @@ def main():
240240
app, err := NewApplet(filename, []byte(src))
241241
assert.NoError(t, err)
242242

243-
starlarkP := app.globals["test_png.star"]["img"]
243+
starlarkP := app.Globals["test_png.star"]["img"]
244244
require.IsType(t, &render_runtime.Image{}, starlarkP)
245245

246246
actualIm := render.PaintWidget(starlarkP.(*render_runtime.Image).AsRenderWidget(), image.Rect(0, 0, 64, 32), 0)

schema/module.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ func newSchema(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple
170170
)
171171
}
172172

173-
s.Schema.Notifications = append(s.Schema.Notifications, n.AsSchemaField())
173+
s.Schema.Notifications = append(s.Schema.Notifications, *n)
174174
}
175175
}
176176

schema/notification.go

+13-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
type Notification struct {
1111
SchemaField
12+
Builder *starlark.Function `json:"-"`
1213
starlarkSounds *starlark.List
1314
}
1415

@@ -19,11 +20,12 @@ func newNotification(
1920
kwargs []starlark.Tuple,
2021
) (starlark.Value, error) {
2122
var (
22-
id starlark.String
23-
name starlark.String
24-
desc starlark.String
25-
icon starlark.String
26-
sounds *starlark.List
23+
id starlark.String
24+
name starlark.String
25+
desc starlark.String
26+
icon starlark.String
27+
sounds *starlark.List
28+
builder *starlark.Function
2729
)
2830

2931
if err := starlark.UnpackArgs(
@@ -34,6 +36,7 @@ func newNotification(
3436
"desc", &desc,
3537
"icon", &icon,
3638
"sounds", &sounds,
39+
"builder", &builder,
3740
); err != nil {
3841
return nil, fmt.Errorf("unpacking arguments for Notification: %s", err)
3942
}
@@ -44,6 +47,7 @@ func newNotification(
4447
s.Name = name.GoString()
4548
s.Description = desc.GoString()
4649
s.Icon = icon.GoString()
50+
s.Builder = builder
4751

4852
var soundVal starlark.Value
4953
soundIter := sounds.Iterate()
@@ -75,7 +79,7 @@ func (s *Notification) AsSchemaField() SchemaField {
7579

7680
func (s *Notification) AttrNames() []string {
7781
return []string{
78-
"id", "name", "desc", "icon", "sounds",
82+
"id", "name", "desc", "icon", "sounds", "builder",
7983
}
8084
}
8185

@@ -97,6 +101,9 @@ func (s *Notification) Attr(name string) (starlark.Value, error) {
97101
case "sounds":
98102
return s.starlarkSounds, nil
99103

104+
case "builder":
105+
return s.Builder, nil
106+
100107
default:
101108
return nil, nil
102109
}

schema/notification_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing/fstest"
77

88
"github.com/stretchr/testify/assert"
9+
910
"tidbyt.dev/pixlet/runtime"
1011
)
1112

@@ -29,6 +30,7 @@ s = schema.Notification(
2930
desc = "A new message has arrived",
3031
icon = "message",
3132
sounds = sounds,
33+
builder = lambda: None,
3234
)
3335
3436
assert.eq(s.id, "notification1")

schema/schema.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ const (
2626
// Schema holds a configuration object for an applet. It holds a list of fields
2727
// that are exported from an applet.
2828
type Schema struct {
29-
Version string `json:"version" validate:"required"`
30-
Fields []SchemaField `json:"schema" validate:"dive"`
31-
Notifications []SchemaField `json:"notifications,omitempty" validate:"dive"`
29+
Version string `json:"version" validate:"required"`
30+
Fields []SchemaField `json:"schema" validate:"dive"`
31+
Notifications []Notification `json:"notifications,omitempty" validate:"dive"`
3232

3333
Handlers map[string]SchemaHandler `json:"-"`
3434
}
@@ -107,7 +107,7 @@ func (s Schema) MarshalJSON() ([]byte, error) {
107107
a.Fields = make([]SchemaField, 0)
108108
}
109109
if a.Notifications == nil {
110-
a.Notifications = make([]SchemaField, 0)
110+
a.Notifications = make([]Notification, 0)
111111
}
112112

113113
js, err := json.Marshal(a)
@@ -165,6 +165,8 @@ func FromStarlark(
165165
if schemaField.StarlarkHandler != nil {
166166
handlerFun = schemaField.StarlarkHandler
167167
} else if schemaField.Handler != "" {
168+
// legacy schema, where the handler was a string instead of
169+
// a function reference
168170
handlerValue, ok := globals[schemaField.Handler]
169171
if !ok {
170172
return nil, fmt.Errorf(

schema/schema_test.go

+18-11
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def typeaheadhandler():
3939
def oauth2handler():
4040
return "a-refresh-token"
4141
42+
def build_notification():
43+
return None
44+
4245
def get_schema():
4346
return schema.Schema(
4447
version = "1",
@@ -49,6 +52,7 @@ def get_schema():
4952
name = "Notification",
5053
desc = "A Notification",
5154
icon = "notification",
55+
builder = build_notification,
5256
sounds = [
5357
schema.Sound(
5458
id = "ding",
@@ -154,18 +158,20 @@ def main():
154158
assert.Equal(t, schema.Schema{
155159
Version: "1",
156160

157-
Notifications: []schema.SchemaField{
161+
Notifications: []schema.Notification{
158162
{
159-
Type: "notification",
160-
ID: "notificationid",
161-
Name: "Notification",
162-
Description: "A Notification",
163-
Icon: "notification",
164-
Sounds: []schema.SchemaSound{
165-
{
166-
ID: "ding",
167-
Title: "Ding!",
168-
Path: "ding.mp3",
163+
SchemaField: schema.SchemaField{
164+
Type: "notification",
165+
ID: "notificationid",
166+
Name: "Notification",
167+
Description: "A Notification",
168+
Icon: "notification",
169+
Sounds: []schema.SchemaSound{
170+
{
171+
ID: "ding",
172+
Title: "Ding!",
173+
Path: "ding.mp3",
174+
},
169175
},
170176
},
171177
},
@@ -307,6 +313,7 @@ def get_schema():
307313
name = "Notification",
308314
desc = "A Notification",
309315
icon = "notification",
316+
builder = lambda: None,
310317
sounds = [
311318
schema.Sound(
312319
id = "ding",

0 commit comments

Comments
 (0)