diff --git a/extensions/cel/libs/http/http.go b/extensions/cel/libs/http/http.go index 84f653a..eaeb7f1 100644 --- a/extensions/cel/libs/http/http.go +++ b/extensions/cel/libs/http/http.go @@ -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) + } + 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 { diff --git a/extensions/cel/libs/http/impl.go b/extensions/cel/libs/http/impl.go index 85800d4..a46bd4d 100644 --- a/extensions/cel/libs/http/impl.go +++ b/extensions/cel/libs/http/impl.go @@ -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) diff --git a/extensions/cel/libs/http/impl_test.go b/extensions/cel/libs/http/impl_test.go index 5ec67ab..405bab7 100644 --- a/extensions/cel/libs/http/impl_test.go +++ b/extensions/cel/libs/http/impl_test.go @@ -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) })`) + 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) + 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) + }) + } +} diff --git a/extensions/cel/libs/http/lib.go b/extensions/cel/libs/http/lib.go index e58314f..3ef0347 100644 --- a/extensions/cel/libs/http/lib.go +++ b/extensions/cel/libs/http/lib.go @@ -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( @@ -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 diff --git a/extensions/cel/libs/http/types.go b/extensions/cel/libs/http/types.go index c4b6ea9..10451e2 100644 --- a/extensions/cel/libs/http/types.go +++ b/extensions/cel/libs/http/types.go @@ -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) }