Skip to content

Commit 7508a4b

Browse files
lucarin91mirkoCrobu
andcommitted
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 d43f0e5 commit 7508a4b

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,13 +18,15 @@ package bricks
1818
import (
1919
"errors"
2020
"fmt"
21+
"log/slog"
2122
"slices"
2223

2324
"github.com/arduino/go-paths-helper"
2425
"go.bug.st/f"
2526

2627
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
2728
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
29+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
2830
"github.com/arduino/arduino-app-cli/internal/orchestrator/modelsindex"
2931
"github.com/arduino/arduino-app-cli/internal/store"
3032
)
@@ -151,7 +153,8 @@ func getBrickVariableDetails(
151153
return variablesMap, variableDetails
152154
}
153155

154-
func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
156+
func (s *Service) BricksDetails(id string, idProvider *app.IDProvider,
157+
cfg config.Configuration) (BrickDetailsResult, error) {
155158
brick, found := s.bricksIndex.FindBrickByID(id)
156159
if !found {
157160
return BrickDetailsResult{}, ErrBrickNotFound
@@ -186,6 +189,11 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
186189
}
187190
})
188191

192+
usedByApps, err := getUsedByApps(cfg, brick.ID, idProvider)
193+
if err != nil {
194+
return BrickDetailsResult{}, fmt.Errorf("unable to get used by apps: %w", err)
195+
}
196+
189197
return BrickDetailsResult{
190198
ID: id,
191199
Name: brick.Name,
@@ -197,9 +205,63 @@ func (s *Service) BricksDetails(id string) (BrickDetailsResult, error) {
197205
Readme: readme,
198206
ApiDocsPath: apiDocsPath,
199207
CodeExamples: codeExamples,
208+
UsedByApps: usedByApps,
200209
}, nil
201210
}
202211

212+
func getUsedByApps(
213+
cfg config.Configuration, brickId string, idProvider *app.IDProvider) ([]AppReference, error) {
214+
var (
215+
pathsToExplore paths.PathList
216+
appPaths paths.PathList
217+
)
218+
pathsToExplore.Add(cfg.ExamplesDir())
219+
pathsToExplore.Add(cfg.AppsDir())
220+
usedByApps := []AppReference{}
221+
222+
for _, p := range pathsToExplore {
223+
res, err := p.ReadDirRecursiveFiltered(func(file *paths.Path) bool {
224+
if file.Base() == ".cache" {
225+
return false
226+
}
227+
if file.Join("app.yaml").NotExist() && file.Join("app.yml").NotExist() {
228+
return true
229+
}
230+
return false
231+
}, paths.FilterDirectories(), paths.FilterOutNames("python", "sketch", ".cache"))
232+
if err != nil {
233+
slog.Error("unable to list apps", slog.String("error", err.Error()))
234+
return usedByApps, err
235+
}
236+
appPaths.AddAllMissing(res)
237+
}
238+
239+
for _, file := range appPaths {
240+
app, err := app.Load(file.String())
241+
if err != nil {
242+
// we are not considering the broken apps
243+
slog.Warn("unable to parse app.yaml, skipping", "path", file.String(), "error", err.Error())
244+
continue
245+
}
246+
247+
for _, b := range app.Descriptor.Bricks {
248+
if b.ID == brickId {
249+
id, err := idProvider.IDFromPath(app.FullPath)
250+
if err != nil {
251+
return usedByApps, fmt.Errorf("failed to get app ID for %s: %w", app.FullPath, err)
252+
}
253+
usedByApps = append(usedByApps, AppReference{
254+
Name: app.Name,
255+
ID: id.String(),
256+
Icon: app.Descriptor.Icon,
257+
})
258+
break
259+
}
260+
}
261+
}
262+
return usedByApps, nil
263+
}
264+
203265
type BrickCreateUpdateRequest struct {
204266
ID string `json:"-"`
205267
Model *string `json:"model"`

0 commit comments

Comments
 (0)