diff --git a/schema/datetime.go b/schema/datetime.go new file mode 100644 index 0000000000..b3422da66c --- /dev/null +++ b/schema/datetime.go @@ -0,0 +1,86 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type DateTime struct { + SchemaField +} + +func newDateTime( + 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 + ) + + if err := starlark.UnpackArgs( + "DateTime", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for DateTime: %s", err) + } + + s := &DateTime{} + s.SchemaField.Type = "datetime" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + + return s, nil +} + +func (s *DateTime) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *DateTime) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", + } +} + +func (s *DateTime) 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 + + default: + return nil, nil + } +} + +func (s *DateTime) String() string { return "DateTime(...)" } +func (s *DateTime) Type() string { return "DateTime" } +func (s *DateTime) Freeze() {} +func (s *DateTime) Truth() starlark.Bool { return true } + +func (s *DateTime) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/datetime_test.go b/schema/datetime_test.go new file mode 100644 index 0000000000..e8e57bc627 --- /dev/null +++ b/schema/datetime_test.go @@ -0,0 +1,41 @@ +package schema_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var dateTimeSource = ` +load("schema.star", "schema") + +def assert(success, message=None): + if not success: + fail(message or "assertion failed") + +t = schema.DateTime( + id = "event_name", + name = "Event Name", + desc = "The time of the event.", + icon = "cog", +) + +assert(t.id == "event_name") +assert(t.name == "Event Name") +assert(t.desc == "The time of the event.") +assert(t.icon == "cog") + +def main(): + return [] +` + +func TestDateTime(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("date_time.star", []byte(dateTimeSource), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/schema/locationbased.go b/schema/locationbased.go new file mode 100644 index 0000000000..e52fec5d75 --- /dev/null +++ b/schema/locationbased.go @@ -0,0 +1,94 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type LocationBased struct { + SchemaField + starlarkHandler *starlark.Function +} + +func newLocationBased( + 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 + handler *starlark.Function + ) + + if err := starlark.UnpackArgs( + "LocationBased", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + "handler", &handler, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for LocationBased: %s", err) + } + + s := &LocationBased{} + s.SchemaField.Type = "locationbased" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + s.Handler = handler.Name() + s.starlarkHandler = handler + + return s, nil +} + +func (s *LocationBased) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *LocationBased) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", "handler", + } +} + +func (s *LocationBased) 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 "handler": + return s.starlarkHandler, nil + + default: + return nil, nil + } +} + +func (s *LocationBased) String() string { return "LocationBased(...)" } +func (s *LocationBased) Type() string { return "LocationBased" } +func (s *LocationBased) Freeze() {} +func (s *LocationBased) Truth() starlark.Bool { return true } + +func (s *LocationBased) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/locationbased_test.go b/schema/locationbased_test.go new file mode 100644 index 0000000000..d76567d01f --- /dev/null +++ b/schema/locationbased_test.go @@ -0,0 +1,71 @@ +package schema_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var locationBasedSource = ` +load("encoding/json.star", "json") +load("schema.star", "schema") + +DEFAULT_LOCATION = """ +{ + "lat": "40.6781784", + "lng": "-73.9441579", + "description": "Brooklyn, NY, USA", + "locality": "Brooklyn", + "place_id": "ChIJCSF8lBZEwokRhngABHRcdoI", + "timezone": "America/New_York" +} +""" + +def assert(success, message = None): + if not success: + fail(message or "assertion failed") + +def get_stations(location): + loc = json.decode(location) + lat, lng = float(loc["lat"]), float(loc["lng"]) + + return [ + schema.Option( + display = "Grand Central", + value = "abc123", + ), + schema.Option( + display = "Penn Station", + value = "xyz123", + ), + ] + +t = schema.LocationBased( + id = "station", + name = "Train Station", + desc = "A list of train stations based on a location.", + icon = "train", + handler = get_stations, +) + +assert(t.id == "station") +assert(t.name == "Train Station") +assert(t.desc == "A list of train stations based on a location.") +assert(t.icon == "train") +assert(t.handler(DEFAULT_LOCATION)[0].display == "Grand Central") + +def main(): + return [] + +` + +func TestLocationBased(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("location_based.star", []byte(locationBasedSource), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/schema/module.go b/schema/module.go index 25b2ff66df..245e2ec363 100644 --- a/schema/module.go +++ b/schema/module.go @@ -24,12 +24,18 @@ func LoadModule() (starlark.StringDict, error) { ModuleName: &starlarkstruct.Module{ Name: ModuleName, Members: starlark.StringDict{ - "Schema": starlark.NewBuiltin("Schema", newSchema), - "Toggle": starlark.NewBuiltin("Toggle", newToggle), - "Option": starlark.NewBuiltin("Option", newOption), - "Dropdown": starlark.NewBuiltin("Dropdown", newDropdown), - "Location": starlark.NewBuiltin("Location", newLocation), - "Text": starlark.NewBuiltin("Text", newText), + "Schema": starlark.NewBuiltin("Schema", newSchema), + "Toggle": starlark.NewBuiltin("Toggle", newToggle), + "Option": starlark.NewBuiltin("Option", newOption), + "Dropdown": starlark.NewBuiltin("Dropdown", newDropdown), + "Location": starlark.NewBuiltin("Location", newLocation), + "Text": starlark.NewBuiltin("Text", newText), + "LocationBased": starlark.NewBuiltin("LocationBased", newLocationBased), + "DateTime": starlark.NewBuiltin("DateTime", newDateTime), + "OAuth2": starlark.NewBuiltin("OAuth2", newOAuth2), + "Radio": starlark.NewBuiltin("Radio", newRadio), + "PhotoSelect": starlark.NewBuiltin("PhotoSelect", newPhotoSelect), + "Typeahead": starlark.NewBuiltin("Typeahead", newTypeahead), }, }, } diff --git a/schema/oauth2.go b/schema/oauth2.go new file mode 100644 index 0000000000..9ab4bb327b --- /dev/null +++ b/schema/oauth2.go @@ -0,0 +1,136 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type OAuth2 struct { + SchemaField + starlarkHandler *starlark.Function + starlarkScopes *starlark.List +} + +func newOAuth2( + 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 + handler *starlark.Function + clientID starlark.String + authEndpoint starlark.String + scopes *starlark.List + ) + + if err := starlark.UnpackArgs( + "OAuth2", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + "handler", &handler, + "client_id", &clientID, + "authorization_endpoint", &authEndpoint, + "scopes", &scopes, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for OAuth2: %s", err) + } + + s := &OAuth2{} + s.SchemaField.Type = "oauth2" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + s.Handler = handler.Name() + s.starlarkHandler = handler + s.ClientID = clientID.GoString() + s.AuthorizationEndpoint = authEndpoint.GoString() + s.starlarkScopes = scopes + + if s.starlarkScopes != nil { + scopesIter := s.starlarkScopes.Iterate() + defer scopesIter.Done() + + var scopeVal starlark.Value + for i := 0; scopesIter.Next(&scopeVal); { + if _, isNone := scopeVal.(starlark.NoneType); isNone { + continue + } + + scope, ok := scopeVal.(starlark.String) + if !ok { + return nil, fmt.Errorf( + "expected fields to be a list of string but found: %s (at index %d)", + scopeVal.Type(), + i, + ) + } + + s.Scopes = append(s.Scopes, scope.GoString()) + } + } + + return s, nil +} + +func (s *OAuth2) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *OAuth2) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", "handler", "client_id", "authorization_endpoint", "scopes", + } +} + +func (s *OAuth2) 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 "handler": + return s.starlarkHandler, nil + + case "client_id": + return starlark.String(s.ClientID), nil + + case "authorization_endpoint": + return starlark.String(s.AuthorizationEndpoint), nil + + case "scopes": + return s.starlarkScopes, nil + + default: + return nil, nil + } +} + +func (s *OAuth2) String() string { return "OAuth2(...)" } +func (s *OAuth2) Type() string { return "OAuth2" } +func (s *OAuth2) Freeze() {} +func (s *OAuth2) Truth() starlark.Bool { return true } + +func (s *OAuth2) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/oauth2_test.go b/schema/oauth2_test.go new file mode 100644 index 0000000000..fde747454d --- /dev/null +++ b/schema/oauth2_test.go @@ -0,0 +1,57 @@ +package schema_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var oauth2Source = ` +load("encoding/json.star", "json") +load("schema.star", "schema") + +def assert(success, message = None): + if not success: + fail(message or "assertion failed") + +def oauth_handler(params): + params = json.decode(params) + return "foobar123" + +t = schema.OAuth2( + id = "auth", + name = "GitHub", + desc = "Connect your GitHub account.", + icon = "github", + handler = oauth_handler, + client_id = "the-oauth2-client-id", + authorization_endpoint = "https://example.com/", + scopes = [ + "read:user", + ], +) + +assert(t.id == "auth") +assert(t.name == "GitHub") +assert(t.desc == "Connect your GitHub account.") +assert(t.icon == "github") +assert(t.handler("{}") == "foobar123") +assert(t.client_id == "the-oauth2-client-id") +assert(t.authorization_endpoint == "https://example.com/") +assert(t.scopes == ["read:user"]) + +def main(): + return [] + +` + +func TestOAuth2(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("oauth2.star", []byte(oauth2Source), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/schema/photoselect.go b/schema/photoselect.go new file mode 100644 index 0000000000..60b6560cd1 --- /dev/null +++ b/schema/photoselect.go @@ -0,0 +1,86 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type PhotoSelect struct { + SchemaField +} + +func newPhotoSelect( + 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 + ) + + if err := starlark.UnpackArgs( + "PhotoSelect", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for PhotoSelect: %s", err) + } + + s := &PhotoSelect{} + s.SchemaField.Type = "png" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + + return s, nil +} + +func (s *PhotoSelect) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *PhotoSelect) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", + } +} + +func (s *PhotoSelect) 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 + + default: + return nil, nil + } +} + +func (s *PhotoSelect) String() string { return "PhotoSelect(...)" } +func (s *PhotoSelect) Type() string { return "PhotoSelect" } +func (s *PhotoSelect) Freeze() {} +func (s *PhotoSelect) Truth() starlark.Bool { return true } + +func (s *PhotoSelect) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/photoselect_test.go b/schema/photoselect_test.go new file mode 100644 index 0000000000..bf4d9830ae --- /dev/null +++ b/schema/photoselect_test.go @@ -0,0 +1,41 @@ +package schema_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var photoSelectSource = ` +load("schema.star", "schema") + +def assert(success, message=None): + if not success: + fail(message or "assertion failed") + +t = schema.PhotoSelect( + id = "photo", + name = "Add Photo", + desc = "A photo.", + icon = "cog", +) + +assert(t.id == "photo") +assert(t.name == "Add Photo") +assert(t.desc == "A photo.") +assert(t.icon == "cog") + +def main(): + return [] +` + +func TestPhotoSelect(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("photo_select.star", []byte(photoSelectSource), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/schema/radio.go b/schema/radio.go new file mode 100644 index 0000000000..4cb1baefa8 --- /dev/null +++ b/schema/radio.go @@ -0,0 +1,119 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type Radio struct { + SchemaField + starlarkOptions *starlark.List +} + +func newRadio( + 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 + def starlark.String + options *starlark.List + ) + + if err := starlark.UnpackArgs( + "Radio", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + "default", &def, + "options", &options, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for Radio: %s", err) + } + + s := &Radio{} + s.SchemaField.Type = "radio" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + s.Default = def.GoString() + + var optionVal starlark.Value + optionIter := options.Iterate() + defer optionIter.Done() + for i := 0; optionIter.Next(&optionVal); { + if _, isNone := optionVal.(starlark.NoneType); isNone { + continue + } + + o, ok := optionVal.(*Option) + if !ok { + return nil, fmt.Errorf( + "expected options to be a list of Option but found: %s (at index %d)", + optionVal.Type(), + i, + ) + } + + s.Options = append(s.Options, o.SchemaOption) + } + s.starlarkOptions = options + + return s, nil +} + +func (s *Radio) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *Radio) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", "default", "options", + } +} + +func (s *Radio) 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 "default": + return starlark.String(s.Default), nil + + case "options": + return s.starlarkOptions, nil + + default: + return nil, nil + } +} + +func (s *Radio) String() string { return "Radio(...)" } +func (s *Radio) Type() string { return "Radio" } +func (s *Radio) Freeze() {} +func (s *Radio) Truth() starlark.Bool { return true } + +func (s *Radio) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/radio_test.go b/schema/radio_test.go new file mode 100644 index 0000000000..a8c77bd2d5 --- /dev/null +++ b/schema/radio_test.go @@ -0,0 +1,61 @@ +package schema_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var radioSource = ` +load("schema.star", "schema") + +def assert(success, message=None): + if not success: + fail(message or "assertion failed") + +options = [ + schema.Option( + display = "Green", + value = "#00FF00", + ), + schema.Option( + display = "Red", + value = "#FF0000", + ), +] + +s = schema.Radio( + id = "colors", + name = "Text Color", + desc = "The color of text to be displayed.", + icon = "brush", + default = options[0].value, + options = options, +) + +assert(s.id == "colors") +assert(s.name == "Text Color") +assert(s.desc == "The color of text to be displayed.") +assert(s.icon == "brush") +assert(s.default == "#00FF00") + +assert(s.options[0].display == "Green") +assert(s.options[0].value == "#00FF00") + +assert(s.options[1].display == "Red") +assert(s.options[1].value == "#FF0000") + +def main(): + return [] +` + +func TestRadio(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("radio.star", []byte(radioSource), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +} diff --git a/schema/schema.go b/schema/schema.go index 88169df010..0a3b366088 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -109,6 +109,9 @@ func FromStarlark( starlarkSchema, ok := val.(*StarlarkSchema) if ok { schema = &starlarkSchema.Schema + if schema.Handlers == nil { + schema.Handlers = make(map[string]SchemaHandler) + } } else { schemaTree, err := unmarshalStarlark(val) if err != nil { diff --git a/schema/schema_test.go b/schema/schema_test.go index 5ac9fcb9f3..cbeaf8bf2e 100644 --- a/schema/schema_test.go +++ b/schema/schema_test.go @@ -22,9 +22,262 @@ func loadApp(code string) (*runtime.Applet, error) { return app, nil } -// appruntime.Schema test with all available config types and flags. func TestSchemaAllTypesSuccess(t *testing.T) { code := ` +load("schema.star", "schema") + +# these won't be called unless GetSchemaHandler() is +def locationbasedhandler(): + return None + +def generatedhandler(): + return None + +def typeaheadhandler(): + return ":)" + +def oauth2handler(): + return "a-refresh-token" + +def get_schema(): + return schema.Schema( + version = "1", + fields = [ + schema.Location( + id = "locationid", + name = "Location", + desc = "A Location", + icon = "place", + ), + schema.LocationBased( + id = "locationbasedid", + name = "Locationbased", + desc = "A Locationbased", + icon = "place", + handler = locationbasedhandler, + ), + schema.Toggle( + id = "onoffid", + name = "On or off", + desc = "An Onoff", + icon = "schedule", + default = False, + ), + schema.Text( + id = "textid", + name = "Text", + desc = "A Text", + icon = "cog", + default = "Default text", + ), + schema.Dropdown( + id = "dropdownid", + name = "Dropdown", + desc = "A Dropdown", + icon = "iconthatdoesntexist", + options = [ + schema.Option( + display = "dt1", + value = "dv1", + ), + schema.Option( + display = "dt2", + value = "dv2", + ), + ], + default = "dv2", + ), + schema.Radio( + id = "radioid", + name = "Radio", + desc = "A Radio", + icon = "iconthatdoesntexist", + options = [ + schema.Option( + display = "rt1", + value = "rv1", + ), + schema.Option( + display = "rt2", + value = "rv2", + ), + ], + default = "rv1", + ), + schema.Typeahead( + id = "typeaheadid", + name = "Typeahead", + desc = "A Typeahead", + icon = "train", + handler = typeaheadhandler, + ), + schema.OAuth2( + id = "oauth2id", + name = "OAuth2", + desc = "Authentication", + icon = "train", + handler = oauth2handler, + client_id = "oauth2_clientid", + authorization_endpoint = "https://example.com/auth", + scopes = [ + "foo", + "bar", + ], + ), + schema.PhotoSelect( + id = "pngid", + name = "Photo", + desc = "Picture", + icon = "photo_camera", + ), + ], + ) + +def main(): + return None +` + + app, err := loadApp(code) + assert.NoError(t, err) + + jsonSchema := app.GetSchema() + + var s schema.Schema + json.Unmarshal([]byte(jsonSchema), &s) + + assert.Equal(t, schema.Schema{ + Version: "1", + Fields: []schema.SchemaField{ + { + Type: "location", + ID: "locationid", + Name: "Location", + Description: "A Location", + Icon: "place", + }, + { + Type: "locationbased", + ID: "locationbasedid", + Name: "Locationbased", + Description: "A Locationbased", + Handler: "locationbasedhandler", + Icon: "place", + }, + { + Type: "onoff", + ID: "onoffid", + Name: "On or off", + Description: "An Onoff", + Default: "false", + Icon: "schedule", + }, + { + Type: "text", + ID: "textid", + Name: "Text", + Description: "A Text", + Icon: "cog", + Default: "Default text", + }, + { + Type: "dropdown", + ID: "dropdownid", + Name: "Dropdown", + Description: "A Dropdown", + Options: []schema.SchemaOption{ + { + Text: "dt1", + Value: "dv1", + }, + { + Text: "dt2", + Value: "dv2", + }, + }, + Default: "dv2", + Icon: "iconthatdoesntexist", + }, + { + Type: "radio", + ID: "radioid", + Name: "Radio", + Description: "A Radio", + Icon: "iconthatdoesntexist", + Options: []schema.SchemaOption{ + { + Text: "rt1", + Value: "rv1", + }, + { + Text: "rt2", + Value: "rv2", + }, + }, + Default: "rv1", + }, + //{ + // Type: "text", + // ID: "invisibletext", + // Name: "Invisible Text", + // Description: "Conditionally visible text", + // Visibility: &schema.SchemaVisibility{ + // Type: "invisible", + // Condition: "equal", + // Variable: "radio", + // Value: "rv2", + // }, + //}, + //{ + // Type: "text", + // ID: "invisibletext2", + // Name: "Invisible Text", + // Description: "Conditionally visible text", + // Visibility: &schema.SchemaVisibility{ + // Type: "invisible", + // Condition: "not_equal", + // Variable: "radio", + // Value: "rv2", + // }, + //}, + //{ + // Type: "generated", + // ID: "generatedid", + // Handler: "generatedhandler", + // Source: "radioid", + //}, + { + Type: "typeahead", + ID: "typeaheadid", + Name: "Typeahead", + Description: "A Typeahead", + Handler: "typeaheadhandler", + Icon: "train", + }, + { + Type: "oauth2", + ID: "oauth2id", + Name: "OAuth2", + Description: "Authentication", + Handler: "oauth2handler", + Icon: "train", + ClientID: "oauth2_clientid", + AuthorizationEndpoint: "https://example.com/auth", + Scopes: []string{"foo", "bar"}, + }, + { + Type: "png", + ID: "pngid", + Name: "Photo", + Description: "Picture", + Icon: "photo_camera", + }, + }, + }, s) +} + +// appruntime.Schema test with all available config types and flags. +func TestSchemaAllTypesSuccessLegacy(t *testing.T) { + code := ` def get_schema(): return [ {"type": "location", diff --git a/schema/text.go b/schema/text.go index 13974588ce..ff79ed4225 100644 --- a/schema/text.go +++ b/schema/text.go @@ -22,6 +22,7 @@ func newText( name starlark.String desc starlark.String icon starlark.String + def starlark.String ) if err := starlark.UnpackArgs( @@ -31,6 +32,7 @@ func newText( "name", &name, "desc", &desc, "icon", &icon, + "default?", &def, ); err != nil { return nil, fmt.Errorf("unpacking arguments for Text: %s", err) } @@ -41,6 +43,7 @@ func newText( s.Name = name.GoString() s.Description = desc.GoString() s.Icon = icon.GoString() + s.Default = def.GoString() return s, nil } @@ -51,7 +54,7 @@ func (s *Text) AsSchemaField() SchemaField { func (s *Text) AttrNames() []string { return []string{ - "id", "name", "desc", "icon", + "id", "name", "desc", "icon", "default", } } @@ -70,6 +73,9 @@ func (s *Text) Attr(name string) (starlark.Value, error) { case "icon": return starlark.String(s.Icon), nil + case "default": + return starlark.String(s.Default), nil + default: return nil, nil } diff --git a/schema/text_test.go b/schema/text_test.go index 300fd10020..c788fc9c66 100644 --- a/schema/text_test.go +++ b/schema/text_test.go @@ -19,12 +19,14 @@ s = schema.Text( name = "Screen Name", desc = "A text entry for your screen name.", icon = "user", + default = "foo", ) assert(s.id == "screen_name") assert(s.name == "Screen Name") assert(s.desc == "A text entry for your screen name.") assert(s.icon == "user") +assert(s.default == "foo") def main(): return [] diff --git a/schema/typeahead.go b/schema/typeahead.go new file mode 100644 index 0000000000..72ebb0924a --- /dev/null +++ b/schema/typeahead.go @@ -0,0 +1,94 @@ +package schema + +import ( + "fmt" + + "github.com/mitchellh/hashstructure/v2" + "go.starlark.net/starlark" +) + +type Typeahead struct { + SchemaField + starlarkHandler *starlark.Function +} + +func newTypeahead( + 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 + handler *starlark.Function + ) + + if err := starlark.UnpackArgs( + "Typeahead", + args, kwargs, + "id", &id, + "name", &name, + "desc", &desc, + "icon", &icon, + "handler", &handler, + ); err != nil { + return nil, fmt.Errorf("unpacking arguments for Typeahead: %s", err) + } + + s := &Typeahead{} + s.SchemaField.Type = "typeahead" + s.ID = id.GoString() + s.Name = name.GoString() + s.Description = desc.GoString() + s.Icon = icon.GoString() + s.Handler = handler.Name() + s.starlarkHandler = handler + + return s, nil +} + +func (s *Typeahead) AsSchemaField() SchemaField { + return s.SchemaField +} + +func (s *Typeahead) AttrNames() []string { + return []string{ + "id", "name", "desc", "icon", "handler", + } +} + +func (s *Typeahead) 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 "handler": + return s.starlarkHandler, nil + + default: + return nil, nil + } +} + +func (s *Typeahead) String() string { return "Typeahead(...)" } +func (s *Typeahead) Type() string { return "Typeahead" } +func (s *Typeahead) Freeze() {} +func (s *Typeahead) Truth() starlark.Bool { return true } + +func (s *Typeahead) Hash() (uint32, error) { + sum, err := hashstructure.Hash(s, hashstructure.FormatV2, nil) + return uint32(sum), err +} diff --git a/schema/typeahead_test.go b/schema/typeahead_test.go new file mode 100644 index 0000000000..c8d20c2efa --- /dev/null +++ b/schema/typeahead_test.go @@ -0,0 +1,56 @@ +package schema_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "tidbyt.dev/pixlet/runtime" +) + +var typeaheadSource = ` +load("schema.star", "schema") + +def assert(success, message = None): + if not success: + fail(message or "assertion failed") + +def search(pattern): + return [ + schema.Option( + display = "Grand Central", + value = "abc123", + ), + schema.Option( + display = "Penn Station", + value = "xyz123", + ), + ] + +t = schema.Typeahead( + id = "search", + name = "Search", + desc = "A list of items that match search.", + icon = "cog", + handler = search, +) + +assert(t.id == "search") +assert(t.name == "Search") +assert(t.desc == "A list of items that match search.") +assert(t.icon == "cog") +assert(t.handler("")[0].display == "Grand Central") + +def main(): + return [] + +` + +func TestTypeahead(t *testing.T) { + app := &runtime.Applet{} + err := app.Load("typeahead.star", []byte(typeaheadSource), nil) + assert.NoError(t, err) + + screens, err := app.Run(map[string]string{}) + assert.NoError(t, err) + assert.NotNil(t, screens) +}