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) +}