From e49016e6e1ab93fb2ba7f31487ca4b5d0737eead Mon Sep 17 00:00:00 2001 From: Roman Novatorov Date: Fri, 15 May 2026 11:27:20 +0200 Subject: [PATCH 1/2] fix: handle legacy rule engine pagination --- .../enaptercli/cmd_rule_engine_rule_list.go | 86 ++++++++++++++++++- .../http_req_resp/rule_engine_rule_list/req_0 | 2 +- .../http_req_resp/rule_engine_rule_list/req_1 | 2 +- .../http_req_resp/rule_engine_rule_list/req_2 | 19 ++++ .../rule_engine_rule_list/resp_1 | 2 +- .../rule_engine_rule_list/resp_2 | 1 + .../cmd.tmpl | 2 + .../out | 1 + .../req_0 | 19 ++++ .../resp_0 | 1 + 10 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_2 create mode 100644 internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_2 create mode 100644 internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/cmd.tmpl create mode 100644 internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/out create mode 100644 internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/req_0 create mode 100644 internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/resp_0 diff --git a/internal/app/enaptercli/cmd_rule_engine_rule_list.go b/internal/app/enaptercli/cmd_rule_engine_rule_list.go index 40bd2a1..79d0886 100644 --- a/internal/app/enaptercli/cmd_rule_engine_rule_list.go +++ b/internal/app/enaptercli/cmd_rule_engine_rule_list.go @@ -1,7 +1,11 @@ package enaptercli import ( + "bytes" "context" + "encoding/json" + "fmt" + "io" "net/http" "github.com/urfave/cli/v3" @@ -37,7 +41,83 @@ func (c *cmdRuleEngineRuleList) Flags() []cli.Flag { } func (c *cmdRuleEngineRuleList) do(ctx context.Context) error { - doPaginateRequestParams := paginateHTTPRequestParams{ + body, err := c.doProbeRequest(ctx) + if err != nil { + return err + } + + if len(body.Rules) == 0 { + return c.printEmptyResponse() + } + + if body.TotalCount == 0 { + return c.doLegacyUnpaginatedPath(body) + } + + return c.doPaginatedPath(ctx) +} + +type ruleListResponse struct { + Rules []json.RawMessage `json:"rules"` + TotalCount int `json:"total_count"` +} + +func (c *cmdRuleEngineRuleList) doProbeRequest(ctx context.Context) (*ruleListResponse, error) { + var body ruleListResponse + err := c.doHTTPRequest(ctx, doHTTPRequestParams{ + Method: http.MethodGet, + Path: "", + RespProcessor: func(resp *http.Response) error { + if resp.StatusCode != http.StatusOK { + return cli.Exit(parseRespErrorMessage(resp), 1) + } + if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { + return fmt.Errorf("decode response: %w", err) + } + return nil + }, + }) + if err != nil { + return nil, err + } + return &body, nil +} + +func (c *cmdRuleEngineRuleList) printEmptyResponse() error { + respBytes, err := json.Marshal(map[string]any{ + "rules": []json.RawMessage{}, + "total_count": 0, + }) + if err != nil { + return fmt.Errorf("marshal empty response: %w", err) + } + + return c.defaultRespProcessor(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(respBytes)), + }) +} + +func (c *cmdRuleEngineRuleList) doLegacyUnpaginatedPath(body *ruleListResponse) error { + body.TotalCount = len(body.Rules) + + if c.limit > 0 && len(body.Rules) > c.limit { + body.Rules = body.Rules[:c.limit] + } + + respBytes, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("marshal legacy response: %w", err) + } + + return c.defaultRespProcessor(&http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(respBytes)), + }) +} + +func (c *cmdRuleEngineRuleList) doPaginatedPath(ctx context.Context) error { + return c.doPaginateRequest(ctx, paginateHTTPRequestParams{ ObjectName: "rules", Limit: c.limit, DoFn: c.doHTTPRequest, @@ -45,7 +125,5 @@ func (c *cmdRuleEngineRuleList) do(ctx context.Context) error { Method: http.MethodGet, Path: "", }, - } - - return c.doPaginateRequest(ctx, doPaginateRequestParams) + }) } diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_0 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_0 index 71d46fc..56d58fe 100644 --- a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_0 +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_0 @@ -1,6 +1,6 @@ { "Method": "GET", - "URL": "/v3/sites/my-site/rule_engine/rules?limit=50\u0026offset=0", + "URL": "/v3/sites/my-site/rule_engine/rules", "Header": { "Accept-Encoding": [ "gzip" diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_1 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_1 index 7c42eab..71d46fc 100644 --- a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_1 +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_1 @@ -1,6 +1,6 @@ { "Method": "GET", - "URL": "/v3/sites/my-site/rule_engine/rules?limit=50\u0026offset=1", + "URL": "/v3/sites/my-site/rule_engine/rules?limit=50\u0026offset=0", "Header": { "Accept-Encoding": [ "gzip" diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_2 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_2 new file mode 100644 index 0000000..7c42eab --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/req_2 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site/rule_engine/rules?limit=50\u0026offset=1", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_1 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_1 index 8a54bed..cd4be09 100644 --- a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_1 +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_1 @@ -1 +1 @@ -{"rules":[],"total_count":1} +{"rules":[{"id":"rule-1"}],"total_count":1} diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_2 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_2 new file mode 100644 index 0000000..8a54bed --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list/resp_2 @@ -0,0 +1 @@ +{"rules":[],"total_count":1} diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/cmd.tmpl b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/cmd.tmpl new file mode 100644 index 0000000..c601def --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/cmd.tmpl @@ -0,0 +1,2 @@ +enapter3 connection add --name my-conn --url {{.URL}} --token {{.Token}} --site-id my-site +enapter3 rule-engine rule list --connection my-conn diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/out b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/out new file mode 100644 index 0000000..a20ee71 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/out @@ -0,0 +1 @@ +{"rules":[{"id":"rule-1"}],"total_count":1} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/req_0 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/req_0 new file mode 100644 index 0000000..56d58fe --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/req_0 @@ -0,0 +1,19 @@ +{ + "Method": "GET", + "URL": "/v3/sites/my-site/rule_engine/rules", + "Header": { + "Accept-Encoding": [ + "gzip" + ], + "Content-Type": [ + "" + ], + "User-Agent": [ + "enapter-cli/" + ], + "X-Enapter-Auth-Token": [ + "enapter_api_test_token" + ] + }, + "Body": "" +} \ No newline at end of file diff --git a/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/resp_0 b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/resp_0 new file mode 100644 index 0000000..e3baf32 --- /dev/null +++ b/internal/app/enaptercli/testdata/http_req_resp/rule_engine_rule_list_legacy_unpaginated/resp_0 @@ -0,0 +1 @@ +{"rules":[{"id":"rule-1"}]} From 16677cd9cf79bf2657a3a1f864fdfdf30ab1ab6e Mon Sep 17 00:00:00 2001 From: Roman Novatorov Date: Fri, 15 May 2026 11:27:20 +0200 Subject: [PATCH 2/2] refactor: use parseRespErrorMessage for detailed error reporting in pagination --- internal/app/enaptercli/cmd_base_pagination.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/enaptercli/cmd_base_pagination.go b/internal/app/enaptercli/cmd_base_pagination.go index b7ad585..91a206b 100644 --- a/internal/app/enaptercli/cmd_base_pagination.go +++ b/internal/app/enaptercli/cmd_base_pagination.go @@ -82,7 +82,7 @@ type paginateRespProcesor struct { func (p *paginateRespProcesor) Process(resp *http.Response) error { if resp.StatusCode != http.StatusOK { - return cli.Exit("Unexpected response status: "+resp.Status, 1) + return cli.Exit(parseRespErrorMessage(resp), 1) } var pageBody map[string]json.RawMessage