Skip to content

Commit 1b200b1

Browse files
API: add "UsedByApps" to the details of a brick (#30)
The API returning the details of a brick show fill in the UsedByApps property. The App Lab uses this to show what examples or apps are using a given brick.* add useByApps field for brick details endpoint * partial test implementation * add test end2end * delete wrong tests * refactoring * make lint happy * code review fixes * fix error message --------- Co-authored-by: mirkoCrobu <m.crobu@ext.arduino.cc>
1 parent a2f7219 commit 1b200b1

File tree

7 files changed

+121
-13
lines changed

7 files changed

+121
-13
lines changed

cmd/arduino-app-cli/brick/bricks.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,18 @@ package brick
1717

1818
import (
1919
"github.com/spf13/cobra"
20+
21+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
2022
)
2123

22-
func NewBrickCmd() *cobra.Command {
24+
func NewBrickCmd(cfg config.Configuration) *cobra.Command {
2325
appCmd := &cobra.Command{
2426
Use: "brick",
2527
Short: "Manage Arduino Bricks",
2628
}
2729

2830
appCmd.AddCommand(newBricksListCmd())
29-
appCmd.AddCommand(newBricksDetailsCmd())
31+
appCmd.AddCommand(newBricksDetailsCmd(cfg))
3032

3133
return appCmd
3234
}

cmd/arduino-app-cli/brick/details.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,23 @@ import (
2525
"github.com/arduino/arduino-app-cli/cmd/arduino-app-cli/internal/servicelocator"
2626
"github.com/arduino/arduino-app-cli/cmd/feedback"
2727
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricks"
28+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
2829
)
2930

30-
func newBricksDetailsCmd() *cobra.Command {
31+
func newBricksDetailsCmd(cfg config.Configuration) *cobra.Command {
3132
return &cobra.Command{
3233
Use: "details",
3334
Short: "Details of a specific brick",
3435
Args: cobra.ExactArgs(1),
3536
Run: func(cmd *cobra.Command, args []string) {
36-
bricksDetailsHandler(args[0])
37+
bricksDetailsHandler(args[0], cfg)
3738
},
3839
}
3940
}
4041

41-
func bricksDetailsHandler(id string) {
42-
res, err := servicelocator.GetBrickService().BricksDetails(id)
42+
func bricksDetailsHandler(id string, cfg config.Configuration) {
43+
res, err := servicelocator.GetBrickService().BricksDetails(id, servicelocator.GetAppIDProvider(),
44+
cfg)
4345
if err != nil {
4446
if errors.Is(err, bricks.ErrBrickNotFound) {
4547
feedback.Fatal(err.Error(), feedback.ErrBadArgument)

cmd/arduino-app-cli/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func run(configuration cfg.Configuration) error {
7171

7272
rootCmd.AddCommand(
7373
app.NewAppCmd(configuration),
74-
brick.NewBrickCmd(),
74+
brick.NewBrickCmd(configuration),
7575
completion.NewCompletionCommand(),
7676
daemon.NewDaemonCmd(configuration, Version),
7777
properties.NewPropertiesCmd(configuration),

internal/api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func NewHTTPRouter(
5656
mux.Handle("GET /v1/version", handlers.HandlerVersion(version))
5757
mux.Handle("GET /v1/config", handlers.HandleConfig(cfg))
5858
mux.Handle("GET /v1/bricks", handlers.HandleBrickList(brickService))
59-
mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService))
59+
mux.Handle("GET /v1/bricks/{brickID}", handlers.HandleBrickDetails(brickService, idProvider, cfg))
6060

6161
mux.Handle("GET /v1/properties", handlers.HandlePropertyKeys(cfg))
6262
mux.Handle("GET /v1/properties/{key}", handlers.HandlePropertyGet(cfg))

internal/api/handlers/bricks.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/arduino/arduino-app-cli/internal/api/models"
2727
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
2828
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricks"
29+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
2930
"github.com/arduino/arduino-app-cli/internal/render"
3031
)
3132

@@ -153,14 +154,15 @@ func HandleBrickCreate(
153154
}
154155
}
155156

156-
func HandleBrickDetails(brickService *bricks.Service) http.HandlerFunc {
157+
func HandleBrickDetails(brickService *bricks.Service, idProvider *app.IDProvider,
158+
cfg config.Configuration) http.HandlerFunc {
157159
return func(w http.ResponseWriter, r *http.Request) {
158160
id := r.PathValue("brickID")
159161
if id == "" {
160162
render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "id must be set"})
161163
return
162164
}
163-
res, err := brickService.BricksDetails(id)
165+
res, err := brickService.BricksDetails(id, idProvider, cfg)
164166
if err != nil {
165167
if errors.Is(err, bricks.ErrBrickNotFound) {
166168
details := fmt.Sprintf("brick with id %q not found", id)

internal/e2e/daemon/brick_test.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,44 @@ import (
2424

2525
"github.com/arduino/go-paths-helper"
2626
"github.com/stretchr/testify/require"
27+
"go.bug.st/f"
2728

2829
"github.com/arduino/arduino-app-cli/internal/api/models"
30+
"github.com/arduino/arduino-app-cli/internal/e2e/client"
2931
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
3032
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
3133
"github.com/arduino/arduino-app-cli/internal/store"
3234
)
3335

36+
func setupTestBrick(t *testing.T) (*client.CreateAppResp, *client.ClientWithResponses) {
37+
httpClient := GetHttpclient(t)
38+
createResp, err := httpClient.CreateAppWithResponse(
39+
t.Context(),
40+
&client.CreateAppParams{SkipSketch: f.Ptr(true)},
41+
client.CreateAppRequest{
42+
Icon: f.Ptr("💻"),
43+
Name: "test-app",
44+
Description: f.Ptr("My app description"),
45+
},
46+
func(ctx context.Context, req *http.Request) error { return nil },
47+
)
48+
require.NoError(t, err)
49+
require.Equal(t, http.StatusCreated, createResp.StatusCode())
50+
require.NotNil(t, createResp.JSON201)
51+
52+
resp, err := httpClient.UpsertAppBrickInstanceWithResponse(
53+
t.Context(),
54+
*createResp.JSON201.Id,
55+
ImageClassifactionBrickID,
56+
client.BrickCreateUpdateRequest{Model: f.Ptr("mobilenet-image-classification")},
57+
func(ctx context.Context, req *http.Request) error { return nil },
58+
)
59+
require.NoError(t, err)
60+
require.Equal(t, http.StatusOK, resp.StatusCode())
61+
62+
return createResp, httpClient
63+
}
64+
3465
func TestBricksList(t *testing.T) {
3566
httpClient := GetHttpclient(t)
3667

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

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

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

110+
expectedUsedByApps := []client.AppReference{
111+
{
112+
Id: f.Ptr("dXNlcjp0ZXN0LWFwcA"),
113+
Name: f.Ptr("test-app"),
114+
Icon: f.Ptr("💻"),
115+
},
116+
}
117+
79118
response, err := httpClient.GetBrickDetailsWithResponse(t.Context(), validBrickID, func(ctx context.Context, req *http.Request) error { return nil })
80119
require.NoError(t, err)
81120
require.Equal(t, http.StatusOK, response.StatusCode(), "status code should be 200 ok")
@@ -92,6 +131,7 @@ func TestBricksDetails(t *testing.T) {
92131
require.Equal(t, "path to the model file", *(*response.JSON200.Variables)["EI_CLASSIFICATION_MODEL"].Description)
93132
require.Equal(t, false, *(*response.JSON200.Variables)["EI_CLASSIFICATION_MODEL"].Required)
94133
require.NotEmpty(t, *response.JSON200.Readme)
95-
require.Nil(t, response.JSON200.UsedByApps)
134+
require.NotNil(t, response.JSON200.UsedByApps, "UsedByApps should not be nil")
135+
require.Equal(t, expectedUsedByApps, *(response.JSON200.UsedByApps))
96136
})
97137
}

internal/orchestrator/bricks/bricks.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package bricks
1818
import (
1919
"errors"
2020
"fmt"
21+
"log/slog"
2122
"maps"
2223
"slices"
2324

@@ -26,6 +27,7 @@ import (
2627

2728
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
2829
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
30+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
2931
"github.com/arduino/arduino-app-cli/internal/orchestrator/modelsindex"
3032
"github.com/arduino/arduino-app-cli/internal/store"
3133
)
@@ -125,7 +127,8 @@ func (s *Service) AppBrickInstanceDetails(a *app.ArduinoApp, brickID string) (Br
125127
}, nil
126128
}
127129

128-
func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
130+
func (s *Service) BricksDetails(id string, idProvider *app.IDProvider,
131+
cfg config.Configuration) (BrickDetailsResult, error) {
129132
brick, found := s.bricksIndex.FindBrickByID(id)
130133
if !found {
131134
return BrickDetailsResult{}, ErrBrickNotFound
@@ -160,6 +163,11 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
160163
}
161164
})
162165

166+
usedByApps, err := getUsedByApps(cfg, brick.ID, idProvider)
167+
if err != nil {
168+
return BrickDetailsResult{}, fmt.Errorf("unable to get used by apps: %w", err)
169+
}
170+
163171
return BrickDetailsResult{
164172
ID: id,
165173
Name: brick.Name,
@@ -171,9 +179,63 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
171179
Readme: readme,
172180
ApiDocsPath: apiDocsPath,
173181
CodeExamples: codeExamples,
182+
UsedByApps: usedByApps,
174183
}, nil
175184
}
176185

186+
func getUsedByApps(
187+
cfg config.Configuration, brickId string, idProvider *app.IDProvider) ([]AppReference, error) {
188+
var (
189+
pathsToExplore paths.PathList
190+
appPaths paths.PathList
191+
)
192+
pathsToExplore.Add(cfg.ExamplesDir())
193+
pathsToExplore.Add(cfg.AppsDir())
194+
usedByApps := []AppReference{}
195+
196+
for _, p := range pathsToExplore {
197+
res, err := p.ReadDirRecursiveFiltered(func(file *paths.Path) bool {
198+
if file.Base() == ".cache" {
199+
return false
200+
}
201+
if file.Join("app.yaml").NotExist() && file.Join("app.yml").NotExist() {
202+
return true
203+
}
204+
return false
205+
}, paths.FilterDirectories(), paths.FilterOutNames("python", "sketch", ".cache"))
206+
if err != nil {
207+
slog.Error("unable to list apps", slog.String("error", err.Error()))
208+
return usedByApps, err
209+
}
210+
appPaths.AddAllMissing(res)
211+
}
212+
213+
for _, file := range appPaths {
214+
app, err := app.Load(file.String())
215+
if err != nil {
216+
// we are not considering the broken apps
217+
slog.Warn("unable to parse app.yaml, skipping", "path", file.String(), "error", err.Error())
218+
continue
219+
}
220+
221+
for _, b := range app.Descriptor.Bricks {
222+
if b.ID == brickId {
223+
id, err := idProvider.IDFromPath(app.FullPath)
224+
if err != nil {
225+
return usedByApps, fmt.Errorf("failed to get app ID for %s: %w", app.FullPath, err)
226+
}
227+
usedByApps = append(usedByApps, AppReference{
228+
Name: app.Name,
229+
ID: id.String(),
230+
Icon: app.Descriptor.Icon,
231+
})
232+
break
233+
}
234+
}
235+
}
236+
return usedByApps, nil
237+
}
238+
177239
type BrickCreateUpdateRequest struct {
178240
ID string `json:"-"`
179241
Model *string `json:"model"`

0 commit comments

Comments
 (0)