Skip to content
Open
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
43 changes: 42 additions & 1 deletion cmd/vsp/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,41 @@ func runExport(cmd *cobra.Command, args []string) error {

// --- search command ---

// canonicalObjectType maps the documented short forms (CLAS, INTF, PROG, ...)
// to the ADT-canonical group codes the SAP server expects on the
// informationsystem/search endpoint. Unknown values pass through verbatim,
// covering already-canonical input ("CLAS/OC"), namespaced types, or custom codes.
func canonicalObjectType(s string) string {
switch strings.ToUpper(s) {
case "":
return ""
case "CLAS":
return "CLAS/OC"
case "INTF":
return "INTF/OI"
case "PROG":
return "PROG/P"
case "FUGR":
return "FUGR/F"
case "FUNC":
return "FUGR/FF"
case "TABL":
return "TABL/DT"
case "DTEL":
return "DTEL/DE"
case "DOMA":
return "DOMA/DD"
case "DDLS":
return "DDLS/DF"
case "MSAG":
return "MSAG/N"
case "TRAN":
return "TRAN/T"
// TODO: add INCL→PROG/I once https://github.com/oisee/vibing-steampunk/pull/121 is merged upstream
}
return s
}

var searchCmd = &cobra.Command{
Use: "search <query>",
Short: "Search for ABAP objects",
Expand Down Expand Up @@ -285,7 +320,13 @@ func runSearch(cmd *cobra.Command, args []string) error {
query := args[0]
ctx := context.Background()

results, err := client.SearchObject(ctx, query, maxResults)
adtType := canonicalObjectType(objectType)
if v, _ := cmd.Flags().GetBool("verbose"); v {
fmt.Fprintf(os.Stderr, "[DEBUG] search: query=%q objectType=%q maxResults=%d\n",
query, adtType, maxResults)
}

results, err := client.SearchObjectByType(ctx, query, adtType, maxResults)
if err != nil {
return fmt.Errorf("search failed: %w", err)
}
Expand Down
13 changes: 13 additions & 0 deletions pkg/adt/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,16 @@ func (c *Client) AllowPackageTemporarily(pkg string) func() {
// SearchObject searches for ABAP objects by name pattern.
// The query parameter supports wildcards (* for multiple chars, ? for single char).
func (c *Client) SearchObject(ctx context.Context, query string, maxResults int) ([]SearchResult, error) {
return c.SearchObjectByType(ctx, query, "", maxResults)
}

// SearchObjectByType searches for ABAP objects by name pattern, optionally
// constrained to a specific ADT object type code (e.g. "CLAS/OC", "PROG/P",
// "INTF/OI"). An empty objectType means "any type" and behaves identically
// to SearchObject. Server-side type filtering is required when combined with
// maxResults: filtering after the fact silently drops results that didn't
// fit in the pre-filter window.
func (c *Client) SearchObjectByType(ctx context.Context, query, objectType string, maxResults int) ([]SearchResult, error) {
if maxResults <= 0 {
maxResults = 100
}
Expand All @@ -255,6 +265,9 @@ func (c *Client) SearchObject(ctx context.Context, query string, maxResults int)
params.Set("operation", "quickSearch")
params.Set("query", query)
params.Set("maxResults", fmt.Sprintf("%d", maxResults))
if objectType != "" {
params.Set("objectType", objectType)
}

resp, err := c.transport.Request(ctx, "/sap/bc/adt/repository/informationsystem/search", &RequestOptions{
Method: http.MethodGet,
Expand Down
59 changes: 59 additions & 0 deletions pkg/adt/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,65 @@ func TestClient_SearchObject(t *testing.T) {
}
}

func TestClient_SearchObjectByType_QueryParams(t *testing.T) {
emptyResponse := `<?xml version="1.0" encoding="UTF-8"?>
<adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core"/>`

cases := []struct {
name string
objectType string
wantPresent bool
wantValue string
}{
{"with type sends objectType param", "CLAS/OC", true, "CLAS/OC"},
{"empty type omits objectType param", "", false, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
mock := &mockTransportClient{
responses: map[string]*http.Response{
"search": newTestResponse(emptyResponse),
"discovery": newTestResponse("OK"),
},
}

cfg := NewConfig("https://sap.example.com:44300", "user", "pass")
transport := NewTransportWithClient(cfg, mock)
client := NewClientWithTransport(cfg, transport)

if _, err := client.SearchObjectByType(context.Background(), "Z*", tc.objectType, 50); err != nil {
t.Fatalf("SearchObjectByType failed: %v", err)
}

var searchReq *http.Request
for _, r := range mock.requests {
if strings.Contains(r.URL.Path, "informationsystem/search") {
searchReq = r
break
}
}
if searchReq == nil {
t.Fatalf("no search request captured (got %d requests)", len(mock.requests))
}

q := searchReq.URL.Query()
if got := q.Get("query"); got != "Z*" {
t.Errorf("query = %q, want %q", got, "Z*")
}
if got := q.Get("maxResults"); got != "50" {
t.Errorf("maxResults = %q, want %q", got, "50")
}
values, present := q["objectType"]
if present != tc.wantPresent {
t.Errorf("objectType present = %v, want %v (values=%v)", present, tc.wantPresent, values)
}
if tc.wantPresent && (len(values) == 0 || values[0] != tc.wantValue) {
t.Errorf("objectType = %v, want %q", values, tc.wantValue)
}
})
}
}

func TestClient_CheckObjectPackageSafety_NormalizesObjectURLs(t *testing.T) {
tests := []struct {
name string
Expand Down