diff --git a/cmd/arduino-app-cli/brick/list.go b/cmd/arduino-app-cli/brick/list.go index e3690e728..5dbae6fb7 100644 --- a/cmd/arduino-app-cli/brick/list.go +++ b/cmd/arduino-app-cli/brick/list.go @@ -36,8 +36,9 @@ func newBricksListCmd() *cobra.Command { }, } } + func bricksListHandler() { - res, err := servicelocator.GetBrickService().List() + res, err := servicelocator.GetBrickService().List(nil) if err != nil { feedback.Fatal(err.Error(), feedback.ErrGeneric) } diff --git a/cmd/arduino-app-cli/completion/completion.go b/cmd/arduino-app-cli/completion/completion.go index 9137e37f6..a793c9693 100644 --- a/cmd/arduino-app-cli/completion/completion.go +++ b/cmd/arduino-app-cli/completion/completion.go @@ -107,7 +107,7 @@ func BrickIDs() cobra.CompletionFunc { func BrickIDsWithFilterFunc(filter func(apps bricks.BrickListItem) bool) cobra.CompletionFunc { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - brickList, err := servicelocator.GetBrickService().List() + brickList, err := servicelocator.GetBrickService().List(nil) if err != nil { return nil, cobra.ShellCompDirectiveError } diff --git a/cmd/gendoc/docs.go b/cmd/gendoc/docs.go index ba8272373..18c4e2450 100644 --- a/cmd/gendoc/docs.go +++ b/cmd/gendoc/docs.go @@ -776,7 +776,9 @@ Contains a JSON object with the details of an error. OperationId: "getBricks", Method: http.MethodGet, Path: "/v1/bricks", - Request: nil, + Request: (*struct { + SupportedOnly bool `query:"supported_only" json:"supported_only" description:"If true, only returns bricks supported by the current board."` + })(nil), CustomSuccessResponse: &CustomResponseDef{ ContentType: "application/json", DataStructure: bricks.BrickListResult{}, diff --git a/internal/api/docs/openapi.yaml b/internal/api/docs/openapi.yaml index ec019c347..786ac2ca3 100644 --- a/internal/api/docs/openapi.yaml +++ b/internal/api/docs/openapi.yaml @@ -799,6 +799,13 @@ paths: description: Returns all the existing bricks. Bricks that are ready to use are marked as installed. operationId: getBricks + parameters: + - description: If true, only returns bricks supported by the current board. + in: query + name: supported_only + schema: + description: If true, only returns bricks supported by the current board. + type: boolean responses: "200": content: @@ -1505,8 +1512,6 @@ components: type: object AppLocalBrickCreateRequest: properties: - description: - type: string name: type: string type: object @@ -1623,6 +1628,8 @@ components: type: string name: type: string + readme: + type: string require_model: type: boolean status: diff --git a/internal/api/handlers/bricks.go b/internal/api/handlers/bricks.go index 5eb120f57..899e4b003 100644 --- a/internal/api/handlers/bricks.go +++ b/internal/api/handlers/bricks.go @@ -24,17 +24,29 @@ import ( "log" "log/slog" "net/http" + "strconv" "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/bricksindex" "github.com/arduino/arduino-app-cli/internal/orchestrator/config" "github.com/arduino/arduino-app-cli/internal/render" ) func HandleBrickList(brickService *bricks.Service) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - res, err := brickService.List() + var brickFilter bricksindex.BrickFilter = nil + + params := r.URL.Query() + filterSupported := params.Get("supported_only") + if filterSupported != "" { + if supported, err := strconv.ParseBool(filterSupported); err == nil && supported { + brickFilter = bricksindex.UnsupportedBrickFilter + } + } + + res, err := brickService.List(brickFilter) if err != nil { slog.Error("Unable to parse the app.yaml", slog.String("error", err.Error())) render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to retrieve brick list"}) diff --git a/internal/e2e/client/client.gen.go b/internal/e2e/client/client.gen.go index 199ef5698..ca3d6eaea 100644 --- a/internal/e2e/client/client.gen.go +++ b/internal/e2e/client/client.gen.go @@ -170,8 +170,7 @@ type AppListResponse struct { // AppLocalBrickCreateRequest defines model for AppLocalBrickCreateRequest. type AppLocalBrickCreateRequest struct { - Description *string `json:"description,omitempty"` - Name *string `json:"name,omitempty"` + Name *string `json:"name,omitempty"` } // AppLocalBrickCreateResponse defines model for AppLocalBrickCreateResponse. @@ -235,6 +234,7 @@ type BrickInstance struct { Id *string `json:"id,omitempty"` Model *string `json:"model,omitempty"` Name *string `json:"name,omitempty"` + Readme *string `json:"readme,omitempty"` RequireModel *bool `json:"require_model,omitempty"` Status *string `json:"status,omitempty"` @@ -524,6 +524,12 @@ type GetAppLogsParams struct { Nofollow *bool `form:"nofollow,omitempty" json:"nofollow,omitempty"` } +// GetBricksParams defines parameters for GetBricks. +type GetBricksParams struct { + // SupportedOnly If true, only returns bricks supported by the current board. + SupportedOnly *bool `form:"supported_only,omitempty" json:"supported_only,omitempty"` +} + // ListLibrariesParams defines parameters for ListLibraries. type ListLibrariesParams struct { // Search Search term to filter libraries by name, sentence, paragraph. @@ -771,7 +777,7 @@ type ClientInterface interface { StopApp(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) // GetBricks request - GetBricks(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + GetBricks(ctx context.Context, params *GetBricksParams, reqEditors ...RequestEditorFn) (*http.Response, error) // GetBrickDetails request GetBrickDetails(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1186,8 +1192,8 @@ func (c *Client) StopApp(ctx context.Context, id string, reqEditors ...RequestEd return c.Client.Do(req) } -func (c *Client) GetBricks(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewGetBricksRequest(c.Server) +func (c *Client) GetBricks(ctx context.Context, params *GetBricksParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetBricksRequest(c.Server, params) if err != nil { return nil, err } @@ -1681,7 +1687,7 @@ func NewCreateAppLocalBrickRequestWithBody(server string, appID string, contentT var pathParam0 string - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "appID", runtime.ParamLocationPath, appID) + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "appID", appID, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) if err != nil { return nil, err } @@ -2504,7 +2510,7 @@ func NewStopAppRequest(server string, id string) (*http.Request, error) { } // NewGetBricksRequest generates requests for GetBricks -func NewGetBricksRequest(server string) (*http.Request, error) { +func NewGetBricksRequest(server string, params *GetBricksParams) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -2522,6 +2528,28 @@ func NewGetBricksRequest(server string) (*http.Request, error) { return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if params.SupportedOnly != nil { + + if queryFrag, err := runtime.StyleParamWithOptions("form", true, "supported_only", *params.SupportedOnly, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationQuery, Type: "boolean", Format: ""}); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -3367,7 +3395,7 @@ type ClientWithResponsesInterface interface { StopAppWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*StopAppResp, error) // GetBricksWithResponse request - GetBricksWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetBricksResp, error) + GetBricksWithResponse(ctx context.Context, params *GetBricksParams, reqEditors ...RequestEditorFn) (*GetBricksResp, error) // GetBrickDetailsWithResponse request GetBrickDetailsWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*GetBrickDetailsResp, error) @@ -4651,8 +4679,8 @@ func (c *ClientWithResponses) StopAppWithResponse(ctx context.Context, id string } // GetBricksWithResponse request returning *GetBricksResp -func (c *ClientWithResponses) GetBricksWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetBricksResp, error) { - rsp, err := c.GetBricks(ctx, reqEditors...) +func (c *ClientWithResponses) GetBricksWithResponse(ctx context.Context, params *GetBricksParams, reqEditors ...RequestEditorFn) (*GetBricksResp, error) { + rsp, err := c.GetBricks(ctx, params, reqEditors...) if err != nil { return nil, err } diff --git a/internal/e2e/daemon/brick_test.go b/internal/e2e/daemon/brick_test.go index d4b7467d8..50016fd60 100644 --- a/internal/e2e/daemon/brick_test.go +++ b/internal/e2e/daemon/brick_test.go @@ -67,7 +67,7 @@ func setupTestBrick(t *testing.T) (*client.CreateAppResp, *client.ClientWithResp func TestBricksList(t *testing.T) { httpClient := GetHttpclient(t) - response, err := httpClient.GetBricksWithResponse(t.Context(), func(ctx context.Context, req *http.Request) error { return nil }) + response, err := httpClient.GetBricksWithResponse(t.Context(), &client.GetBricksParams{}) require.NoError(t, err) require.NotEmpty(t, response.JSON200.Bricks) diff --git a/internal/orchestrator/bricks/bricks.go b/internal/orchestrator/bricks/bricks.go index e490b3d44..b724d0a4d 100644 --- a/internal/orchestrator/bricks/bricks.go +++ b/internal/orchestrator/bricks/bricks.go @@ -53,9 +53,9 @@ func NewService( } } -func (s *Service) List() (BrickListResult, error) { - res := BrickListResult{Bricks: make([]BrickListItem, len(s.bricksIndex.ListBricks()))} - for i, brick := range s.bricksIndex.ListBricks() { +func (s *Service) List(filter bricksindex.BrickFilter) (BrickListResult, error) { + res := BrickListResult{Bricks: make([]BrickListItem, len(s.bricksIndex.ListBricks(nil)))} + for i, brick := range s.bricksIndex.ListBricks(filter) { res.Bricks[i] = BrickListItem{ ID: brick.ID, Name: brick.Name, diff --git a/internal/orchestrator/bricksindex/bricks_index.go b/internal/orchestrator/bricksindex/bricks_index.go index f300ea705..d91fd5bef 100644 --- a/internal/orchestrator/bricksindex/bricks_index.go +++ b/internal/orchestrator/bricksindex/bricks_index.go @@ -30,6 +30,7 @@ import ( yaml "github.com/goccy/go-yaml" "github.com/arduino/arduino-app-cli/internal/orchestrator/peripherals" + "github.com/arduino/arduino-app-cli/internal/platform" ) type BricksIndex struct { @@ -55,12 +56,19 @@ func (b *BricksIndex) FindBrickByID(id string) (*Brick, bool) { return nil, false } +type BrickFilter func(brick Brick) bool + // TODO: use iterator instead of returning a slice -func (b *BricksIndex) ListBricks() []Brick { +func (b *BricksIndex) ListBricks(filter BrickFilter) []Brick { bricks := slices.Concat(b.AppBricks, b.BuiltInBricks) slices.SortFunc(bricks, func(a, b Brick) int { return strings.Compare(a.Name, b.Name) }) + + // Filters out bricks where the provided function evaluates to true. + if filter != nil { + bricks = slices.DeleteFunc(bricks, filter) + } return bricks } @@ -80,6 +88,7 @@ type Brick struct { ID string `yaml:"id"` Name string `yaml:"name"` Description string `yaml:"description"` + SupportedBoards []string `yaml:"supported_boards,omitempty"` Category string `yaml:"category,omitempty"` RequiresDisplay string `yaml:"requires_display,omitempty"` RequireContainer bool `yaml:"require_container"` @@ -202,3 +211,21 @@ func parseBrickID(brickID string) (namespace, name string, err error) { } return namespace, brickName, nil } + +func GetBoard() string { + fqbn := platform.GetPlatform().FQBN + i := strings.LastIndex(fqbn, ":") + if i != -1 { + return fqbn[i+1:] + } + return "" +} + +var boardProvider = GetBoard() + +func UnsupportedBrickFilter(b Brick) bool { + if len(b.SupportedBoards) == 0 { + return false + } + return !slices.Contains(b.SupportedBoards, boardProvider) +} diff --git a/internal/orchestrator/bricksindex/bricks_index_test.go b/internal/orchestrator/bricksindex/bricks_index_test.go index 95be2a330..5148391b7 100644 --- a/internal/orchestrator/bricksindex/bricks_index_test.go +++ b/internal/orchestrator/bricksindex/bricks_index_test.go @@ -322,3 +322,90 @@ func TestLoadBrickYamlBrickIndex(t *testing.T) { }) } + +func TestListBricksWithFilter(t *testing.T) { + brick1 := Brick{ID: "1", Name: "brick1", Category: "catA"} + brick2 := Brick{ID: "2", Name: "brick2", Category: "catB"} + brick3 := Brick{ID: "3", Name: "brick3", Category: "catB"} + appBricks := []Brick{brick1, brick2, brick3} + + tests := []struct { + name string + filter BrickFilter + wantBricks []Brick + }{ + { + name: "Nil filter does not apply any filter", + filter: nil, + wantBricks: appBricks, + }, + { + name: "Filters out catB bricks", + filter: func(b Brick) bool { return b.Category == "catB" }, + wantBricks: []Brick{brick1}, + }, + { + name: "Filters out catA bricks", + filter: func(b Brick) bool { return b.Category == "catA" }, + wantBricks: []Brick{brick2, brick3}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &BricksIndex{ + AppBricks: appBricks, + } + + // act + got := b.ListBricks(tt.filter) + require.Equal(t, len(tt.wantBricks), len(got)) + require.Equal(t, tt.wantBricks[0].ID, got[0].ID) + }) + } +} + +func TestUnsupportedBoardFilter(t *testing.T) { + brickEmptySupportedList := Brick{ID: "1", Name: "brick1", SupportedBoards: []string{}} + brickMyBoard := Brick{ID: "2", Name: "brick2", SupportedBoards: []string{"MyBoard"}} + brickOneQ := Brick{ID: "3", Name: "brickOneQ", SupportedBoards: []string{"UnoQ"}} + brickOneQVentunoQ := Brick{ID: "4", Name: "brickOneQVentunoQ", SupportedBoards: []string{"UnoQ", "VentunoQ"}} + brickVentunoQ := Brick{ID: "5", Name: "brickVentunoQ", SupportedBoards: []string{"VentunoQ"}} + + boardProvider = "UnoQ" + + tests := []struct { + name string + initialBricks []Brick + wantBricks []Brick + }{ + { + name: "Empty board list means all supported", + initialBricks: []Brick{brickEmptySupportedList}, + wantBricks: []Brick{brickEmptySupportedList}, + }, + { + name: "Only OneQ board supported bricks with two candidate bricks", + initialBricks: []Brick{brickOneQ, brickOneQVentunoQ, brickVentunoQ}, + wantBricks: []Brick{brickOneQ, brickOneQVentunoQ}, + }, + { + name: "Only OneQ board supported bricks with no candidate bricks", + initialBricks: []Brick{brickMyBoard, brickVentunoQ}, + wantBricks: []Brick{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := &BricksIndex{ + AppBricks: tt.initialBricks, + } + + // act + got := b.ListBricks(UnsupportedBrickFilter) + require.Equal(t, len(tt.wantBricks), len(got)) + require.Equal(t, tt.wantBricks, got) + }) + } +}