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
18 changes: 18 additions & 0 deletions extensions/cel/libs/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,24 @@ 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)
}
Comment on lines +322 to +325

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.

Put() now uses buildRequestData(), but buildRequestData’s internal error text still says "failed to encode HTTP POST data ...". If encoding fails on a PUT request, the returned error chain will misleadingly mention POST; consider making buildRequestData’s error message method-agnostic (or pass the HTTP method into it).

Copilot uses AI. Check for mistakes.
req, err := http.NewRequestWithContext(context.TODO(), "PUT", 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
29 changes: 29 additions & 0 deletions extensions/cel/libs/http/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,35 @@ 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) 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
124 changes: 124 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,127 @@ 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) })`)

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.

Existing tests for this library exercise the lowercase function names (e.g., http.get/http.post) which are version-gated, but this new test only compiles the PascalCase variant (http.Put). Add coverage for http.put as well (or switch to it) to ensure the version-gated overload is actually exercised.

Suggested change
ast, issues := env.Compile(`http.Put("http://localhost:8080", { "key": dyn("value"), "foo": dyn(2) })`)
ast, issues := env.Compile(`http.put("http://localhost:8080", { "key": dyn("value"), "foo": dyn(2) })`)

Copilot uses AI. Check for mistakes.
fmt.Println(issues.String())
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)
assert.NotNil(t, ast)
Comment on lines +800 to +804

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 test also only covers the PascalCase overload (http.Put). To keep consistency with the rest of the suite and verify the version-gated camelCase overload, add a corresponding compile/eval test for http.put.

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"], "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)
})
}
}
19 changes: 19 additions & 0 deletions extensions/cel/libs/http/lib.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) {
}
}

buildPutOverloads := func(suffix string) []cel.FunctionOpt {
return []cel.FunctionOpt{
cel.MemberOverload(
fmt.Sprintf("http_put_string_any_%s", suffix),
[]*cel.Type{ContextType, types.StringType, types.AnyType},
types.AnyType,
cel.FunctionBinding(impl.put_request_string),
),
cel.MemberOverload(
fmt.Sprintf("http_put_string_any_headers_%s", suffix),
[]*cel.Type{ContextType, types.StringType, types.AnyType, types.NewMapType(types.StringType, types.StringType)},
types.AnyType,
cel.FunctionBinding(impl.put_request_with_headers_string),
),
}
}

buildClientOverloads := func(suffix string) []cel.FunctionOpt {
return []cel.FunctionOpt{
cel.MemberOverload(
Expand All @@ -105,12 +122,14 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) {
libraryDecls := map[string][]cel.FunctionOpt{
"Get": buildGetOverloads("pascal"),
"Post": buildPostOverloads("pascal"),
"Put": buildPutOverloads("pascal"),
"Client": buildClientOverloads("pascal"),
}

if c.version.AtLeast(version.MajorMinor(1, 18)) {
libraryDecls["get"] = buildGetOverloads("camel")
libraryDecls["post"] = buildPostOverloads("camel")
libraryDecls["put"] = buildPutOverloads("camel")
libraryDecls["client"] = buildClientOverloads("camel")
}
// create env options corresponding to our function overloads
Expand Down
1 change: 1 addition & 0 deletions extensions/cel/libs/http/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ var ContextType = types.NewOpaqueType("http.Context")
type ContextInterface interface {
Get(url string, headers map[string]string) (any, error)
Post(url string, data any, headers map[string]string) (any, error)
Put(url string, data any, headers map[string]string) (any, error)
Client(caBundle string) (ContextInterface, error)
Comment on lines 9 to 13

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.

Adding Put to the exported ContextInterface is a Go breaking change for any downstream implementation passed into Lib(...). To avoid forcing all consumers to update, consider keeping ContextInterface as-is and adding a Put method on the Context wrapper (delegating via a type assertion), or introducing a separate extended interface (e.g., ContextInterfaceWithPut) and returning a clear runtime error when Put isn’t supported.

Copilot uses AI. Check for mistakes.
}

Expand Down
Loading