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
36 changes: 36 additions & 0 deletions extensions/cel/libs/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,42 @@ func (r *contextImpl) Post(url string, data any, headers map[string]string) (any
return r.executeRequest(req)
}

func (r *contextImpl) Put(url string, data any, headers map[string]string) (any, error) {
if err := r.validateURL(url); err != nil {
return nil, err
}
body, err := buildRequestData(data)
if err != nil {
return nil, fmt.Errorf("failed to encode request data: %w", err)
}
req, err := http.NewRequestWithContext(context.TODO(), "PUT", url, body)
Comment on lines +322 to +326

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildRequestData returns an error message that hard-codes "HTTP POST data", but this code path is now used by PUT (and PATCH below). Consider making the error message method-agnostic (or passing the HTTP method into the helper) so failures during PUT/PATCH encoding don't misleadingly mention POST.

Copilot uses AI. Check for mistakes.
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
for h, v := range headers {
req.Header.Add(h, v)
}
return r.executeRequest(req)
}

func (r *contextImpl) Patch(url string, data any, headers map[string]string) (any, error) {
if err := r.validateURL(url); err != nil {
return nil, err
}
body, err := buildRequestData(data)
if err != nil {
return nil, fmt.Errorf("failed to encode request data: %w", err)
}
req, err := http.NewRequestWithContext(context.TODO(), "PATCH", url, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
for h, v := range headers {
req.Header.Add(h, v)
}
return r.executeRequest(req)
}

func (r *contextImpl) executeRequest(req *http.Request) (any, error) {
resp, err := r.client.Do(req)
if err != nil {
Expand Down
58 changes: 58 additions & 0 deletions extensions/cel/libs/http/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,64 @@ func (c *impl) post_request_with_headers_string(args ...ref.Val) ref.Val {
return c.post_request_string_with_client(args...)
}

func (c *impl) put_request_string_with_client(args ...ref.Val) ref.Val {
if len(args) != 4 {
return types.NewErr("expected 4 arguments, got %d", len(args))
}
if request, err := utils.GetArg[Context](args, 0); err != nil {
return err
} else if url, err := utils.GetArg[string](args, 1); err != nil {
return err
} else if data, err := utils.GetArg[*structpb.Value](args, 2); err != nil {
return err
} else if header, err := utils.GetArg[map[string]string](args, 3); err != nil {
return err
} else {
data, err := request.Put(url, data, header)
if err != nil {
return types.NewErr("request failed: %v", err)
}
return c.NativeToValue(data)
}
}

func (c *impl) put_request_string(args ...ref.Val) ref.Val {
return c.put_request_string_with_client(args[0], args[1], args[2], c.NativeToValue(make(map[string]string, 0)))
}

func (c *impl) put_request_with_headers_string(args ...ref.Val) ref.Val {
return c.put_request_string_with_client(args...)
}

func (c *impl) patch_request_string_with_client(args ...ref.Val) ref.Val {
if len(args) != 4 {
return types.NewErr("expected 4 arguments, got %d", len(args))
}
if request, err := utils.GetArg[Context](args, 0); err != nil {
return err
} else if url, err := utils.GetArg[string](args, 1); err != nil {
return err
} else if data, err := utils.GetArg[*structpb.Value](args, 2); err != nil {
return err
} else if header, err := utils.GetArg[map[string]string](args, 3); err != nil {
return err
} else {
data, err := request.Patch(url, data, header)
if err != nil {
return types.NewErr("request failed: %v", err)
}
return c.NativeToValue(data)
}
}

func (c *impl) patch_request_string(args ...ref.Val) ref.Val {
return c.patch_request_string_with_client(args[0], args[1], args[2], c.NativeToValue(make(map[string]string, 0)))
}

func (c *impl) patch_request_with_headers_string(args ...ref.Val) ref.Val {
return c.patch_request_string_with_client(args...)
}

func (c *impl) http_client_string(request, caBundle ref.Val) ref.Val {
if request, err := utils.ConvertToNative[Context](request); err != nil {
return types.NewErr("invalid arg %d: %v", 0, err)
Expand Down
248 changes: 248 additions & 0 deletions extensions/cel/libs/http/impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -728,3 +728,251 @@ func Test_impl_post_request_with_400_bad_request(t *testing.T) {
assert.Equal(t, body["code"], "INVALID_DATA")
assert.Equal(t, body["statusCode"], http.StatusBadRequest)
}

func Test_impl_put_request(t *testing.T) {
base, err := compiler.NewBaseEnv()
assert.NoError(t, err)
assert.NotNil(t, base)

ctx := Context{&contextImpl{
client: testClient{
doFunc: func(req *http.Request) (*http.Response, error) {
assert.Equal(t, req.URL.String(), "http://localhost:8080")
assert.Equal(t, req.Method, "PUT")

var data any
err := json.NewDecoder(req.Body).Decode(&data)
assert.NoError(t, err)
assert.Equal(t, data.(map[string]any)["key"], "value")
assert.Equal(t, data.(map[string]any)["foo"], float64(2))

return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"body": "updated"}`))}, nil
},
},
}}

env, err := base.Extend(
Lib(&ctx, version.MajorMinor(1, 18)),
)
assert.NoError(t, err)
assert.NotNil(t, env)
ast, issues := env.Compile(`http.Put("http://localhost:8080", { "key": dyn("value"), "foo": dyn(2) })`)
fmt.Println(issues.String())

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fmt.Println(issues.String()) writes to stdout during go test runs and makes CI output noisy. Consider removing it, or switching to t.Log/Logf only when issues != nil and issues.Err() != nil (or equivalent) to log failures only.

Suggested change
fmt.Println(issues.String())
if issues != nil {
t.Log(issues.String())
}

Copilot uses AI. Check for mistakes.
assert.Nil(t, issues)
assert.NotNil(t, ast)
prog, err := env.Program(ast)
assert.NoError(t, err)
assert.NotNil(t, prog)

out, _, err := prog.Eval(map[string]any{})
assert.NoError(t, err)
body := out.Value().(map[string]any)
assert.Equal(t, body["body"], "updated")
assert.Equal(t, body["statusCode"], http.StatusOK)
}

func Test_impl_put_request_with_headers(t *testing.T) {
base, err := compiler.NewBaseEnv()
assert.NoError(t, err)
assert.NotNil(t, base)

ctx := Context{&contextImpl{
client: testClient{
doFunc: func(req *http.Request) (*http.Response, error) {
assert.Equal(t, req.URL.String(), "http://localhost:8080")
assert.Equal(t, req.Method, "PUT")
assert.Equal(t, req.Header.Get("Authorization"), "Bearer token")

var data any
err := json.NewDecoder(req.Body).Decode(&data)
assert.NoError(t, err)
assert.Equal(t, data.(map[string]any)["key"], "value")

return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"body": "updated"}`))}, nil
},
},
}}

env, err := base.Extend(
Lib(&ctx, version.MajorMinor(1, 18)),
)
assert.NoError(t, err)
assert.NotNil(t, env)
ast, issues := env.Compile(`http.Put("http://localhost:8080", {"key": "value"}, {"Authorization": "Bearer token"})`)
fmt.Println(issues.String())
assert.Nil(t, issues)
Comment on lines +801 to +803

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fmt.Println(issues.String()) writes to stdout during go test runs and makes CI output noisy. Consider removing it, or switching to t.Log/Logf only when issues != nil and issues.Err() != nil (or equivalent) to log failures only.

Copilot uses AI. Check for mistakes.
assert.NotNil(t, ast)
prog, err := env.Program(ast)
assert.NoError(t, err)
assert.NotNil(t, prog)

out, _, err := prog.Eval(map[string]any{})
assert.NoError(t, err)
body := out.Value().(map[string]any)
assert.Equal(t, body["body"], "updated")
assert.Equal(t, body["statusCode"], http.StatusOK)
}

func Test_impl_put_request_string_with_client_error(t *testing.T) {
base, err := compiler.NewBaseEnv()
assert.NoError(t, err)
assert.NotNil(t, base)

env, err := base.Extend(
Lib(nil, version.MajorMinor(1, 18)),
)
assert.NoError(t, err)
assert.NotNil(t, env)
tests := []struct {
name string
args []ref.Val
want ref.Val
}{{
name: "not enough args",
args: nil,
want: types.NewErr("expected 4 arguments, got %d", 0),
}, {
name: "bad arg 1",
args: []ref.Val{types.String("foo"), types.String("http://localhost:8080"), types.String("payload"), env.CELTypeAdapter().NativeToValue(make(map[string]string, 0))},
want: types.NewErr("invalid arg 0: unsupported native conversion from string to 'http.Context'"),
}, {
name: "bad arg 2",
args: []ref.Val{env.CELTypeAdapter().NativeToValue(Context{}), types.Bool(false), types.String("payload"), env.CELTypeAdapter().NativeToValue(make(map[string]string, 0))},
want: types.NewErr("invalid arg 1: type conversion error from bool to 'string'"),
}, {
name: "bad arg 4",
args: []ref.Val{env.CELTypeAdapter().NativeToValue(Context{}), types.String("http://localhost:8080"), types.String("payload"), types.Bool(false)},
want: types.NewErr("invalid arg 3: type conversion error from bool to 'map[string]string'"),
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &impl{}
got := c.put_request_string_with_client(tt.args...)
assert.Equal(t, tt.want, got)
})
}
}

func Test_impl_patch_request(t *testing.T) {
base, err := compiler.NewBaseEnv()
assert.NoError(t, err)
assert.NotNil(t, base)

ctx := Context{&contextImpl{
client: testClient{
doFunc: func(req *http.Request) (*http.Response, error) {
assert.Equal(t, req.URL.String(), "http://localhost:8080")
assert.Equal(t, req.Method, "PATCH")

var data any
err := json.NewDecoder(req.Body).Decode(&data)
assert.NoError(t, err)
assert.Equal(t, data.(map[string]any)["key"], "value")

return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"body": "patched"}`))}, nil
},
},
}}

env, err := base.Extend(
Lib(&ctx, version.MajorMinor(1, 18)),
)
assert.NoError(t, err)
assert.NotNil(t, env)
ast, issues := env.Compile(`http.Patch("http://localhost:8080", {"key": "value"})`)
fmt.Println(issues.String())
assert.Nil(t, issues)
assert.NotNil(t, ast)
Comment on lines +882 to +885

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fmt.Println(issues.String()) writes to stdout during go test runs and makes CI output noisy. Consider removing it, or switching to t.Log/Logf only when issues != nil and issues.Err() != nil (or equivalent) to log failures only.

Copilot uses AI. Check for mistakes.
prog, err := env.Program(ast)
assert.NoError(t, err)
assert.NotNil(t, prog)

out, _, err := prog.Eval(map[string]any{})
assert.NoError(t, err)
body := out.Value().(map[string]any)
assert.Equal(t, body["body"], "patched")
assert.Equal(t, body["statusCode"], http.StatusOK)
}

func Test_impl_patch_request_with_headers(t *testing.T) {
base, err := compiler.NewBaseEnv()
assert.NoError(t, err)
assert.NotNil(t, base)

ctx := Context{&contextImpl{
client: testClient{
doFunc: func(req *http.Request) (*http.Response, error) {
assert.Equal(t, req.URL.String(), "http://localhost:8080")
assert.Equal(t, req.Method, "PATCH")
assert.Equal(t, req.Header.Get("Authorization"), "Bearer token")
assert.Equal(t, req.Header.Get("Content-Type"), "application/json")

var data any
err := json.NewDecoder(req.Body).Decode(&data)
assert.NoError(t, err)
assert.Equal(t, data.(map[string]any)["key"], "value")

return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"body": "patched"}`))}, nil
},
},
}}

env, err := base.Extend(
Lib(&ctx, version.MajorMinor(1, 18)),
)
assert.NoError(t, err)
assert.NotNil(t, env)
ast, issues := env.Compile(`http.Patch("http://localhost:8080", {"key": "value"}, {"Authorization": "Bearer token", "Content-Type": "application/json"})`)
fmt.Println(issues.String())
assert.Nil(t, issues)
assert.NotNil(t, ast)
Comment on lines +925 to +928

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fmt.Println(issues.String()) writes to stdout during go test runs and makes CI output noisy. Consider removing it, or switching to t.Log/Logf only when issues != nil and issues.Err() != nil (or equivalent) to log failures only.

Copilot uses AI. Check for mistakes.
prog, err := env.Program(ast)
assert.NoError(t, err)
assert.NotNil(t, prog)

out, _, err := prog.Eval(map[string]any{})
assert.NoError(t, err)
body := out.Value().(map[string]any)
assert.Equal(t, body["body"], "patched")
assert.Equal(t, body["statusCode"], http.StatusOK)
}

func Test_impl_patch_request_string_with_client_error(t *testing.T) {
base, err := compiler.NewBaseEnv()
assert.NoError(t, err)
assert.NotNil(t, base)

env, err := base.Extend(
Lib(nil, version.MajorMinor(1, 18)),
)
assert.NoError(t, err)
assert.NotNil(t, env)
tests := []struct {
name string
args []ref.Val
want ref.Val
}{{
name: "not enough args",
args: nil,
want: types.NewErr("expected 4 arguments, got %d", 0),
}, {
name: "bad arg 1",
args: []ref.Val{types.String("foo"), types.String("http://localhost:8080"), types.String("payload"), env.CELTypeAdapter().NativeToValue(make(map[string]string, 0))},
want: types.NewErr("invalid arg 0: unsupported native conversion from string to 'http.Context'"),
}, {
name: "bad arg 2",
args: []ref.Val{env.CELTypeAdapter().NativeToValue(Context{}), types.Bool(false), types.String("payload"), env.CELTypeAdapter().NativeToValue(make(map[string]string, 0))},
want: types.NewErr("invalid arg 1: type conversion error from bool to 'string'"),
}, {
name: "bad arg 4",
args: []ref.Val{env.CELTypeAdapter().NativeToValue(Context{}), types.String("http://localhost:8080"), types.String("payload"), types.Bool(false)},
want: types.NewErr("invalid arg 3: type conversion error from bool to 'map[string]string'"),
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &impl{}
got := c.patch_request_string_with_client(tt.args...)
assert.Equal(t, tt.want, got)
})
}
}
Loading
Loading