feat(module,app): basic module to list apps

This commit is contained in:
wpetit 2023-03-22 20:48:09 +01:00
parent 07452ad8ab
commit ed535b6f5d
13 changed files with 399 additions and 7 deletions

View File

@ -1,7 +1,9 @@
package app
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
@ -13,6 +15,8 @@ import (
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
appHTTP "forge.cadoles.com/arcad/edge/pkg/http"
"forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
appModuleMemory "forge.cadoles.com/arcad/edge/pkg/module/app/memory"
"forge.cadoles.com/arcad/edge/pkg/module/auth"
authHTTP "forge.cadoles.com/arcad/edge/pkg/module/auth/http"
"forge.cadoles.com/arcad/edge/pkg/module/blob"
@ -106,6 +110,10 @@ func RunCommand() *cli.Command {
storageFile := injectAppID(ctx.String("storage-file"), manifest.ID)
if err := ensureDir(storageFile); err != nil {
return errors.WithStack(err)
}
db, err := sqlite.Open(storageFile)
if err != nil {
return errors.WithStack(err)
@ -117,7 +125,7 @@ func RunCommand() *cli.Command {
handler := appHTTP.NewHandler(
appHTTP.WithBus(bus),
appHTTP.WithServerModules(getServerModules(bus, ds, bs)...),
appHTTP.WithServerModules(getServerModules(bus, ds, bs, manifest, address)...),
)
if err := handler.Load(bundle); err != nil {
return errors.Wrap(err, "could not load app bundle")
@ -158,7 +166,7 @@ func RunCommand() *cli.Command {
}
}
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore) []app.ServerModuleFactory {
func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStore, manifest *app.Manifest, address string) []app.ServerModuleFactory {
return []app.ServerModuleFactory{
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
@ -190,6 +198,16 @@ func getServerModules(bus bus.Bus, ds storage.DocumentStore, bs storage.BlobStor
}
},
),
appModule.ModuleFactory(appModuleMemory.NewRepository(
func(ctx context.Context, i app.ID) (string, error) {
if strings.HasPrefix(address, ":") {
address = "0.0.0.0" + address
}
return fmt.Sprintf("http://%s", address), nil
},
manifest,
)),
}
}

View File

@ -20,6 +20,7 @@ function onInit() {
Listes des modules disponibles côté serveur.
- [`app`](./app.md)
- [`auth`](./auth.md)
- [`blob`](./blob.md)
- [`cast`](./cast.md)

View File

@ -0,0 +1,57 @@
# Module `app`
Ce module permet de récupérer des informations sur les applications actives dans l'environnement Edge courant.
## Méthodes
### `app.list(ctx: Context): []Manifest`
Récupère la liste des applications actives.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
#### Valeur de retour
Liste des objets `Manifest` décrivant chaque application active.
### `app.get(ctx: Context, appId: string): Manifest`
Récupère les informations de l'application identifiée par `appId`.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
- `appId` **string** Identifiant de l'application
#### Valeur de retour
Objet `Manifest` associé à l'application, ou `null` si aucune application n'a été trouvée correspondant à l'identifiant.
### `app.getUrl(ctx: Context, appId: string): Manifest`
Retourne l'URL permettant d'accéder à l'application identifiée par `appId`.
#### Arguments
- `ctx` **Context** Le contexte d'exécution. Voir la documentation du module [`context`](./context.md)
- `appId` **string** Identifiant de l'application
#### Valeur de retour
URL associée à l'application ou `null`, ou `null` si aucune application n'a été trouvée correspondant à l'identifiant.
## Objets
### `Manifest`
```typescript
interface Manifest {
id: string // Identifiant de l'application
version: string // Version de l'application
title: string // Titre associé à l'application
description: string // Description associée à l'application
tags: string[] // Mots clés associés à l'application
}
```

View File

@ -25,6 +25,7 @@
<script src="/test/net-module.js"></script>
<script src="/test/rpc-module.js"></script>
<script src="/test/file-module.js"></script>
<script src="/test/app-module.js"></script>
<script class="mocha-exec">
mocha.run();
</script>

View File

@ -0,0 +1,37 @@
describe('App Module', function() {
before(() => {
return Edge.connect();
});
after(() => {
Edge.disconnect();
});
it('should list apps', function() {
return Edge.rpc("listApps")
.then(apps => {
console.log("listApps result:", apps);
chai.assert.isNotNull(apps);
chai.assert.isAtLeast(apps.length, 1);
})
});
it('should retrieve requested app', function() {
return Edge.rpc("getApp", { appId: "edge.sdk.client.test" })
.then(app => {
console.log("getApp result:", app);
chai.assert.isNotNull(app);
chai.assert.equal(app.id, "edge.sdk.client.test");
})
});
it('should retrieve requested app url', function() {
return Edge.rpc("getAppUrl", { appId: "edge.sdk.client.test" })
.then(url => {
console.log("getAppUrl result:", url);
chai.assert.isNotEmpty(url);
})
});
});

View File

@ -11,6 +11,10 @@ function onInit() {
rpc.register("reset", reset);
rpc.register("total", total);
rpc.register("getUserInfo", getUserInfo);
rpc.register("listApps");
rpc.register("getApp");
rpc.register("getAppUrl");
}
// Called for each client message
@ -79,4 +83,18 @@ function getUserInfo(ctx, params) {
role: role,
preferredUsername: preferredUsername,
};
}
function listApps(ctx) {
return app.list(ctx);
}
function getApp(ctx, params) {
var appId = params.appId;
return app.get(ctx, appId);
}
function getAppUrl(ctx, params) {
var appId = params.appId;
return app.getUrl(ctx, appId);
}

View File

@ -9,11 +9,11 @@ import (
type ID string
type Manifest struct {
ID ID `yaml:"id"`
Version string `yaml:"version"`
Title string `yaml:"title"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
ID ID `yaml:"id" json:"id"`
Version string `yaml:"version" json:"version"`
Title string `yaml:"title" json:"title"`
Description string `yaml:"description" json:"description"`
Tags []string `yaml:"tags" json:"tags"`
}
func LoadManifest(b bundle.Bundle) (*Manifest, error) {

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

@ -0,0 +1,5 @@
package app
import "errors"
var ErrNotFound = errors.New("not found")

View File

@ -0,0 +1,58 @@
package memory
import (
"context"
"fmt"
"io/ioutil"
"testing"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module"
appModule "forge.cadoles.com/arcad/edge/pkg/module/app"
"github.com/pkg/errors"
)
func TestAppModuleWithMemoryRepository(t *testing.T) {
t.Parallel()
server := app.NewServer(
module.ContextModuleFactory(),
module.ConsoleModuleFactory(),
appModule.ModuleFactory(NewRepository(
func(ctx context.Context, id app.ID) (string, error) {
return fmt.Sprintf("http//%s.example.com", id), nil
},
&app.Manifest{
ID: "dummy1.arcad.app",
Version: "0.0.0",
Title: "Dummy 1",
Description: "Dummy App 1",
Tags: []string{"dummy", "first"},
},
&app.Manifest{
ID: "dummy2.arcad.app",
Version: "0.0.0",
Title: "Dummy 2",
Description: "Dummy App 2",
Tags: []string{"dummy", "second"},
},
)),
)
file := "testdata/app.js"
data, err := ioutil.ReadFile(file)
if err != nil {
t.Fatal(err)
}
if err := server.Load(file, string(data)); err != nil {
t.Fatal(err)
}
defer server.Stop()
if err := server.Start(); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
}

View File

@ -0,0 +1,50 @@
package memory
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
module "forge.cadoles.com/arcad/edge/pkg/module/app"
"github.com/pkg/errors"
)
type GetURLFunc func(context.Context, app.ID) (string, error)
type Repository struct {
getURL GetURLFunc
apps []*app.Manifest
}
// GetURL implements app.Repository
func (r *Repository) GetURL(ctx context.Context, id app.ID) (string, error) {
url, err := r.getURL(ctx, id)
if err != nil {
return "", errors.WithStack(err)
}
return url, nil
}
// Get implements app.Repository
func (r *Repository) Get(ctx context.Context, id app.ID) (*app.Manifest, error) {
for _, app := range r.apps {
if app.ID != id {
continue
}
return app, nil
}
return nil, module.ErrNotFound
}
// List implements app.Repository
func (r *Repository) List(ctx context.Context) ([]*app.Manifest, error) {
return r.apps, nil
}
func NewRepository(getURL GetURLFunc, manifests ...*app.Manifest) *Repository {
return &Repository{getURL, manifests}
}
var _ module.Repository = &Repository{}

17
pkg/module/app/memory/testdata/app.js vendored Normal file
View File

@ -0,0 +1,17 @@
var ctx = context.new();
var manifests = app.list(ctx);
if (manifests.length !== 2) {
throw new Error("apps.length: expected '2', got '"+manifests.length+"'");
}
var manifest = app.get(ctx, 'dummy2.arcad.app');
if (!manifest) {
throw new Error("manifest should not be null");
}
if (manifest.id !== "dummy2.arcad.app") {
throw new Error("manifest.id: expected 'dummy2.arcad.app', got '"+manifest.id+"'");
}

117
pkg/module/app/module.go Normal file
View File

@ -0,0 +1,117 @@
package app
import (
"fmt"
"forge.cadoles.com/arcad/edge/pkg/app"
"forge.cadoles.com/arcad/edge/pkg/module/util"
"github.com/dop251/goja"
"github.com/pkg/errors"
)
type Module struct {
repository Repository
}
type gojaManifest struct {
ID string `goja:"id" json:"id"`
Version string `goja:"version" json:"version"`
Title string `goja:"title" json:"title"`
Description string `goja:"description" json:"description"`
Tags []string `goja:"tags" json:"tags"`
}
func toGojaManifest(manifest *app.Manifest) *gojaManifest {
return &gojaManifest{
ID: string(manifest.ID),
Version: manifest.Version,
Title: manifest.Title,
Description: manifest.Description,
Tags: manifest.Tags,
}
}
func toGojaManifests(manifests []*app.Manifest) []*gojaManifest {
gojaManifests := make([]*gojaManifest, len(manifests))
for i, m := range manifests {
gojaManifests[i] = toGojaManifest(m)
}
return gojaManifests
}
func (m *Module) Name() string {
return "app"
}
func (m *Module) Export(export *goja.Object) {
if err := export.Set("list", m.list); err != nil {
panic(errors.Wrap(err, "could not set 'list' function"))
}
if err := export.Set("get", m.get); err != nil {
panic(errors.Wrap(err, "could not set 'get' function"))
}
if err := export.Set("getUrl", m.getURL); err != nil {
panic(errors.Wrap(err, "could not set 'list' function"))
}
}
func (m *Module) list(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
manifests, err := m.repository.List(ctx)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaManifests(manifests))
}
func (m *Module) get(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
appID := assertAppID(call.Argument(1), rt)
manifest, err := m.repository.Get(ctx, appID)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(toGojaManifest(manifest))
}
func (m *Module) getURL(call goja.FunctionCall, rt *goja.Runtime) goja.Value {
ctx := util.AssertContext(call.Argument(0), rt)
appID := assertAppID(call.Argument(1), rt)
url, err := m.repository.GetURL(ctx, appID)
if err != nil {
panic(rt.ToValue(errors.WithStack(err)))
}
return rt.ToValue(url)
}
func ModuleFactory(repository Repository) app.ServerModuleFactory {
return func(server *app.Server) app.ServerModule {
return &Module{
repository: repository,
}
}
}
func assertAppID(value goja.Value, rt *goja.Runtime) app.ID {
appID, ok := value.Export().(app.ID)
if !ok {
rawAppID, ok := value.Export().(string)
if !ok {
panic(rt.NewTypeError(fmt.Sprintf("app id must be an appid or a string, got '%T'", value.Export())))
}
appID = app.ID(rawAppID)
}
return appID
}

View File

@ -0,0 +1,13 @@
package app
import (
"context"
"forge.cadoles.com/arcad/edge/pkg/app"
)
type Repository interface {
List(context.Context) ([]*app.Manifest, error)
Get(context.Context, app.ID) (*app.Manifest, error)
GetURL(context.Context, app.ID) (string, error)
}