diff --git a/cmd/community/validateicons.go b/cmd/community/validateicons.go index d693995bd2..c6916f0433 100644 --- a/cmd/community/validateicons.go +++ b/cmd/community/validateicons.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/spf13/cobra" + "tidbyt.dev/pixlet/icons" "tidbyt.dev/pixlet/runtime" "tidbyt.dev/pixlet/schema" @@ -55,12 +56,12 @@ func ValidateIcons(cmd *cobra.Command, args []string) error { } s := schema.Schema{} - schemaStr := applet.GetSchema() - if schemaStr == "" { + js := applet.SchemaJSON + if len(js) == 0 { return nil } - err = json.Unmarshal([]byte(schemaStr), &s) + err = json.Unmarshal(js, &s) if err != nil { return fmt.Errorf("failed to load schema: %w", err) } diff --git a/runtime/applet.go b/runtime/applet.go index 0c4ddddc5c..9ba34a0504 100644 --- a/runtime/applet.go +++ b/runtime/applet.go @@ -29,6 +29,7 @@ import ( "tidbyt.dev/pixlet/render" "tidbyt.dev/pixlet/runtime/modules/animation_runtime" + "tidbyt.dev/pixlet/runtime/modules/file" "tidbyt.dev/pixlet/runtime/modules/hmac" "tidbyt.dev/pixlet/runtime/modules/humanize" "tidbyt.dev/pixlet/runtime/modules/qrcode" @@ -61,12 +62,12 @@ type Applet struct { globals map[string]starlark.StringDict - mainFile string - mainFun *starlark.Function - + mainFile string + mainFun *starlark.Function schemaFile string - schema *schema.Schema - schemaJSON []byte + + Schema *schema.Schema + SchemaJSON []byte } func WithModuleLoader(loader ModuleLoader) AppletOption { @@ -189,7 +190,7 @@ func (a *Applet) RunWithConfig(ctx context.Context, config map[string]string) (r // CallSchemaHandler calls a schema handler, passing it a single // string parameter and returning a single string value. func (app *Applet) CallSchemaHandler(ctx context.Context, handlerName, parameter string) (result string, err error) { - handler, found := app.schema.Handlers[handlerName] + handler, found := app.Schema.Handlers[handlerName] if !found { return "", fmt.Errorf("no exported handler named '%s'", handlerName) } @@ -238,11 +239,6 @@ func (app *Applet) CallSchemaHandler(ctx context.Context, handlerName, parameter return "", fmt.Errorf("a very unexpected error happened for handler \"%s\"", handlerName) } -// GetSchema returns the config for the applet. -func (app *Applet) GetSchema() string { - return string(app.schemaJSON) -} - // RunTests runs all test functions that are defined in the applet source. func (app *Applet) RunTests(t *testing.T) { app.initializers = append(app.initializers, func(thread *starlark.Thread) *starlark.Thread { @@ -439,12 +435,12 @@ func (a *Applet) ensureLoaded(fsys fs.FS, pathToLoad string, currentlyLoading .. return fmt.Errorf("calling schema function for %s: %w", a.ID, err) } - a.schema, err = schema.FromStarlark(schemaVal, globals) + a.Schema, err = schema.FromStarlark(schemaVal, globals) if err != nil { return fmt.Errorf("parsing schema for %s: %w", a.ID, err) } - a.schemaJSON, err = json.Marshal(a.schema) + a.SchemaJSON, err = json.Marshal(a.Schema) if err != nil { return fmt.Errorf("serializing schema to JSON for %s: %w", a.ID, err) } @@ -452,10 +448,10 @@ func (a *Applet) ensureLoaded(fsys fs.FS, pathToLoad string, currentlyLoading .. default: a.globals[pathToLoad] = starlark.StringDict{ - "file": File{ - fsys: fsys, - path: pathToLoad, - }.Struct(), + "file": &file.File{ + FS: fsys, + Path: pathToLoad, + }, } } diff --git a/runtime/applet_test.go b/runtime/applet_test.go index bcb121f99a..a844a883f0 100644 --- a/runtime/applet_test.go +++ b/runtime/applet_test.go @@ -13,6 +13,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.starlark.net/starlark" + "tidbyt.dev/pixlet/schema" ) @@ -199,9 +200,8 @@ def get_schema(): require.NoError(t, err) require.NotNil(t, app) - jsonSchema := app.GetSchema() var s schema.Schema - json.Unmarshal([]byte(jsonSchema), &s) + json.Unmarshal(app.SchemaJSON, &s) assert.Equal(t, "1", s.Version) roots, err := app.Run(context.Background()) @@ -374,7 +374,7 @@ func TestZIPModule(t *testing.T) { // https://go.dev/src/archive/zip/example_test.go buf := new(bytes.Buffer) w := zip.NewWriter(buf) - var files = []struct { + files := []struct { Name, Body string }{ {"readme.txt", "This archive contains some text files."}, @@ -422,4 +422,35 @@ def main(config): }, printedText) } +func TestReadFile(t *testing.T) { + src := ` +load("hello.txt", hello = "file") + +def assert_eq(message, actual, expected): + if not expected == actual: + fail(message, "-", "expected", expected, "actual", actual) + +def test_readall(): + assert_eq("readall", hello.readall(), "hello world") + +def test_readall_binary(): + assert_eq("readall_binary", hello.readall("rb"), b"hello world") + +def main(): + pass + +` + + helloTxt := `hello world` + + vfs := &fstest.MapFS{ + "main.star": {Data: []byte(src)}, + "hello.txt": {Data: []byte(helloTxt)}, + } + + app, err := NewAppletFromFS("test_read_file", vfs) + require.NoError(t, err) + app.RunTests(t) +} + // TODO: test Screens, especially Screens.Render() diff --git a/runtime/file_test.go b/runtime/file_test.go deleted file mode 100644 index 4bd2bb6d1e..0000000000 --- a/runtime/file_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package runtime - -import ( - "testing" - "testing/fstest" - - "github.com/stretchr/testify/require" -) - -func TestReadFile(t *testing.T) { - src := ` -load("hello.txt", hello = "file") - -def assert_eq(message, actual, expected): - if not expected == actual: - fail(message, "-", "expected", expected, "actual", actual) - -def test_readall(): - assert_eq("readall", hello.readall(), "hello world") - -def test_readall_binary(): - assert_eq("readall_binary", hello.readall("rb"), b"hello world") - -def main(): - pass - -` - - helloTxt := `hello world` - - vfs := &fstest.MapFS{ - "main.star": {Data: []byte(src)}, - "hello.txt": {Data: []byte(helloTxt)}, - } - - app, err := NewAppletFromFS("test_read_file", vfs) - require.NoError(t, err) - app.RunTests(t) -} diff --git a/runtime/file.go b/runtime/modules/file/file.go similarity index 68% rename from runtime/file.go rename to runtime/modules/file/file.go index c06a1115b3..b02eea3e1d 100644 --- a/runtime/file.go +++ b/runtime/modules/file/file.go @@ -1,27 +1,21 @@ -package runtime +package file import ( "fmt" "io" "io/fs" + "github.com/mitchellh/hashstructure/v2" "go.starlark.net/starlark" "go.starlark.net/starlarkstruct" ) type File struct { - fsys fs.FS - path string + FS fs.FS + Path string } -func (f File) Struct() *starlarkstruct.Struct { - return starlarkstruct.FromStringDict(starlark.String("File"), starlark.StringDict{ - "path": starlark.String(f.path), - "readall": starlark.NewBuiltin("readall", f.readall), - }) -} - -func (f File) readall(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { +func (f *File) readall(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var mode starlark.String if err := starlark.UnpackArgs("readall", args, kwargs, "mode?", &mode); err != nil { return nil, err @@ -36,7 +30,7 @@ func (f File) readall(thread *starlark.Thread, _ *starlark.Builtin, args starlar return r.read(thread, nil, nil, nil) } -func (f File) reader(mode string) (*Reader, error) { +func (f *File) reader(mode string) (*Reader, error) { var binaryMode bool switch mode { case "", "r", "rt": @@ -49,13 +43,41 @@ func (f File) reader(mode string) (*Reader, error) { return nil, fmt.Errorf("unsupported mode: %s", mode) } - fl, err := f.fsys.Open(f.path) + fl, err := f.FS.Open(f.Path) if err != nil { return nil, err } return &Reader{fl, binaryMode}, nil } +func (f *File) AttrNames() []string { + return []string{"path", "readall"} +} + +func (f *File) Attr(name string) (starlark.Value, error) { + switch name { + + case "path": + return starlark.String(f.Path), nil + + case "readall": + return starlark.NewBuiltin("readall", f.readall), nil + + default: + return nil, nil + } +} + +func (f *File) String() string { return "File(...)" } +func (f *File) Type() string { return "File" } +func (f *File) Freeze() {} +func (f *File) Truth() starlark.Bool { return true } + +func (f *File) Hash() (uint32, error) { + sum, err := hashstructure.Hash(f, hashstructure.FormatV2, nil) + return uint32(sum), err +} + type Reader struct { io.ReadCloser binaryMode bool diff --git a/schema/module.go b/schema/module.go index 0d60ffb4d1..2b865dd0dd 100644 --- a/schema/module.go +++ b/schema/module.go @@ -49,6 +49,8 @@ func LoadModule() (starlark.StringDict, error) { "HandlerType": handlerType, "Generated": starlark.NewBuiltin("Generated", newGenerated), "Color": starlark.NewBuiltin("Color", newColor), + "Notification": starlark.NewBuiltin("Notification", newNotification), + "Sound": starlark.NewBuiltin("Sound", newSound), }, }, } @@ -63,24 +65,27 @@ type Field interface { type StarlarkSchema struct { Schema - Handlers map[string]SchemaHandler - starlarkFields *starlark.List - starlarkHandlers *starlark.List + Handlers map[string]SchemaHandler + starlarkFields *starlark.List + starlarkHandlers *starlark.List + starlarkNotifications *starlark.List } func newSchema(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var ( - version starlark.String - fields *starlark.List - handlers *starlark.List + version starlark.String + fields *starlark.List + handlers *starlark.List + notifications *starlark.List ) if err := starlark.UnpackArgs( "Schema", args, kwargs, "version", &version, - "fields", &fields, + "fields?", &fields, "handlers?", &handlers, + "notifications?", ¬ifications, ); err != nil { return nil, fmt.Errorf("unpacking arguments for Schema: %s", err) } @@ -93,9 +98,10 @@ func newSchema(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple Schema: Schema{ Version: version.GoString(), }, - Handlers: map[string]SchemaHandler{}, - starlarkFields: fields, - starlarkHandlers: handlers, + Handlers: map[string]SchemaHandler{}, + starlarkFields: fields, + starlarkHandlers: handlers, + starlarkNotifications: notifications, } if s.starlarkFields != nil { @@ -115,6 +121,12 @@ func newSchema(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple fieldVal.Type(), i, ) + } else if f.AsSchemaField().Type == "notification" { + return nil, fmt.Errorf( + "expected fields to be a list of Field but found: %s (at index %d)", + fieldVal.Type(), + i, + ) } s.Schema.Fields = append(s.Schema.Fields, f.AsSchemaField()) @@ -139,6 +151,29 @@ func newSchema(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple } } + if s.starlarkNotifications != nil { + notificationIter := s.starlarkNotifications.Iterate() + defer notificationIter.Done() + + var notificationVal starlark.Value + for i := 0; notificationIter.Next(¬ificationVal); { + if _, isNone := notificationVal.(starlark.NoneType); isNone { + continue + } + + n, ok := notificationVal.(*Notification) + if !ok { + return nil, fmt.Errorf( + "expected notifications to be a list of Notification but found: %s (at index %d)", + notificationVal.Type(), + i, + ) + } + + s.Schema.Notifications = append(s.Schema.Notifications, n.AsSchemaField()) + } + } + return s, nil } @@ -161,13 +196,16 @@ func (s StarlarkSchema) Attr(name string) (starlark.Value, error) { case "handlers": return s.starlarkHandlers, nil + case "notifications": + return s.starlarkNotifications, nil + default: return nil, nil } } func (s StarlarkSchema) String() string { return "Schema(...)" } -func (s StarlarkSchema) Type() string { return "StarlarkSchema" } +func (s StarlarkSchema) Type() string { return "Schema" } func (s StarlarkSchema) Freeze() {} func (s StarlarkSchema) Truth() starlark.Bool { return true } diff --git a/schema/notification.go b/schema/notification.go new file mode 100644 index 0000000000..a68e16b7ed --- /dev/null +++ b/schema/notification.go @@ -0,0 +1,113 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type Notification struct { + SchemaField + starlarkSounds *starlark.List +} + +func newNotification( + thread *starlark.Thread, + _ *starlark.Builtin, + args starlark.Tuple, + kwargs []starlark.Tuple, +) (starlark.Value, error) { + var ( + id starlark.String + name starlark.String + desc starlark.String + icon starlark.String + sounds *starlark.List + ) + + if err := starlark.UnpackArgs( + "Notification", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + "sounds", &sounds, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for Notification: %s", err) + } + + s := &Notification{} + s.SchemaField.Type = "notification" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + + var soundVal starlark.Value + soundIter := sounds.Iterate() + defer soundIter.Done() + for i := 0; soundIter.Next(&soundVal); { + if _, isNone := soundVal.(starlark.NoneType); isNone { + continue + } + + o, ok := soundVal.(*Sound) + if !ok { + return nil, fmt.Errorf( + "expected options to be a list of Sound but found: %s (at index %d)", + soundVal.Type(), + i, + ) + } + + s.Sounds = append(s.Sounds, o.AsSchemaSound()) + } + s.starlarkSounds = sounds + + return s, nil +} + +func (s *Notification) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *Notification) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", "sounds", + } +} + +func (s *Notification) Attr(name string) (starlark.Value, error) { + switch name { + + case "id": + return starlark.String(s.ID), nil + + case "name": + return starlark.String(s.Name), nil + + case "desc": + return starlark.String(s.Description), nil + + case "icon": + return starlark.String(s.Icon), nil + + case "sounds": + return s.starlarkSounds, nil + + default: + return nil, nil + } +} + +func (s *Notification) String() string { return "Notification(...)" } +func (s *Notification) Type() string { return "Notification" } +func (s *Notification) Freeze() {} +func (s *Notification) Truth() starlark.Bool { return true } + +func (s *Notification) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/notification_test.go b/schema/notification_test.go new file mode 100644 index 0000000000..4bd0b3ad2e --- /dev/null +++ b/schema/notification_test.go @@ -0,0 +1,56 @@ +package schema_test + +import ( + "context" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var notificationSource = ` +load("assert.star", "assert") +load("schema.star", "schema") +load("sound.mp3", "file") + +sounds = [ + schema.Sound( + title = "Ding!", + file = file, + ), + +] + +s = schema.Notification( + id = "notification1", + name = "New message", + desc = "A new message has arrived", + icon = "message", + sounds = sounds, +) + +assert.eq(s.id, "notification1") +assert.eq(s.name, "New message") +assert.eq(s.desc, "A new message has arrived") +assert.eq(s.icon, "message") + +assert.eq(s.sounds[0].title, "Ding!") +assert.eq(s.sounds[0].file, file) + +def main(): + return [] +` + +func TestNotification(t *testing.T) { + vfs := fstest.MapFS{ + "sound.mp3": &fstest.MapFile{Data: []byte("sound data")}, + "notification.star": &fstest.MapFile{Data: []byte(notificationSource)}, + } + app, err := runtime.NewAppletFromFS("sound", vfs) + assert.NoError(t, err) + + screens, err := app.Run(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/schema/schema.go b/schema/schema.go index dfe9caf7ee..45c5b375c4 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -26,15 +26,16 @@ const ( // Schema holds a configuration object for an applet. It holds a list of fields // that are exported from an applet. type Schema struct { - Version string `json:"version" validate:"required"` - Fields []SchemaField `json:"schema" validate:"dive"` + Version string `json:"version" validate:"required"` + Fields []SchemaField `json:"schema" validate:"dive"` + Notifications []SchemaField `json:"notifications,omitempty" validate:"dive"` Handlers map[string]SchemaHandler `json:"-"` } // SchemaField represents an item in the config used to confgure an applet. type SchemaField struct { - Type string `json:"type" validate:"required,oneof=color datetime dropdown generated location locationbased onoff radio text typeahead oauth2 oauth1 png"` + Type string `json:"type" validate:"required,oneof=color datetime dropdown generated location locationbased onoff radio text typeahead oauth2 oauth1 png notification"` ID string `json:"id" validate:"required"` Name string `json:"name,omitempty" validate:"required_for=datetime dropdown location locationbased onoff radio text typeahead png"` Description string `json:"description,omitempty"` @@ -44,6 +45,7 @@ type SchemaField struct { Default string `json:"default,omitempty" validate:"required_for=dropdown onoff radio"` Options []SchemaOption `json:"options,omitempty" validate:"required_for=dropdown radio,dive"` Palette []string `json:"palette,omitempty"` + Sounds []SchemaSound `json:"sounds,omitempty" validate:"required_for=notification,dive"` Source string `json:"source,omitempty" validate:"required_for=generated"` Handler string `json:"handler,omitempty" validate:"required_for=generated locationbased typeahead oauth2"` @@ -61,6 +63,12 @@ type SchemaOption struct { Value string `json:"value" validate:"required"` } +// SchemaSound represents a sound that can be played by the applet. +type SchemaSound struct { + Title string `json:"title" validate:"required"` + Path string `json:"path" validate:"required"` +} + // SchemaVisibility enables conditional fields inside of the mobile app. For // example, if a field should be invisible until a login is provided. type SchemaVisibility struct { @@ -96,6 +104,9 @@ func (s Schema) MarshalJSON() ([]byte, error) { if a.Fields == nil { a.Fields = make([]SchemaField, 0) } + if a.Notifications == nil { + a.Notifications = make([]SchemaField, 0) + } js, err := json.Marshal(a) @@ -105,7 +116,8 @@ func (s Schema) MarshalJSON() ([]byte, error) { // FromStarlark creates a new Schema from a Starlark schema object. func FromStarlark( val starlark.Value, - globals starlark.StringDict) (*Schema, error) { + globals starlark.StringDict, +) (*Schema, error) { var schema *Schema starlarkSchema, ok := val.(*StarlarkSchema) @@ -186,8 +198,8 @@ func FromStarlark( // Encodes a list of schema options into validated json. func EncodeOptions( - starlarkOptions starlark.Value) (string, error) { - + starlarkOptions starlark.Value, +) (string, error) { optionsTree, err := unmarshalStarlark(starlarkOptions) if err != nil { return "", err @@ -317,7 +329,6 @@ func buildOptions(options interface{}) ([]SchemaOption, error) { // Validates a Schema object. func validateSchema(schema *Schema) error { - // This custom validator function implements // "required_for", which makes the tagged field required // whenever SchemaField.Type matches one of the parameters. diff --git a/schema/schema_test.go b/schema/schema_test.go index a356e6cc7f..2ae18790f7 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -4,20 +4,27 @@ import ( "context" "encoding/json" "testing" + "testing/fstest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "tidbyt.dev/pixlet/runtime" "tidbyt.dev/pixlet/schema" ) func loadApp(code string) (*runtime.Applet, error) { - return runtime.NewApplet("test.star", []byte(code)) + vfs := fstest.MapFS{ + "test.star": &fstest.MapFile{Data: []byte(code)}, + "ding.mp3": &fstest.MapFile{Data: []byte("ding data")}, + } + return runtime.NewAppletFromFS("test", vfs) } func TestSchemaAllTypesSuccess(t *testing.T) { code := ` load("schema.star", "schema") +load("ding.mp3", ding = "file") # these won't be called unless GetSchemaHandler() is def locationbasedhandler(): @@ -35,6 +42,22 @@ def oauth2handler(): def get_schema(): return schema.Schema( version = "1", + + notifications = [ + schema.Notification( + id = "notificationid", + name = "Notification", + desc = "A Notification", + icon = "notification", + sounds = [ + schema.Sound( + title = "Ding!", + file = ding, + ), + ], + ), + ], + fields = [ schema.Location( id = "locationid", @@ -124,13 +147,28 @@ def main(): app, err := loadApp(code) assert.NoError(t, err) - jsonSchema := app.GetSchema() - var s schema.Schema - json.Unmarshal([]byte(jsonSchema), &s) + json.Unmarshal(app.SchemaJSON, &s) assert.Equal(t, schema.Schema{ Version: "1", + + Notifications: []schema.SchemaField{ + { + Type: "notification", + ID: "notificationid", + Name: "Notification", + Description: "A Notification", + Icon: "notification", + Sounds: []schema.SchemaSound{ + { + Title: "Ding!", + Path: "ding.mp3", + }, + }, + }, + }, + Fields: []schema.SchemaField{ { Type: "location", @@ -252,6 +290,67 @@ def main(): }, s) } +func TestSchemaWithNotificationInFields(t *testing.T) { + code := ` +load("schema.star", "schema") +load("ding.mp3", ding = "file") + +def get_schema(): + return schema.Schema( + version = "1", + + fields = [ + schema.Notification( + id = "notificationid", + name = "Notification", + desc = "A Notification", + icon = "notification", + sounds = [ + schema.Sound( + title = "Ding!", + file = ding, + ), + ], + ), + ], + ) + +def main(): + return None +` + + _, err := loadApp(code) + assert.ErrorContains(t, err, "expected fields") +} + +func TestSchemaWithFieldsInNotifications(t *testing.T) { + code := ` +load("schema.star", "schema") +load("ding.mp3", ding = "file") + +def get_schema(): + return schema.Schema( + version = "1", + + notifications = [ + schema.Color( + id = "colorid", + name = "Color", + desc = "A Color", + icon = "brush", + default = "ffaa66", + ), + ], + ) + +def main(): + return None +` + + _, err := loadApp(code) + assert.ErrorContains(t, err, "expected notifications") +} + // test with all available config types and flags. func TestSchemaAllTypesSuccessLegacy(t *testing.T) { code := ` @@ -378,10 +477,8 @@ def main(): app, err := loadApp(code) assert.NoError(t, err) - jsonSchema := app.GetSchema() - var s schema.Schema - json.Unmarshal([]byte(jsonSchema), &s) + json.Unmarshal(app.SchemaJSON, &s) assert.Equal(t, schema.Schema{ Version: "1", @@ -1052,5 +1149,4 @@ def main(): assert.Equal(t, 2, len(options)) assert.Equal(t, "L08", options[0].Value) assert.Equal(t, "3rd", options[1].Value) - } diff --git a/schema/sound.go b/schema/sound.go new file mode 100644 index 0000000000..eaaa7d9ac7 --- /dev/null +++ b/schema/sound.go @@ -0,0 +1,74 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" + + "tidbyt.dev/pixlet/runtime/modules/file" +) + +type Sound struct { + SchemaSound + file *file.File +} + +func newSound( + thread *starlark.Thread, + _ *starlark.Builtin, + args starlark.Tuple, + kwargs []starlark.Tuple, +) (starlark.Value, error) { + var ( + title starlark.String + file *file.File + ) + + if err := starlark.UnpackArgs( + "Sound", + args, kwargs, + "title", &title, + "file", &file, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for Sound: %s", err) + } + + s := &Sound{file: file} + s.Title = title.GoString() + s.Path = file.Path + + return s, nil +} + +func (s *Sound) AsSchemaSound() SchemaSound { + return s.SchemaSound +} + +func (s *Sound) AttrNames() []string { + return []string{"title", "file"} +} + +func (s *Sound) Attr(name string) (starlark.Value, error) { + switch name { + + case "title": + return starlark.String(s.Title), nil + + case "file": + return s.file, nil + + default: + return nil, nil + } +} + +func (s *Sound) String() string { return "Sound(...)" } +func (s *Sound) Type() string { return "Sound" } +func (s *Sound) Freeze() {} +func (s *Sound) Truth() starlark.Bool { return true } + +func (s *Sound) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/sound_test.go b/schema/sound_test.go new file mode 100644 index 0000000000..2fd008bf5b --- /dev/null +++ b/schema/sound_test.go @@ -0,0 +1,44 @@ +package schema_test + +import ( + "context" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var soundSource = ` +load("schema.star", "schema") +load("sound.mp3", "file") + +def assert(success, message=None): + if not success: + fail(message or "assertion failed") + +s = schema.Sound( + title = "Sneezing Elephant", + file = file, +) + +assert(s.title == "Sneezing Elephant") +assert(s.file == file) +assert(s.file.readall() == "sound data") + +def main(): + return [] +` + +func TestSound(t *testing.T) { + vfs := fstest.MapFS{ + "sound.mp3": &fstest.MapFile{Data: []byte("sound data")}, + "sound.star": &fstest.MapFile{Data: []byte(soundSource)}, + } + app, err := runtime.NewAppletFromFS("sound", vfs) + assert.NoError(t, err) + + screens, err := app.Run(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/server/loader/loader.go b/server/loader/loader.go index 103a157e45..4f04f68983 100644 --- a/server/loader/loader.go +++ b/server/loader/loader.go @@ -50,7 +50,6 @@ func NewLoader( maxDuration int, timeout int, ) (*Loader, error) { - l := &Loader{ fs: fs, fileChanges: fileChanges, @@ -116,7 +115,7 @@ func (l *Loader) Run() error { up.Err = err } else { up.WebP = webp - up.Schema = l.applet.GetSchema() + up.Schema = string(l.applet.SchemaJSON) } l.updatesChan <- up @@ -141,7 +140,7 @@ func (l *Loader) LoadApplet(config map[string]string) (string, error) { func (l *Loader) GetSchema() []byte { <-l.initialLoad - s := []byte(l.applet.GetSchema()) + s := l.applet.SchemaJSON if len(s) > 0 { return s }