Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions cmd/arduino-app-cli/brick/bricks.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ package brick

import (
"github.com/spf13/cobra"

"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
)

func NewBrickCmd() *cobra.Command {
func NewBrickCmd(cfg config.Configuration) *cobra.Command {
appCmd := &cobra.Command{
Use: "brick",
Short: "Manage Arduino Bricks",
}

appCmd.AddCommand(newBricksListCmd())
appCmd.AddCommand(newBricksDetailsCmd())
appCmd.AddCommand(newBricksDetailsCmd(cfg))

return appCmd
}
10 changes: 6 additions & 4 deletions cmd/arduino-app-cli/brick/details.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,23 @@ import (
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/internal/servicelocator"
"github.com/arduino/arduino-app-cli/cmd/feedback"
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricks"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
)

func newBricksDetailsCmd() *cobra.Command {
func newBricksDetailsCmd(cfg config.Configuration) *cobra.Command {
return &cobra.Command{
Use: "details",
Short: "Details of a specific brick",
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
bricksDetailsHandler(args[0])
bricksDetailsHandler(args[0], cfg)
},
}
}

func bricksDetailsHandler(id string) {
res, err := servicelocator.GetBrickService().BricksDetails(id)
func bricksDetailsHandler(id string, cfg config.Configuration) {
res, err := servicelocator.GetBrickService().BricksDetails(id, servicelocator.GetAppIDProvider(),
cfg)
if err != nil {
if errors.Is(err, bricks.ErrBrickNotFound) {
feedback.Fatal(err.Error(), feedback.ErrBadArgument)
Expand Down
2 changes: 1 addition & 1 deletion cmd/arduino-app-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func run(configuration cfg.Configuration) error {

rootCmd.AddCommand(
app.NewAppCmd(configuration),
brick.NewBrickCmd(),
brick.NewBrickCmd(configuration),
completion.NewCompletionCommand(),
daemon.NewDaemonCmd(configuration, Version),
properties.NewPropertiesCmd(configuration),
Expand Down
2 changes: 1 addition & 1 deletion internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func NewHTTPRouter(
mux.Handle("GET /v1/version", handlers.HandlerVersion(version))
mux.Handle("GET /v1/config", handlers.HandleConfig(cfg))
mux.Handle("GET /v1/bricks", handlers.HandleBrickList(brickService))
mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService))
mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService, idProvider, cfg))

mux.Handle("GET /v1/properties", handlers.HandlePropertyKeys(cfg))
mux.Handle("GET /v1/properties/{key}", handlers.HandlePropertyGet(cfg))
Expand Down
6 changes: 4 additions & 2 deletions internal/api/handlers/bricks.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/arduino/arduino-app-cli/internal/api/models"
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricks"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
"github.com/arduino/arduino-app-cli/internal/render"
)

Expand Down Expand Up @@ -153,14 +154,15 @@ func HandleBrickCreate(
}
}

func HandleBrickDetails(brickService *bricks.Service) http.HandlerFunc {
func HandleBrickDetails(brickService *bricks.Service, idProvider *app.IDProvider,
cfg config.Configuration) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("brickID")
if id == "" {
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "id must be set"})
return
}
res, err := brickService.BricksDetails(id)
res, err := brickService.BricksDetails(id, idProvider, cfg)
if err != nil {
if errors.Is(err, bricks.ErrBrickNotFound) {
details := fmt.Sprintf("brick with id %q not found", id)
Expand Down
44 changes: 42 additions & 2 deletions internal/e2e/daemon/brick_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,44 @@ import (

"github.com/arduino/go-paths-helper"
"github.com/stretchr/testify/require"
"go.bug.st/f"

"github.com/arduino/arduino-app-cli/internal/api/models"
"github.com/arduino/arduino-app-cli/internal/e2e/client"
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
"github.com/arduino/arduino-app-cli/internal/store"
)

func setupTestBrick(t *testing.T) (*client.CreateAppResp, *client.ClientWithResponses) {
httpClient := GetHttpclient(t)
createResp, err := httpClient.CreateAppWithResponse(
t.Context(),
&client.CreateAppParams{SkipSketch: f.Ptr(true)},
client.CreateAppRequest{
Icon: f.Ptr("💻"),
Name: "test-app",
Description: f.Ptr("My app description"),
},
func(ctx context.Context, req *http.Request) error { return nil },
)
require.NoError(t, err)
require.Equal(t, http.StatusCreated, createResp.StatusCode())
require.NotNil(t, createResp.JSON201)

resp, err := httpClient.UpsertAppBrickInstanceWithResponse(
t.Context(),
*createResp.JSON201.Id,
ImageClassifactionBrickID,
client.BrickCreateUpdateRequest{Model: f.Ptr("mobilenet-image-classification")},
func(ctx context.Context, req *http.Request) error { return nil },
)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode())

return createResp, httpClient
}

func TestBricksList(t *testing.T) {
httpClient := GetHttpclient(t)

Expand All @@ -56,8 +87,8 @@ func TestBricksList(t *testing.T) {
}

func TestBricksDetails(t *testing.T) {
_, httpClient := setupTestBrick(t)

httpClient := GetHttpclient(t)
t.Run("should return 404 Not Found for an invalid brick ID", func(t *testing.T) {
invalidBrickID := "notvalidBrickId"
var actualBody models.ErrorResponse
Expand All @@ -76,6 +107,14 @@ func TestBricksDetails(t *testing.T) {
t.Run("should return 200 OK with full details for a valid brick ID", func(t *testing.T) {
validBrickID := "arduino:image_classification"

expectedUsedByApps := []client.AppReference{
{
Id: f.Ptr("dXNlcjp0ZXN0LWFwcA"),
Name: f.Ptr("test-app"),
Icon: f.Ptr("💻"),
},
}

response, err := httpClient.GetBrickDetailsWithResponse(t.Context(), validBrickID, func(ctx context.Context, req *http.Request) error { return nil })
require.NoError(t, err)
require.Equal(t, http.StatusOK, response.StatusCode(), "status code should be 200 ok")
Expand All @@ -92,6 +131,7 @@ func TestBricksDetails(t *testing.T) {
require.Equal(t, "path to the model file", *(*response.JSON200.Variables)["EI_CLASSIFICATION_MODEL"].Description)
require.Equal(t, false, *(*response.JSON200.Variables)["EI_CLASSIFICATION_MODEL"].Required)
require.NotEmpty(t, *response.JSON200.Readme)
require.Nil(t, response.JSON200.UsedByApps)
require.NotNil(t, response.JSON200.UsedByApps, "UsedByApps should not be nil")
require.Equal(t, expectedUsedByApps, *(response.JSON200.UsedByApps))
})
}
64 changes: 63 additions & 1 deletion internal/orchestrator/bricks/bricks.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package bricks
import (
"errors"
"fmt"
"log/slog"
"maps"
"slices"

Expand All @@ -26,6 +27,7 @@ import (

"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
"github.com/arduino/arduino-app-cli/internal/orchestrator/modelsindex"
"github.com/arduino/arduino-app-cli/internal/store"
)
Expand Down Expand Up @@ -125,7 +127,8 @@ func (s *Service) AppBrickInstanceDetails(a *app.ArduinoApp, brickID string) (Br
}, nil
}

func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
func (s *Service) BricksDetails(id string, idProvider *app.IDProvider,
cfg config.Configuration) (BrickDetailsResult, error) {
brick, found := s.bricksIndex.FindBrickByID(id)
if !found {
return BrickDetailsResult{}, ErrBrickNotFound
Expand Down Expand Up @@ -160,6 +163,11 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
}
})

usedByApps, err := getUsedByApps(cfg, brick.ID, idProvider)
if err != nil {
return BrickDetailsResult{}, fmt.Errorf("unable to get used by apps: %w", err)
}

return BrickDetailsResult{
ID: id,
Name: brick.Name,
Expand All @@ -171,9 +179,63 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
Readme: readme,
ApiDocsPath: apiDocsPath,
CodeExamples: codeExamples,
UsedByApps: usedByApps,
}, nil
}

func getUsedByApps(
cfg config.Configuration, brickId string, idProvider *app.IDProvider) ([]AppReference, error) {
var (
pathsToExplore paths.PathList
appPaths paths.PathList
)
pathsToExplore.Add(cfg.ExamplesDir())
pathsToExplore.Add(cfg.AppsDir())
usedByApps := []AppReference{}

for _, p := range pathsToExplore {
res, err := p.ReadDirRecursiveFiltered(func(file *paths.Path) bool {
if file.Base() == ".cache" {
return false
}
if file.Join("app.yaml").NotExist() && file.Join("app.yml").NotExist() {
return true
}
return false
}, paths.FilterDirectories(), paths.FilterOutNames("python", "sketch", ".cache"))
if err != nil {
slog.Error("unable to list apps", slog.String("error", err.Error()))
return usedByApps, err
}
appPaths.AddAllMissing(res)
}

for _, file := range appPaths {
app, err := app.Load(file.String())
if err != nil {
// we are not considering the broken apps
slog.Warn("unable to parse app.yaml, skipping", "path", file.String(), "error", err.Error())
continue
}

for _, b := range app.Descriptor.Bricks {
if b.ID == brickId {
id, err := idProvider.IDFromPath(app.FullPath)
if err != nil {
return usedByApps, fmt.Errorf("failed to get app ID for %s: %w", app.FullPath, err)
}
usedByApps = append(usedByApps, AppReference{
Name: app.Name,
ID: id.String(),
Icon: app.Descriptor.Icon,
})
break
}
}
}
return usedByApps, nil
}

type BrickCreateUpdateRequest struct {
ID string `json:"-"`
Model *string `json:"model"`
Expand Down