Compare commits

..

7 Commits

26 changed files with 1066 additions and 138 deletions

View File

@ -125,23 +125,34 @@ func copyDir(writer *zip.Writer, baseDir string, zipBasePath string) error {
}
func copyFile(writer *zip.Writer, srcPath string, zipPath string) error {
r, err := os.Open(srcPath)
srcFile, err := os.Open(srcPath)
if err != nil {
return errors.WithStack(err)
}
srcStat, err := os.Stat(srcPath)
if err != nil {
return errors.WithStack(err)
}
defer func() {
if err := r.Close(); err != nil {
if err := srcFile.Close(); err != nil {
panic(errors.WithStack(err))
}
}()
f, err := writer.Create(zipPath)
fileHeader := &zip.FileHeader{
Name: zipPath,
Modified: srcStat.ModTime().UTC(),
Method: zip.Deflate,
}
file, err := writer.CreateHeader(fileHeader)
if err != nil {
return errors.WithStack(err)
}
if _, err = io.Copy(f, r); err != nil {
if _, err = io.Copy(file, srcFile); err != nil {
return errors.WithStack(err)
}

View File

@ -8,6 +8,7 @@ import (
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
"forge.cadoles.com/arcad/edge/pkg/module/cast"
"forge.cadoles.com/arcad/edge/pkg/storage/sqlite"
"gitlab.com/wpetit/goweb/logger"
@ -96,6 +97,7 @@ func RunCommand() *cli.Command {
appHTTP.WithServerModules(
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
cast.CastModuleFactory(),
module.LifecycleModuleFactory(bus),
module.NetModuleFactory(bus),
module.RPCModuleFactory(bus),

View File

@ -12,7 +12,7 @@ Comme son nom l'indique, elle permet d'exécuter des opérations d'initialisatio
```js
function onInit() {
console.log("My app booted !")
}
```
@ -25,4 +25,5 @@ Listes des modules disponibles côté serveur.
- [`net`](./net.md)
- [`rpc`](./rpc.md)
- [`store`](./store.md)
- [`blob`](./blob.md)
- [`blob`](./blob.md)
- [`cast`](./cast.md)

View File

@ -0,0 +1,38 @@
# Module `cast`
Ce module permet de communiquer avec des appareils de présentation de type [Chromecast](https://store.google.com/fr/product/chromecast_setup?hl=fr).
## Méthodes
### `cast.refreshDevices(timeout?: string = '30s'): Promise<Device[]>`
Rafraichit la liste locale des appareils de présentation disponibles sur les réseaux locaux de la borne.
L'appel à cette méthode rafraîchit également la liste mise en cache et renvoyée par `cast.getDevices()`.
### `cast.getDevices(): []Device`
Retourne la liste mise en cache des appareils de présentation disponibles sur les réseaux locaux de la borne.
La liste est initialement vide. Un appel initial à `cast.refreshDevices()` est nécessaire afin de mettre à jour celle ci.
### `cast.loadUrl(deviceUuid: string, url: string, timeout?: string = '30s'): Promise<void>`
Charge l'URL donnée sur l'appareil de présentation identifié par l'UUID `deviceUuid`.
### `cast.stopCast(deviceUuid: string, timeout?: string = '30s'): Promise<void>`
Stoppe l'application courante sur l'appareil de présentation identifié par l'UUID `deviceUuid`.
## Objets
### `Device`
```typescript
interface Device {
uuid: string // UUID de l'appareil
name: string // Nom de l'appareil
host: string // Adresse IPv4 de l'appareil
port: number // Port distant du service
}
```

8
go.mod
View File

@ -4,9 +4,17 @@ go 1.19
require modernc.org/sqlite v1.20.4
require (
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e // indirect
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 // indirect
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 // indirect
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 // indirect
)
require (
cdr.dev/slog v1.4.0 // indirect
github.com/alecthomas/chroma v0.7.0 // indirect
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect

15
go.sum
View File

@ -52,6 +52,8 @@ github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYU
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692 h1:JW4WZlqyaNWUUahfr7MigeDW6jmtam5cTzzo1lwsFhE=
github.com/barnybug/go-cast v0.0.0-20201201064555-a87ccbc26692/go.mod h1:Au0ipPuCBA7zsOC61SnyrYetm8VT3vo1UJtwHeYke44=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
@ -66,6 +68,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.0.1-0.20160907170601-6d212800a42e/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -105,6 +108,8 @@ github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3yg
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e h1:eeyMpoxANuWNQ9O2auv4wXxJsrXzLUhdHaOmNWEGkRY=
github.com/gogo/protobuf v0.0.0-20161014173244-50d1bd39ce4e/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -172,9 +177,13 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073 h1:9dodOMuH6u7LvPEkVydBv6KTHdm+SqsHOxHTzRW+1+w=
github.com/hashicorp/go.net v0.0.0-20151006203346-104dcad90073/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8 h1:yupxZNIxm5U8Tfb8g65irIuHkgF8c4koHC7daPSyMTE=
github.com/hashicorp/mdns v0.0.0-20151206042412-9d85cf22f9f8/go.mod h1:aa76Av3qgPeIQp9Y3qIkTBPieQYNkQ13Kxe7pze9Wb0=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/igm/sockjs-go/v3 v3.0.2 h1:2m0k53w0DBiGozeQUIEPR6snZFmpFpYvVsGnfLPNXbE=
@ -202,6 +211,8 @@ github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGe
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8 h1:ALvJ9V8nNf04PFHMR2sot56N/pjrx5LzZGvUlnhdiCE=
github.com/miekg/dns v0.0.0-20161006100029-fc4e1e2843d8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
@ -217,6 +228,7 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
@ -229,11 +241,13 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.1.5-0.20160925220609-976c720a22c8/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli/v2 v2.24.3 h1:7Q1w8VN8yE0MJEHP06bv89PjYsN4IHWED2s1v/Zlfm0=
github.com/urfave/cli/v2 v2.24.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@ -299,6 +313,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20161013035702-8b4af36cd21a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -145,7 +145,7 @@ func (s *Server) Stop() {
func (s *Server) initModules(factories ...ServerModuleFactory) {
runtime := goja.New()
runtime.SetFieldNameMapper(goja.UncapFieldNameMapper())
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("goja", true))
runtime.SetRandSource(createRandomSource())
modules := make([]ServerModule, 0, len(factories))

View File

@ -26,7 +26,7 @@ func defaultHandlerOptions() *HandlerOptions {
Bus: memory.NewBus(),
SockJS: sockjsOptions,
ServerModuleFactories: make([]app.ServerModuleFactory, 0),
UploadMaxFileSize: 1024 * 10, // 10Mb
UploadMaxFileSize: 10 << (10 * 2), // 10Mb
}
}

209
pkg/module/cast/cast.go Normal file
View File

@ -0,0 +1,209 @@
package cast
import (
"context"
"net"
"time"
"github.com/barnybug/go-cast"
"github.com/barnybug/go-cast/discovery"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type Device struct {
UUID string `goja:"uuid" json:"uuid"`
Host net.IP `goja:"host" json:"host"`
Port int `goja:"port" json:"port"`
Name string `goja:"name" json:"name"`
}
type DeviceStatus struct {
CurrentApp DeviceStatusCurrentApp `goja:"currentApp" json:"currentApp"`
Volume DeviceStatusVolume `goja:"volume" json:"volume"`
}
type DeviceStatusCurrentApp struct {
ID string `goja:"id" json:"id"`
DisplayName string `goja:"displayName" json:"displayName"`
StatusText string `goja:"statusText" json:"statusText"`
}
type DeviceStatusVolume struct {
Level float64 `goja:"level" json:"level"`
Muted bool `goja:"muted" json:"muted"`
}
const (
serviceDiscoveryPollingInterval time.Duration = 2 * time.Second
)
func getDeviceClientByUUID(ctx context.Context, uuid string) (*cast.Client, error) {
device, err := findDeviceByUUID(ctx, uuid)
if err != nil {
return nil, errors.WithStack(err)
}
client := cast.NewClient(device.Host, device.Port)
return client, nil
}
func findDeviceByUUID(ctx context.Context, uuid string) (*Device, error) {
service := discovery.NewService(ctx)
defer service.Stop()
go func() {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
LOOP:
for {
select {
case c := <-service.Found():
if c.Uuid() == uuid {
return &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
}, nil
}
case <-ctx.Done():
break LOOP
}
}
if err := ctx.Err(); err != nil {
return nil, errors.WithStack(err)
}
return nil, errors.WithStack(ErrDeviceNotFound)
}
func findDevices(ctx context.Context) ([]*Device, error) {
service := discovery.NewService(ctx)
defer service.Stop()
go func() {
if err := service.Run(ctx, serviceDiscoveryPollingInterval); err != nil && !errors.Is(err, context.DeadlineExceeded) {
logger.Error(ctx, "error while running cast service discovery", logger.E(errors.WithStack(err)))
}
}()
devices := make([]*Device, 0)
found := make(map[string]struct{})
LOOP:
for {
select {
case c := <-service.Found():
if _, exists := found[c.Uuid()]; exists {
continue
}
devices = append(devices, &Device{
Host: c.IP().To4(),
Port: c.Port(),
Name: c.Name(),
UUID: c.Uuid(),
})
found[c.Uuid()] = struct{}{}
case <-ctx.Done():
break LOOP
}
}
if err := ctx.Err(); err != nil && !errors.Is(err, context.DeadlineExceeded) {
return nil, errors.WithStack(err)
}
return devices, nil
}
func loadURL(ctx context.Context, deviceUUID string, url string) error {
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return errors.WithStack(err)
}
defer client.Close()
controller, err := client.URL(ctx)
if err != nil {
return errors.WithStack(err)
}
// Ignore context.DeadlineExceeded errors. github.com/barnybug/go-cast bug ?
if _, err := controller.LoadURL(ctx, url); err != nil && !isLoadURLContextExceeded(err) {
return errors.WithStack(err)
}
return nil
}
// False positive workaround.
func isLoadURLContextExceeded(err error) bool {
return err.Error() == "Failed to send load command: context deadline exceeded"
}
func stopCast(ctx context.Context, deviceUUID string) error {
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return errors.WithStack(err)
}
defer client.Close()
if _, err := client.Receiver().QuitApp(ctx); err != nil {
return errors.WithStack(err)
}
return nil
}
func getStatus(ctx context.Context, deviceUUID string) (*DeviceStatus, error) {
client, err := getDeviceClientByUUID(ctx, deviceUUID)
if err != nil {
return nil, errors.WithStack(err)
}
if err := client.Connect(ctx); err != nil {
return nil, errors.WithStack(err)
}
defer client.Close()
ctrlStatus, err := client.Receiver().GetStatus(ctx)
if err != nil {
return nil, errors.WithStack(err)
}
status := &DeviceStatus{
CurrentApp: DeviceStatusCurrentApp{
ID: "",
DisplayName: "",
StatusText: "",
},
Volume: DeviceStatusVolume{
Level: *ctrlStatus.Volume.Level,
Muted: *ctrlStatus.Volume.Muted,
},
}
if len(ctrlStatus.Applications) > 0 {
status.CurrentApp.ID = *ctrlStatus.Applications[0].AppID
status.CurrentApp.DisplayName = *ctrlStatus.Applications[0].DisplayName
status.CurrentApp.StatusText = *ctrlStatus.Applications[0].StatusText
}
return status, nil
}

View File

@ -0,0 +1,63 @@
package cast
import (
"context"
"os"
"testing"
"time"
"cdr.dev/slog"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestCastLoadURL(t *testing.T) {
t.Parallel()
if os.Getenv("TEST_CAST_MODULE") != "yes" {
t.Skip("Test skipped. Set environment variable TEST_CAST_MODULE=yes to run.")
return
}
logger.SetLevel(slog.LevelDebug)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
devices, err := findDevices(ctx)
if err != nil {
t.Error(errors.WithStack(err))
}
if e, g := 1, len(devices); e != g {
t.Fatalf("len(devices): expected '%v', got '%v'", e, g)
}
dev := devices[0]
ctx, cancel2 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel2()
if err := loadURL(ctx, dev.UUID, "https://go.dev"); err != nil {
t.Error(errors.WithStack(err))
}
ctx, cancel3 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel3()
status, err := getStatus(ctx, dev.UUID)
if err != nil {
t.Error(errors.WithStack(err))
}
spew.Dump(status)
ctx, cancel4 := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel4()
if err := stopCast(ctx, dev.UUID); err != nil {
t.Error(errors.WithStack(err))
}
}

5
pkg/module/cast/error.go Normal file
View File

@ -0,0 +1,5 @@
package cast
import "errors"
var ErrDeviceNotFound = errors.New("device not found")

263
pkg/module/cast/module.go Normal file
View File

@ -0,0 +1,263 @@
package cast
import (
"context"
"sync"
"time"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
"github.com/dop251/goja"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
const (
defaultTimeout = 30 * time.Second
)
type Module struct {
ctx context.Context
server *app.Server
mutex struct {
devices sync.RWMutex
refreshDevices sync.Mutex
loadURL sync.Mutex
quitApp sync.Mutex
getStatus sync.Mutex
}
devices []*Device
}
func (m *Module) Name() string {
return "cast"
}
func (m *Module) Export(export *goja.Object) {
if err := export.Set("refreshDevices", m.refreshDevices); err != nil {
panic(errors.Wrap(err, "could not set 'refreshDevices' function"))
}
if err := export.Set("getDevices", m.getDevices); err != nil {
panic(errors.Wrap(err, "could not set 'getDevices' function"))
}
if err := export.Set("loadUrl", m.loadUrl); err != nil {
panic(errors.Wrap(err, "could not set 'loadUrl' function"))
}
if err := export.Set("stopCast", m.stopCast); err != nil {
panic(errors.Wrap(err, "could not set 'stopCast' function"))
}
if err := export.Set("getStatus", m.getStatus); err != nil {
panic(errors.Wrap(err, "could not set 'getStatus' function"))
}
}
func (m *Module) refreshDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
rawTimeout := call.Argument(0).String()
timeout, err := m.parseTimeout(rawTimeout)
if err != nil {
panic(errors.WithStack(err))
}
promise := m.server.NewPromise()
go func() {
m.mutex.refreshDevices.Lock()
defer m.mutex.refreshDevices.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
devices, err := findDevices(ctx)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
err = errors.WithStack(err)
logger.Error(ctx, "error refreshing casting devices list", logger.E(errors.WithStack(err)))
promise.Reject(err)
return
}
if err == nil {
m.mutex.devices.Lock()
m.devices = devices
m.mutex.devices.Unlock()
}
devicesCopy := m.getDevicesCopy(devices)
promise.Resolve(devicesCopy)
}()
return rt.ToValue(promise)
}
func (m *Module) getDevices(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
m.mutex.devices.RLock()
defer m.mutex.devices.RUnlock()
devices := m.getDevicesCopy(m.devices)
return rt.ToValue(devices)
}
func (m *Module) loadUrl(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
if len(call.Arguments) < 2 {
panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber))
}
deviceUUID := call.Argument(0).String()
url := call.Argument(1).String()
rawTimeout := call.Argument(2).String()
timeout, err := m.parseTimeout(rawTimeout)
if err != nil {
panic(errors.WithStack(err))
}
promise := m.server.NewPromise()
go func() {
m.mutex.loadURL.Lock()
defer m.mutex.loadURL.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err := loadURL(ctx, deviceUUID, url)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "error while casting url", logger.E(err))
promise.Reject(err)
return
}
promise.Resolve(nil)
}()
return m.server.ToValue(promise)
}
func (m *Module) stopCast(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber))
}
deviceUUID := call.Argument(0).String()
rawTimeout := call.Argument(1).String()
timeout, err := m.parseTimeout(rawTimeout)
if err != nil {
panic(errors.WithStack(err))
}
promise := m.server.NewPromise()
go func() {
m.mutex.quitApp.Lock()
defer m.mutex.quitApp.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err := stopCast(ctx, deviceUUID)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "error while quitting casting device app", logger.E(errors.WithStack(err)))
promise.Reject(err)
return
}
promise.Resolve(nil)
}()
return m.server.ToValue(promise)
}
func (m *Module) getStatus(call goja.FunctionCall) goja.Value {
if len(call.Arguments) < 1 {
panic(errors.WithStack(module.ErrUnexpectedArgumentsNumber))
}
deviceUUID := call.Argument(0).String()
rawTimeout := call.Argument(1).String()
timeout, err := m.parseTimeout(rawTimeout)
if err != nil {
panic(errors.WithStack(err))
}
promise := m.server.NewPromise()
go func() {
m.mutex.getStatus.Lock()
defer m.mutex.getStatus.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
status, err := getStatus(ctx, deviceUUID)
if err != nil {
err = errors.WithStack(err)
logger.Error(ctx, "error while getting casting device status", logger.E(err))
promise.Reject(err)
return
}
promise.Resolve(status)
}()
return m.server.ToValue(promise)
}
func (m *Module) getDevicesCopy(devices []*Device) []Device {
devicesCopy := make([]Device, 0, len(m.devices))
for _, d := range devices {
devicesCopy = append(devicesCopy, Device{
UUID: d.UUID,
Name: d.Name,
Host: d.Host,
Port: d.Port,
})
}
return devicesCopy
}
func (m *Module) parseTimeout(rawTimeout string) (time.Duration, error) {
var (
timeout time.Duration
err error
)
if rawTimeout == "undefined" {
timeout = defaultTimeout
} else {
timeout, err = time.ParseDuration(rawTimeout)
if err != nil {
return defaultTimeout, errors.Wrapf(err, "invalid duration format '%s'", rawTimeout)
}
}
return timeout, nil
}
func CastModuleFactory() app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
server: server,
devices: make([]*Device, 0),
}
}
}

View File

@ -0,0 +1,95 @@
package cast
import (
"io/ioutil"
"os"
"testing"
"time"
"cdr.dev/slog"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
func TestCastModule(t *testing.T) {
t.Parallel()
if os.Getenv("TEST_CAST_MODULE") != "yes" {
t.Skip("Test skipped. Set environment variable TEST_CAST_MODULE=yes to run.")
return
}
logger.SetLevel(slog.LevelDebug)
server := app.NewServer(
module.ConsoleModuleFactory(),
CastModuleFactory(),
)
data, err := ioutil.ReadFile("testdata/cast.js")
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/cast.js", string(data)); err != nil {
t.Fatal(err)
}
if err := server.Start(); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
time.Sleep(20 * time.Second)
}
func TestCastModuleRefreshDevices(t *testing.T) {
t.Parallel()
if os.Getenv("TEST_CAST_MODULE") != "yes" {
t.Skip("Test skipped. Set environment variable TEST_CAST_MODULE=yes to run.")
return
}
logger.SetLevel(slog.LevelDebug)
server := app.NewServer(
module.ConsoleModuleFactory(),
CastModuleFactory(),
)
data, err := ioutil.ReadFile("testdata/refresh_devices.js")
if err != nil {
t.Fatal(err)
}
if err := server.Load("testdata/refresh_devices.js", string(data)); err != nil {
t.Fatal(err)
}
if err := server.Start(); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
defer server.Stop()
result, err := server.ExecFuncByName("refreshDevices")
if err != nil {
t.Error(errors.WithStack(err))
}
promise, ok := server.IsPromise(result)
if !ok {
t.Fatal("expected promise")
}
value := server.WaitForPromise(promise)
spew.Dump(value.Export())
}

21
pkg/module/cast/testdata/cast.js vendored Normal file
View File

@ -0,0 +1,21 @@
cast.refreshDevices('5s')
.then(function(devices) {
console.log(devices)
if (devices === null) {
throw new Error("devices should not be null");
}
if (devices.length === 0) {
throw new Error("devices.length should not be 0");
}
return devices
})
.then(function(devices) {
return cast.getStatus(devices[0].uuid)
})
.then(function(status) {
console.log(status)
})

View File

@ -0,0 +1,6 @@
function refreshDevices() {
return cast.refreshDevices('5s')
.then(function(devices) {
return devices
})
}

View File

@ -91,22 +91,24 @@ func (m *StoreModule) query(call goja.FunctionCall, rt *goja.Runtime) goja.Value
queryOptionsFuncs := make([]storage.QueryOptionFunc, 0)
if queryOptions.Limit != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit))
}
if queryOptions != nil {
if queryOptions.Limit != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithLimit(*queryOptions.Limit))
}
if queryOptions.OrderBy != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy))
}
if queryOptions.OrderBy != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderBy(*queryOptions.OrderBy))
}
if queryOptions.Offset != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
}
if queryOptions.Offset != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOffset(*queryOptions.Limit))
}
if queryOptions.OrderDirection != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection(
storage.OrderDirection(*queryOptions.OrderDirection),
))
if queryOptions.OrderDirection != nil {
queryOptionsFuncs = append(queryOptionsFuncs, storage.WithOrderDirection(
storage.OrderDirection(*queryOptions.OrderDirection),
))
}
}
documents, err := m.store.Query(ctx, collection, filter, queryOptionsFuncs...)
@ -144,6 +146,10 @@ func (m *StoreModule) assertCollection(value goja.Value, rt *goja.Runtime) strin
}
func (m *StoreModule) assertFilter(value goja.Value, rt *goja.Runtime) *filter.Filter {
if value.Export() == nil {
return nil
}
rawFilter, ok := value.Export().(map[string]interface{})
if !ok {
panic(rt.NewTypeError(fmt.Sprintf("filter must be an object, got '%T'", value.Export())))
@ -172,6 +178,10 @@ func (m *StoreModule) assertDocumentID(value goja.Value, rt *goja.Runtime) stora
}
func (m *StoreModule) assertQueryOptions(value goja.Value, rt *goja.Runtime) *queryOptions {
if value.Export() == nil {
return nil
}
rawQueryOptions, ok := value.Export().(map[string]interface{})
if !ok {
panic(rt.NewTypeError(fmt.Sprintf("query options must be an object, got '%T'", value.Export())))

View File

@ -31,12 +31,17 @@ func (d Document) ID() (DocumentID, bool) {
return "", false
}
id, ok := rawID.(string)
strID, ok := rawID.(string)
if ok {
return "", false
return DocumentID(strID), true
}
return DocumentID(id), true
docID, ok := rawID.(DocumentID)
if ok {
return docID, true
}
return "", false
}
func (d Document) CreatedAt() (time.Time, bool) {
@ -54,7 +59,7 @@ func (d Document) timeAttr(attr string) (time.Time, bool) {
}
t, ok := rawTime.(time.Time)
if ok {
if !ok {
return time.Time{}, false
}

View File

@ -0,0 +1,15 @@
package sql
const (
OpIn = "IN"
OpLesserThan = "<"
OpLesserThanEqual = "<="
OpEqual = "="
OpNotEqual = "!="
OpSuperiorThan = ">"
OpSuperiorThanEqual = ">="
OpAnd = "AND"
OpOr = "OR"
OpLike = "LIKE"
OpNot = "NOT"
)

View File

@ -71,6 +71,12 @@ func WithDefaultTransform() OptionFunc {
}
}
func WithTransform(transform TransformFunc) OptionFunc {
return func(opt *Option) {
opt.Transform = transform
}
}
func WithNoOpValueTransform() OptionFunc {
return WithValueTransform(func(value interface{}) interface{} {
return value

View File

@ -60,7 +60,7 @@ func transformAndOperator(op filter.Operator, option *Option) (string, []interfa
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenAnd, op.Token())
}
return aggregatorToSQL("AND", option, andOp.Children()...)
return aggregatorToSQL(OpAnd, option, andOp.Children()...)
}
func transformOrOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -69,7 +69,7 @@ func transformOrOperator(op filter.Operator, option *Option) (string, []interfac
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenOr, op.Token())
}
return aggregatorToSQL("OR", option, orOp.Children()...)
return aggregatorToSQL(OpOr, option, orOp.Children()...)
}
func transformEqOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -78,7 +78,7 @@ func transformEqOperator(op filter.Operator, option *Option) (string, []interfac
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenEq, op.Token())
}
return fieldsToSQL("=", false, eqOp.Fields(), option)
return fieldsToSQL(OpEqual, false, eqOp.Fields(), option)
}
func transformNeqOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -87,7 +87,7 @@ func transformNeqOperator(op filter.Operator, option *Option) (string, []interfa
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNeq, op.Token())
}
return fieldsToSQL("!=", false, eqOp.Fields(), option)
return fieldsToSQL(OpNotEqual, false, eqOp.Fields(), option)
}
func transformGtOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -96,7 +96,7 @@ func transformGtOperator(op filter.Operator, option *Option) (string, []interfac
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGt, op.Token())
}
return fieldsToSQL(">", false, gtOp.Fields(), option)
return fieldsToSQL(OpSuperiorThan, false, gtOp.Fields(), option)
}
func transformGteOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -105,7 +105,7 @@ func transformGteOperator(op filter.Operator, option *Option) (string, []interfa
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenGte, op.Token())
}
return fieldsToSQL(">=", false, gteOp.Fields(), option)
return fieldsToSQL(OpSuperiorThanEqual, false, gteOp.Fields(), option)
}
func transformLtOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -114,7 +114,7 @@ func transformLtOperator(op filter.Operator, option *Option) (string, []interfac
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLt, op.Token())
}
return fieldsToSQL("<", false, ltOp.Fields(), option)
return fieldsToSQL(OpLesserThan, false, ltOp.Fields(), option)
}
func transformLteOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -123,7 +123,7 @@ func transformLteOperator(op filter.Operator, option *Option) (string, []interfa
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLte, op.Token())
}
return fieldsToSQL("<=", false, lteOp.Fields(), option)
return fieldsToSQL(OpLesserThanEqual, false, lteOp.Fields(), option)
}
func transformInOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -132,7 +132,7 @@ func transformInOperator(op filter.Operator, option *Option) (string, []interfac
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenIn, op.Token())
}
return fieldsToSQL("IN", true, inOp.Fields(), option)
return fieldsToSQL(OpIn, true, inOp.Fields(), option)
}
func transformLikeOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -141,7 +141,7 @@ func transformLikeOperator(op filter.Operator, option *Option) (string, []interf
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenLike, op.Token())
}
return fieldsToSQL("LIKE", false, likeOp.Fields(), option)
return fieldsToSQL(OpLike, false, likeOp.Fields(), option)
}
func transformNotOperator(op filter.Operator, option *Option) (string, []interface{}, error) {
@ -150,10 +150,10 @@ func transformNotOperator(op filter.Operator, option *Option) (string, []interfa
return "", nil, errors.Wrapf(filter.ErrUnexpectedOperator, "expected '%s', got '%s'", filter.TokenNot, op.Token())
}
sql, args, err := aggregatorToSQL("AND", option, notOp.Children()...)
sql, args, err := aggregatorToSQL(OpAnd, option, notOp.Children()...)
if err != nil {
return "", nil, errors.WithStack(err)
}
return "NOT " + sql, args, nil
return OpNot + " " + sql, args, nil
}

View File

@ -32,7 +32,7 @@ func DefaultTransform(operator string, invert bool, key string, value interface{
return "", nil, errors.WithStack(err)
}
if _, err := sb.WriteString(key); err != nil {
if _, err := sb.WriteString(option.KeyTransform(key)); err != nil {
return "", nil, errors.WithStack(err)
}
} else {

View File

@ -93,15 +93,23 @@ func (s *DocumentStore) Query(ctx context.Context, collection string, filter *fi
var documents []storage.Document
err := s.withTx(ctx, func(tx *sql.Tx) error {
criteria, args, err := filterSQL.ToSQL(
filter.Root(),
filterSQL.WithPreparedParameter("$", 2),
filterSQL.WithKeyTransform(func(key string) string {
return fmt.Sprintf("json_extract(data, '$.%s')", key)
}),
)
if err != nil {
return errors.WithStack(err)
criteria := "1 = 1"
args := make([]any, 0)
var err error
if filter != nil {
criteria, args, err = filterSQL.ToSQL(
filter.Root(),
filterSQL.WithPreparedParameter("$", 2),
filterSQL.WithTransform(transformOperator),
filterSQL.WithKeyTransform(func(key string) string {
return fmt.Sprintf("json_extract(data, '$.%s')", key)
}),
)
if err != nil {
return errors.WithStack(err)
}
}
query := `
@ -180,10 +188,6 @@ func (s *DocumentStore) Upsert(ctx context.Context, collection string, document
id = storage.NewDocumentID()
}
delete(document, storage.DocumentAttrID)
delete(document, storage.DocumentAttrCreatedAt)
delete(document, storage.DocumentAttrUpdatedAt)
args := []any{id, collection, JSONMap(document), now, now}
row := tx.QueryRowContext(ctx, query, args...)

View File

@ -0,0 +1,24 @@
package sqlite
import (
"fmt"
"forge.cadoles.com/arcad/edge/pkg/storage/filter/sql"
)
func transformOperator(operator string, invert bool, key string, value any, option *sql.Option) (string, any, error) {
switch operator {
case sql.OpIn:
return transformInOperator(key, value, option)
default:
return sql.DefaultTransform(operator, invert, key, value, option)
}
}
func transformInOperator(key string, value any, option *sql.Option) (string, any, error) {
return fmt.Sprintf(
"EXISTS (SELECT 1 FROM json_each(json_extract(data, \"$.%v\")) WHERE value = %v)",
key,
option.PreparedParameter(),
), option.ValueTransform(value), nil
}

View File

@ -7,8 +7,8 @@ import (
)
func TestDocumentStore(t *testing.T, store storage.DocumentStore) {
t.Run("Query", func(t *testing.T) {
t.Run("Ops", func(t *testing.T) {
// t.Parallel()
testDocumentStoreQuery(t, store)
testDocumentStoreOps(t, store)
})
}

View File

@ -0,0 +1,212 @@
package testsuite
import (
"context"
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
)
type documentStoreOpsTestCase struct {
Name string
Run func(ctx context.Context, store storage.DocumentStore) error
}
var documentStoreOpsTestCases = []documentStoreOpsTestCase{
{
Name: "Basic query",
Run: func(ctx context.Context, store storage.DocumentStore) error {
collection := "simple_select"
docs := []storage.Document{
{
"attr1": "Foo",
},
{
"attr1": "Bar",
},
}
for _, d := range docs {
if _, err := store.Upsert(ctx, collection, d); err != nil {
return errors.WithStack(err)
}
}
filter := filter.New(
filter.NewEqOperator(map[string]interface{}{
"attr1": "Foo",
}),
)
results, err := store.Query(ctx, collection, filter, nil)
if err != nil {
return errors.WithStack(err)
}
if e, g := 1, len(results); e != g {
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
}
if e, g := "Foo", results[0]["attr1"]; e != g {
return errors.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Query with 'IN' operator",
Run: func(ctx context.Context, store storage.DocumentStore) error {
docs := []storage.Document{
{
"counter": 1,
"tags": []string{"foo", "bar"},
},
{
"counter": 1,
"tags": []string{"nope"},
},
}
collection := "in_operator"
for _, doc := range docs {
if _, err := store.Upsert(ctx, collection, doc); err != nil {
return errors.WithStack(err)
}
}
filter := filter.New(
filter.NewAndOperator(
filter.NewEqOperator(map[string]any{
"counter": 1,
}),
filter.NewInOperator(map[string]any{
"tags": "foo",
}),
),
)
results, err := store.Query(ctx, collection, filter, nil)
if err != nil {
return errors.WithStack(err)
}
if e, g := 1, len(results); e != g {
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
}
return nil
},
},
{
Name: "Double upsert",
Run: func(ctx context.Context, store storage.DocumentStore) error {
collection := "double_upsert"
oriDoc := storage.Document{
"attr1": "Foo",
}
// Upsert document for the first time
upsertedDoc, err := store.Upsert(ctx, collection, oriDoc)
if err != nil {
return errors.WithStack(err)
}
id, exists := upsertedDoc.ID()
if !exists {
return errors.New("id, exists := upsertedDoc.ID(): 'exists' should be true")
}
if id == storage.DocumentID("") {
return errors.New("id, exists := upsertedDoc.ID(): 'id' should not be an empty string")
}
createdAt, exists := upsertedDoc.CreatedAt()
if !exists {
return errors.New("createdAt, exists := upsertedDoc.CreatedAt(): 'exists' should be true")
}
if createdAt.IsZero() {
return errors.New("createdAt, exists := upsertedDoc.CreatedAt(): 'createdAt' should not be zero time")
}
updatedAt, exists := upsertedDoc.UpdatedAt()
if !exists {
return errors.New("updatedAt, exists := upsertedDoc.UpdatedAt(): 'exists' should be true")
}
if updatedAt.IsZero() {
return errors.New("updatedAt, exists := upsertedDoc.UpdatedAt(): 'updatedAt' should not be zero time")
}
if e, g := oriDoc["attr1"], upsertedDoc["attr1"]; e != g {
return errors.Errorf("upsertedDoc[\"attr1\"]: expected '%v', got '%v'", e, g)
}
// Check that document does not have unexpected properties
if e, g := 4, len(upsertedDoc); e != g {
return errors.Errorf("len(upsertedDoc): expected '%v', got '%v'", e, g)
}
// Upsert document for the second time
upsertedDoc2, err := store.Upsert(ctx, collection, upsertedDoc)
if err != nil {
return errors.WithStack(err)
}
spew.Dump(upsertedDoc, upsertedDoc2)
prevID, _ := upsertedDoc.ID()
newID, _ := upsertedDoc2.ID()
if e, g := prevID, newID; e != g {
return errors.Errorf("newID: expected '%v', got '%v'", e, g)
}
createdAt1, _ := upsertedDoc.CreatedAt()
createdAt2, _ := upsertedDoc2.CreatedAt()
if e, g := createdAt1, createdAt2; e != g {
return errors.Errorf("upsertedDoc2.CreatedAt(): expected '%v', got '%v'", e, g)
}
updatedAt1, _ := upsertedDoc.UpdatedAt()
updatedAt2, _ := upsertedDoc2.UpdatedAt()
if e, g := updatedAt1, updatedAt2; e == g {
return errors.New("upsertedDoc2.UpdatedAt() should have been different than upsertedDoc.UpdatedAt()")
}
// Verify that there is no additional created document in the collection
results, err := store.Query(ctx, collection, nil, nil)
if err != nil {
return errors.WithStack(err)
}
if e, g := 1, len(results); e != g {
return errors.Errorf("len(results): expected '%v', got '%v'", e, g)
}
return nil
},
},
}
func testDocumentStoreOps(t *testing.T, store storage.DocumentStore) {
for _, tc := range documentStoreOpsTestCases {
func(tc documentStoreOpsTestCase) {
t.Run(tc.Name, func(t *testing.T) {
if err := tc.Run(context.Background(), store); err != nil {
t.Errorf("%+v", errors.WithStack(err))
}
})
}(tc)
}
}

View File

@ -1,85 +0,0 @@
package testsuite
import (
"context"
"testing"
"forge.cadoles.com/arcad/edge/pkg/storage"
"forge.cadoles.com/arcad/edge/pkg/storage/filter"
"github.com/pkg/errors"
)
type documentStoreQueryTestCase struct {
Name string
Before func(ctx context.Context, store storage.DocumentStore) error
Collection string
Filter *filter.Filter
QueryOptionsFuncs []storage.QueryOptionFunc
After func(t *testing.T, results []storage.Document, err error)
}
var documentStoreQueryTestCases = []documentStoreQueryTestCase{
{
Name: "Simple select",
Before: func(ctx context.Context, store storage.DocumentStore) error {
doc1 := storage.Document{
"attr1": "Foo",
}
if _, err := store.Upsert(ctx, "simple_select", doc1); err != nil {
return errors.WithStack(err)
}
doc2 := storage.Document{
"attr1": "Bar",
}
if _, err := store.Upsert(ctx, "simple_select", doc2); err != nil {
return errors.WithStack(err)
}
return nil
},
Collection: "simple_select",
Filter: filter.New(
filter.NewEqOperator(map[string]interface{}{
"attr1": "Foo",
}),
),
After: func(t *testing.T, results []storage.Document, err error) {
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := 1, len(results); e != g {
t.Errorf("len(results): expected '%v', got '%v'", e, g)
}
if e, g := "Foo", results[0]["attr1"]; e != g {
t.Errorf("results[0][\"Attr1\"]: expected '%v', got '%v'", e, g)
}
},
},
}
func testDocumentStoreQuery(t *testing.T, store storage.DocumentStore) {
for _, tc := range documentStoreQueryTestCases {
func(tc documentStoreQueryTestCase) {
t.Run(tc.Name, func(t *testing.T) {
// t.Parallel()
ctx := context.Background()
if tc.Before != nil {
if err := tc.Before(ctx, store); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
}
documents, err := store.Query(ctx, tc.Collection, tc.Filter, tc.QueryOptionsFuncs...)
tc.After(t, documents, err)
})
}(tc)
}
}