diff --git a/cmd/cli/command/app/run.go b/cmd/cli/command/app/run.go
index e28805b..e4d1742 100644
--- a/cmd/cli/command/app/run.go
+++ b/cmd/cli/command/app/run.go
@@ -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,
+ )),
}
}
diff --git a/doc/apps/server-api/README.md b/doc/apps/server-api/README.md
index 5c986c7..c5f067a 100644
--- a/doc/apps/server-api/README.md
+++ b/doc/apps/server-api/README.md
@@ -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)
diff --git a/doc/apps/server-api/app.md b/doc/apps/server-api/app.md
new file mode 100644
index 0000000..90bba8f
--- /dev/null
+++ b/doc/apps/server-api/app.md
@@ -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
+}
+```
diff --git a/misc/client-sdk-testsuite/src/public/index.html b/misc/client-sdk-testsuite/src/public/index.html
index 1f39f5e..c217348 100644
--- a/misc/client-sdk-testsuite/src/public/index.html
+++ b/misc/client-sdk-testsuite/src/public/index.html
@@ -25,6 +25,7 @@
+
diff --git a/misc/client-sdk-testsuite/src/public/test/app-module.js b/misc/client-sdk-testsuite/src/public/test/app-module.js
new file mode 100644
index 0000000..a9c226a
--- /dev/null
+++ b/misc/client-sdk-testsuite/src/public/test/app-module.js
@@ -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);
+ })
+ });
+
+});
\ No newline at end of file
diff --git a/misc/client-sdk-testsuite/src/server/main.js b/misc/client-sdk-testsuite/src/server/main.js
index fe3af2a..0b2fbe3 100644
--- a/misc/client-sdk-testsuite/src/server/main.js
+++ b/misc/client-sdk-testsuite/src/server/main.js
@@ -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);
}
\ No newline at end of file
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 757e454..5d5d81c 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -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) {
diff --git a/pkg/module/app/error.go b/pkg/module/app/error.go
new file mode 100644
index 0000000..98e9c55
--- /dev/null
+++ b/pkg/module/app/error.go
@@ -0,0 +1,5 @@
+package app
+
+import "errors"
+
+var ErrNotFound = errors.New("not found")
diff --git a/pkg/module/app/memory/module_test.go b/pkg/module/app/memory/module_test.go
new file mode 100644
index 0000000..6c434b1
--- /dev/null
+++ b/pkg/module/app/memory/module_test.go
@@ -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))
+ }
+}
diff --git a/pkg/module/app/memory/repository.go b/pkg/module/app/memory/repository.go
new file mode 100644
index 0000000..111bfba
--- /dev/null
+++ b/pkg/module/app/memory/repository.go
@@ -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{}
diff --git a/pkg/module/app/memory/testdata/app.js b/pkg/module/app/memory/testdata/app.js
new file mode 100644
index 0000000..3c5e1f6
--- /dev/null
+++ b/pkg/module/app/memory/testdata/app.js
@@ -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+"'");
+}
\ No newline at end of file
diff --git a/pkg/module/app/module.go b/pkg/module/app/module.go
new file mode 100644
index 0000000..56b85dd
--- /dev/null
+++ b/pkg/module/app/module.go
@@ -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
+}
diff --git a/pkg/module/app/repository.go b/pkg/module/app/repository.go
new file mode 100644
index 0000000..030295d
--- /dev/null
+++ b/pkg/module/app/repository.go
@@ -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)
+}