diff --git a/README.md b/README.md index 54d36461..fc281b4f 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,6 @@ MY_HEADER = "X-Special" def add_special_headers(ctx): """Shows how to modify an HTTP request before it is sent""" - assert that(ctx).is_of_type("ctx_before_request") req = ctx.request if type(req) != "http_request": diff --git a/fuzzymonkey.star b/fuzzymonkey.star index b33165a7..5fcac7b4 100644 --- a/fuzzymonkey.star +++ b/fuzzymonkey.star @@ -39,7 +39,6 @@ MY_HEADER = "X-Special" def add_special_headers(ctx): """Shows how to modify an HTTP request before it is sent""" - assert that(ctx).is_of_type("ctx_before_request") req = ctx.request if type(req) != "http_request": diff --git a/pkg/runtime/call.go b/pkg/runtime/call.go index fd372128..3e4f5db4 100644 --- a/pkg/runtime/call.go +++ b/pkg/runtime/call.go @@ -22,7 +22,7 @@ func (rt *Runtime) call(ctx context.Context, msg *fm.Srv_Call, tagsFilter *tags. log.Printf("[NFO] raw input: %.999v", msg.GetInput()) // 1. msg.GetInput() --to-> starlark value - cx := newCxModBeforeRequest(newCxBeforeRequest(msg.GetInput())) + cx := newCxModBeforeRequest(newCxRequestBeforeRequest(msg.GetInput())) // Runs check(before_request = ..) sequentially err := rt.forEachBeforeRequestCheck(func(name string, chk *check) error { diff --git a/pkg/runtime/ctx_modules.go b/pkg/runtime/ctx_modules.go index 4038c1a4..cc6245fb 100644 --- a/pkg/runtime/ctx_modules.go +++ b/pkg/runtime/ctx_modules.go @@ -1,155 +1,13 @@ package runtime import ( - "errors" - "fmt" - "sort" - "go.starlark.net/starlark" - "google.golang.org/protobuf/types/known/structpb" "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" - "github.com/FuzzyMonkeyCo/monkey/pkg/starlarkvalue" ) // TODO: rename `ctx` to `cx` -type ( - ctxctor2 func(*fm.Clt_CallResponseRaw_Output) ctxctor1 - ctxctor1 func(*starlark.Dict) *ctxModule -) - -func ctxCurry(callInput *fm.Clt_CallRequestRaw_Input) ctxctor2 { - request := inputAsValue(callInput) - request.Freeze() - return func(callOutput *fm.Clt_CallResponseRaw_Output) ctxctor1 { - response := outputAsValue(callOutput) - response.Freeze() - return func(state *starlark.Dict) *ctxModule { - // state is mutated through checks - return &ctxModule{ - request: request, - response: response, - state: state, - } - } - } -} - -// ctxModule is the `ctx` starlark value accessible during execution of checks -type ctxModule struct { - accessedState bool - request *ctxRequest - response *ctxResponse - state *starlark.Dict - //TODO: specs starlark.Value => provide models as JSON for now until we find a suitable Python-ish API - //TODO: CLI filter `--only="starlark.expr(ctx.specs)"` - //TODO: ctx.specs stops being accessible on first ctx.state access -} - -// TODO? easy access to generated parameters. For instance: -// post_id = ctx.request["parameters"]["path"]["{id}"] (note decoded int) - -var _ starlark.HasAttrs = (*ctxModule)(nil) - -func (m *ctxModule) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } -func (m *ctxModule) String() string { return "ctx" } -func (m *ctxModule) Truth() starlark.Bool { return true } -func (m *ctxModule) Type() string { return "ctx" } -func (m *ctxModule) AttrNames() []string { return []string{"request", "response", "state"} } - -func (m *ctxModule) Freeze() { - m.request.Freeze() - m.response.Freeze() - m.state.Freeze() -} - -func (m *ctxModule) Attr(name string) (starlark.Value, error) { - switch name { - case "request": - if m.accessedState { - return nil, errors.New("cannot access ctx.request after accessing ctx.state") - } - return m.request, nil - case "response": - if m.accessedState { - return nil, errors.New("cannot access ctx.response after accessing ctx.state") - } - return m.response, nil - case "state": - m.accessedState = true - return m.state, nil - default: - return nil, nil // no such method - } -} - -// ctxRequest represents request data as a Starlark value for user assertions. -type ctxRequest struct { - ty string - - attrs starlark.StringDict - attrnames []string - - protoBodyDecoded *structpb.Value - body starlark.Value - - protoHeaders []*fm.HeaderPair - headers starlark.Value -} - -var _ starlark.HasAttrs = (*ctxRequest)(nil) - -func (m *ctxRequest) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } -func (m *ctxRequest) String() string { return "ctx_request" } -func (m *ctxRequest) Truth() starlark.Bool { return true } -func (m *ctxRequest) Type() string { return m.ty } - -func (m *ctxRequest) Freeze() { - m.attrs.Freeze() - // NOTE: m.body.Freeze() in Attr() - // NOTE: m.headers.Freeze() in Attr() -} - -func (m *ctxRequest) AttrNames() []string { - if m.attrnames == nil { - names := append(m.attrs.Keys(), "headers") - if m.protoBodyDecoded != nil { - names = append(names, "body") - } - sort.Strings(names) - m.attrnames = names - } - return m.attrnames -} - -func (m *ctxRequest) Attr(name string) (starlark.Value, error) { - switch { - case name == "body" && m.protoBodyDecoded != nil: - if m.body == nil { - m.body = starlarkvalue.FromProtoValue(m.protoBodyDecoded) - m.body.Freeze() - } - return m.body, nil - - case name == "headers": - if m.headers == nil { - var err error - if m.headers, err = headerPairs(m.protoHeaders); err != nil { - return nil, err - } - m.headers.Freeze() - } - return m.headers, nil - - default: - if v := m.attrs[name]; v != nil { - return v, nil - } - return nil, nil // no such method - } -} - func headerPairs(protoHeaders []*fm.HeaderPair) (starlark.Value, error) { d := starlark.NewDict(len(protoHeaders)) //fixme: dont make a dict out of repeated HeaderPair.s @@ -165,123 +23,3 @@ func headerPairs(protoHeaders []*fm.HeaderPair) (starlark.Value, error) { } return d, nil } - -func inputAsValue(i *fm.Clt_CallRequestRaw_Input) (cr *ctxRequest) { - cr = &ctxRequest{ - attrs: make(starlark.StringDict, 3), - } - switch x := i.GetInput().(type) { - - case *fm.Clt_CallRequestRaw_Input_HttpRequest_: - cr.ty = cxRequestHttp - - reqProto := i.GetHttpRequest() - cr.attrs["method"] = starlark.String(reqProto.Method) - cr.attrs["url"] = starlark.String(reqProto.Url) - cr.attrs["content"] = starlark.String(reqProto.Body) - cr.protoHeaders = reqProto.Headers //FIXME: cxHeaders + Freeze - if reqProto.Body != nil { - cr.protoBodyDecoded = reqProto.BodyDecoded - } - - default: - panic(fmt.Errorf("unhandled output %T: %+v", x, i)) - } - return -} - -// ctxResponse represents response data as a Starlark value for user assertions. -type ctxResponse struct { - ty string - - attrs starlark.StringDict - attrnames []string - - protoBodyDecoded *structpb.Value - body starlark.Value - - protoHeaders []*fm.HeaderPair - headers starlark.Value -} - -var _ starlark.HasAttrs = (*ctxResponse)(nil) - -func (m *ctxResponse) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } -func (m *ctxResponse) String() string { return "ctx_response" } -func (m *ctxResponse) Truth() starlark.Bool { return true } -func (m *ctxResponse) Type() string { return m.ty } - -func (m *ctxResponse) Freeze() { - m.attrs.Freeze() - // NOTE: m.body.Freeze() in Attr() - // NOTE: m.headers.Freeze() in Attr() -} - -func (m *ctxResponse) AttrNames() []string { - if m.attrnames == nil { - names := append(m.attrs.Keys(), "headers") - if m.protoBodyDecoded != nil { - names = append(names, "body") - } - sort.Strings(names) - m.attrnames = names - } - return m.attrnames -} - -func (m *ctxResponse) Attr(name string) (starlark.Value, error) { - switch { - case name == "body" && m.protoBodyDecoded != nil: - if m.body == nil { - m.body = starlarkvalue.FromProtoValue(m.protoBodyDecoded) - m.body.Freeze() - } - return m.body, nil - - case name == "headers": - if m.headers == nil { - var err error - if m.headers, err = headerPairs(m.protoHeaders); err != nil { - return nil, err - } - m.headers.Freeze() - } - return m.headers, nil - - default: - if v := m.attrs[name]; v != nil { - return v, nil - } - return nil, nil // no such method - } -} - -const ctxHttpResponse = "http_response" - -func outputAsValue(o *fm.Clt_CallResponseRaw_Output) (cr *ctxResponse) { - cr = &ctxResponse{ - attrs: make(starlark.StringDict, 5), - } - switch x := o.GetOutput().(type) { - - case *fm.Clt_CallResponseRaw_Output_HttpResponse_: - cr.ty = ctxHttpResponse - - repProto := o.GetHttpResponse() - cr.attrs["status_code"] = starlark.MakeUint(uint(repProto.StatusCode)) - cr.attrs["reason"] = starlark.String(repProto.Reason) - cr.attrs["content"] = starlark.String(repProto.Body) - cr.attrs["elapsed_ns"] = starlark.MakeInt64(repProto.ElapsedNs) - cr.attrs["elapsed_ms"] = starlark.MakeInt64(repProto.ElapsedNs / 1.e6) - // "error": repProto.Error Checks make this unreachable - // "history" :: []Rep (redirects)? - cr.protoHeaders = repProto.Headers - if repProto.Body != nil { - cr.protoBodyDecoded = repProto.BodyDecoded - } - - default: - panic(fmt.Errorf("unhandled output %T: %+v", x, o)) - } - return -} diff --git a/pkg/runtime/ctx_header.go b/pkg/runtime/cx_head.go similarity index 55% rename from pkg/runtime/ctx_header.go rename to pkg/runtime/cx_head.go index 70d80bbd..2019203f 100644 --- a/pkg/runtime/ctx_header.go +++ b/pkg/runtime/cx_head.go @@ -11,8 +11,9 @@ import ( "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" ) -// ctxHeader represents header data (e.g. HTTP headers) as a Starlark value for user assertions or mutation. -type ctxHeader struct { +// cxHead represents header data (e.g. HTTP headers) as a Starlark value for user assertions or mutation. +// Should be accessible through checks under `ctx.request.headers` and/or `ctx.respones.headers`. +type cxHead struct { header textproto.MIMEHeader keys []string @@ -20,48 +21,63 @@ type ctxHeader struct { itercount uint32 // number of active iterators (ignored if frozen) } -var _ starlark.HasAttrs = (*ctxHeader)(nil) +func newcxHead(protoHeader []*fm.HeaderPair) *cxHead { + ch := &cxHead{ + header: make(textproto.MIMEHeader, len(protoHeader)), + keys: make([]string, 0, len(protoHeader)), + } + for _, pair := range protoHeader { + key := textproto.CanonicalMIMEHeaderKey(pair.GetKey()) + ch.keys = append(ch.keys, key) + for _, value := range pair.GetValues() { + ch.header.Add(key, value) + } + } + return ch +} + +var _ starlark.HasAttrs = (*cxHead)(nil) -func ctxHeaderAdd(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { +func cxHeadAdd(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var k, v starlark.String if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &k, &v); err != nil { return nil, err } - ch := b.Receiver().(*ctxHeader) + ch := b.Receiver().(*cxHead) key := textproto.CanonicalMIMEHeaderKey(k.GoString()) ch.header.Add(key, v.GoString()) ch.keys = append(ch.keys, key) return starlark.None, nil } -func ctxHeaderDel(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { +func cxHeadDel(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var k starlark.String if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &k); err != nil { return nil, err } - ch := b.Receiver().(*ctxHeader) + ch := b.Receiver().(*cxHead) key := textproto.CanonicalMIMEHeaderKey(k.GoString()) ch.header.Del(key) ch.keys = slices.DeleteFunc(ch.keys, func(k string) bool { return k == key }) return starlark.None, nil } -func ctxHeaderGet(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { +func cxHeadGet(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var k starlark.String if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &k); err != nil { return nil, err } - ch := b.Receiver().(*ctxHeader) + ch := b.Receiver().(*cxHead) key := textproto.CanonicalMIMEHeaderKey(k.GoString()) return starlark.String(ch.header.Get(key)), nil } -func ctxHeaderSet(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { +func cxHeadSet(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var k, v starlark.String if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 2, &k, &v); err != nil { return nil, err } - ch := b.Receiver().(*ctxHeader) + ch := b.Receiver().(*cxHead) key := textproto.CanonicalMIMEHeaderKey(k.GoString()) ch.header.Set(key, v.GoString()) if !slices.Contains(ch.keys, key) { @@ -70,88 +86,60 @@ func ctxHeaderSet(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, return starlark.None, nil } -func ctxHeaderValues(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { +func cxHeadValues(_ *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { var k starlark.String if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 1, &k); err != nil { return nil, err } - ch := b.Receiver().(*ctxHeader) + ch := b.Receiver().(*cxHead) key := textproto.CanonicalMIMEHeaderKey(k.GoString()) return starlark.NewList(fromStrings(ch.header.Values(key))), nil } -var ctxHeaderMethods = map[string]*starlark.Builtin{ - "add": starlark.NewBuiltin("add", ctxHeaderAdd), - "del": starlark.NewBuiltin("del", ctxHeaderDel), - "get": starlark.NewBuiltin("get", ctxHeaderGet), - "set": starlark.NewBuiltin("set", ctxHeaderSet), - "values": starlark.NewBuiltin("values", ctxHeaderValues), +var cxHeadMethods = map[string]*starlark.Builtin{ + "add": starlark.NewBuiltin("add", cxHeadAdd), + "del": starlark.NewBuiltin("del", cxHeadDel), + "get": starlark.NewBuiltin("get", cxHeadGet), + "set": starlark.NewBuiltin("set", cxHeadSet), + "values": starlark.NewBuiltin("values", cxHeadValues), } -func (ch *ctxHeader) Attr(name string) (starlark.Value, error) { +func (ch *cxHead) Attr(name string) (starlark.Value, error) { switch name { case "add": if err := ch.checkMutable(name); err != nil { return nil, err } - return ctxHeaderMethods[name].BindReceiver(ch), nil + return cxHeadMethods[name].BindReceiver(ch), nil case "del": if err := ch.checkMutable(name); err != nil { return nil, err } - return ctxHeaderMethods[name].BindReceiver(ch), nil + return cxHeadMethods[name].BindReceiver(ch), nil case "get": - return ctxHeaderMethods[name].BindReceiver(ch), nil + return cxHeadMethods[name].BindReceiver(ch), nil case "set": if err := ch.checkMutable(name); err != nil { return nil, err } - return ctxHeaderMethods[name].BindReceiver(ch), nil + return cxHeadMethods[name].BindReceiver(ch), nil case "values": - return ctxHeaderMethods[name].BindReceiver(ch), nil + return cxHeadMethods[name].BindReceiver(ch), nil default: return nil, nil // no such method } } -func (ch *ctxHeader) AttrNames() []string { - names := make([]string, 0, len(ctxHeaderMethods)) - for name := range ctxHeaderMethods { +func (ch *cxHead) AttrNames() []string { + names := make([]string, 0, len(cxHeadMethods)) + for name := range cxHeadMethods { names = append(names, name) } sort.Strings(names) return names } -func newCtxHeader(protoHeader []*fm.HeaderPair) *ctxHeader { - ch := &ctxHeader{ - header: make(textproto.MIMEHeader, len(protoHeader)), - keys: make([]string, 0, len(protoHeader)), - } - for _, pair := range protoHeader { - key := textproto.CanonicalMIMEHeaderKey(pair.GetKey()) - ch.keys = append(ch.keys, key) - for _, value := range pair.GetValues() { - ch.header.Add(key, value) - } - } - // h := starlark.NewDict(len(protoHeader)) - // for _, kvs := range protoHeader { - // key := starlark.String(kvs.GetKey()) - // values := kvs.GetValues() - // vs := make([]starlark.Value, 0, len(values)) - // for _, value := range values { - // vs = append(vs, starlark.String(value)) - // } - // if err := h.SetKey(key, starlark.NewList(vs)); err != nil { - // return nil, err - // } - // } - // return &ctxHeader{header: h}, nil - return ch -} - -func (ch *ctxHeader) IntoProto() []*fm.HeaderPair { +func (ch *cxHead) IntoProto() []*fm.HeaderPair { hs := make([]*fm.HeaderPair, 0, len(ch.keys)) for _, key := range ch.keys { h := &fm.HeaderPair{ @@ -163,19 +151,15 @@ func (ch *ctxHeader) IntoProto() []*fm.HeaderPair { return hs } -var _ starlark.IterableMapping = (*ctxHeader)(nil) - -func (ch *ctxHeader) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", ch.Type()) } -func (ch *ctxHeader) String() string { return ch.Type() } -func (ch *ctxHeader) Truth() starlark.Bool { return true } -func (ch *ctxHeader) Type() string { return "ctx_header" } +var _ starlark.IterableMapping = (*cxHead)(nil) -func (ch *ctxHeader) Freeze() { - // ch.header.Freeze() - ch.frozen = true -} +func (ch *cxHead) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", ch.Type()) } +func (ch *cxHead) String() string { return ch.Type() } +func (ch *cxHead) Truth() starlark.Bool { return true } +func (ch *cxHead) Type() string { return "ctx_headers" } +func (ch *cxHead) Freeze() { ch.frozen = true } -func (ch *ctxHeader) Get(x starlark.Value) (v starlark.Value, found bool, err error) { +func (ch *cxHead) Get(x starlark.Value) (v starlark.Value, found bool, err error) { s, ok := x.(starlark.String) if !ok { return @@ -187,7 +171,7 @@ func (ch *ctxHeader) Get(x starlark.Value) (v starlark.Value, found bool, err er return starlark.NewList(fromStrings(vs)), true, nil } -func (ch *ctxHeader) Items() []starlark.Tuple { +func (ch *cxHead) Items() []starlark.Tuple { kvs := make([]starlark.Tuple, 0, len(ch.header)) for _, key := range ch.keys { values := ch.header.Values(key) @@ -207,14 +191,14 @@ func fromStrings(values []string) []starlark.Value { return vs } -func (ch *ctxHeader) Iterate() starlark.Iterator { +func (ch *cxHead) Iterate() starlark.Iterator { if !ch.frozen { ch.itercount++ } - return &ctxHeaderIterator{ch: ch} + return &cxHeadIterator{ch: ch} } -func (ch *ctxHeader) checkMutable(verb string) error { +func (ch *cxHead) checkMutable(verb string) error { if ch.frozen { return fmt.Errorf("cannot %s frozen hash table", verb) } @@ -224,12 +208,12 @@ func (ch *ctxHeader) checkMutable(verb string) error { return nil } -type ctxHeaderIterator struct { - ch *ctxHeader +type cxHeadIterator struct { + ch *cxHead i int } -func (it *ctxHeaderIterator) Next(p *starlark.Value) bool { +func (it *cxHeadIterator) Next(p *starlark.Value) bool { if it.i < len(it.ch.keys) { key := it.ch.keys[it.i] vs := fromStrings(it.ch.header.Values(key)) @@ -240,7 +224,7 @@ func (it *ctxHeaderIterator) Next(p *starlark.Value) bool { return false } -func (it *ctxHeaderIterator) Done() { +func (it *cxHeadIterator) Done() { if !it.ch.frozen { it.ch.itercount-- } diff --git a/pkg/runtime/cx_mod_after_response.go b/pkg/runtime/cx_mod_after_response.go new file mode 100644 index 00000000..be7b1d5d --- /dev/null +++ b/pkg/runtime/cx_mod_after_response.go @@ -0,0 +1,80 @@ +package runtime + +import ( + "errors" + "fmt" + + "go.starlark.net/starlark" + + "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" +) + +// TODO? easy access to generated parameters. For instance: +// post_id = ctx.request["parameters"]["path"]["{id}"] (note decoded int) + +// cxModAfterResponse is the `ctx` starlark value accessible after executing a call +type cxModAfterResponse struct { + accessedState bool + request *cxRequestAfterResponse + response *cxResponseAfterResponse + state *starlark.Dict + //TODO: specs starlark.Value => provide models as JSON for now until we find a suitable Python-ish API + //TODO: CLI filter `--only="starlark.expr(ctx.specs)"` + //TODO: ctx.specs stops being accessible on first ctx.state access +} + +type ( + ctxctor2 func(*fm.Clt_CallResponseRaw_Output) ctxctor1 + ctxctor1 func(*starlark.Dict) *cxModAfterResponse +) + +func ctxCurry(callInput *fm.Clt_CallRequestRaw_Input) ctxctor2 { + request := newCxRequestAfterResponse(callInput) + request.Freeze() + return func(callOutput *fm.Clt_CallResponseRaw_Output) ctxctor1 { + response := newCxResponseAfterResponse(callOutput) + response.Freeze() + return func(state *starlark.Dict) *cxModAfterResponse { + // state is mutated through checks + return &cxModAfterResponse{ + request: request, + response: response, + state: state, + } + } + } +} + +var _ starlark.HasAttrs = (*cxModAfterResponse)(nil) + +func (m *cxModAfterResponse) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } +func (m *cxModAfterResponse) String() string { return "ctx_after_response" } +func (m *cxModAfterResponse) Truth() starlark.Bool { return true } +func (m *cxModAfterResponse) Type() string { return "ctx" } +func (m *cxModAfterResponse) AttrNames() []string { return []string{"request", "response", "state"} } + +func (m *cxModAfterResponse) Freeze() { + m.request.Freeze() + m.response.Freeze() + m.state.Freeze() +} + +func (m *cxModAfterResponse) Attr(name string) (starlark.Value, error) { + switch name { + case "request": + if m.accessedState { + return nil, errors.New("cannot access ctx.request after accessing ctx.state") + } + return m.request, nil + case "response": + if m.accessedState { + return nil, errors.New("cannot access ctx.response after accessing ctx.state") + } + return m.response, nil + case "state": + m.accessedState = true + return m.state, nil + default: + return nil, nil // no such method + } +} diff --git a/pkg/runtime/cx_mod_before_request.go b/pkg/runtime/cx_mod_before_request.go index 32cd5cb4..57a9fa74 100644 --- a/pkg/runtime/cx_mod_before_request.go +++ b/pkg/runtime/cx_mod_before_request.go @@ -6,26 +6,30 @@ import ( "go.starlark.net/starlark" ) -func newCxModBeforeRequest(req *cxBeforeRequest) *cxModBeforeRequest { - return &cxModBeforeRequest{ - request: req, - } -} - // cxModBeforeRequest is the `ctx` starlark value accessible before executing a call type cxModBeforeRequest struct { - request *cxBeforeRequest + request *cxRequestBeforeRequest // No response: this lives only before the request is attempted // No state: disallowed for now //TODO: specs } +func newCxModBeforeRequest(req *cxRequestBeforeRequest) *cxModBeforeRequest { + return &cxModBeforeRequest{ + request: req, + } +} + var _ starlark.HasAttrs = (*cxModBeforeRequest)(nil) +// For `cx` values and subvalues everywhere: +// * String() MUST roughly match Go type name +// * Type() MUST be closer to Starlark land (shorter, more vague) + func (m *cxModBeforeRequest) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", m.Type()) } -func (m *cxModBeforeRequest) String() string { return "ctx" } +func (m *cxModBeforeRequest) String() string { return "ctx_before_request" } func (m *cxModBeforeRequest) Truth() starlark.Bool { return true } -func (m *cxModBeforeRequest) Type() string { return "ctx_before_request" } +func (m *cxModBeforeRequest) Type() string { return "ctx" } func (m *cxModBeforeRequest) AttrNames() []string { return []string{"request"} } func (m *cxModBeforeRequest) Freeze() { m.request.Freeze() } diff --git a/pkg/runtime/cx_request_after_response.go b/pkg/runtime/cx_request_after_response.go new file mode 100644 index 00000000..859994fd --- /dev/null +++ b/pkg/runtime/cx_request_after_response.go @@ -0,0 +1,108 @@ +package runtime + +import ( + "fmt" + "sort" + + "go.starlark.net/starlark" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" + "github.com/FuzzyMonkeyCo/monkey/pkg/starlarkvalue" +) + +// cxRequestAfterResponse is the `ctx.request` starlark value accessible after executing a call +type cxRequestAfterResponse struct { + ty string + + attrs starlark.StringDict + attrnames []string + + protoBodyDecoded *structpb.Value + body starlark.Value + + protoHeaders []*fm.HeaderPair + headers starlark.Value //FIXME: cxHeaders + Freeze +} + +func newCxRequestAfterResponse(i *fm.Clt_CallRequestRaw_Input) (cr *cxRequestAfterResponse) { + cr = &cxRequestAfterResponse{ + attrs: make(starlark.StringDict, 3), + } + switch x := i.GetInput().(type) { + + case *fm.Clt_CallRequestRaw_Input_HttpRequest_: + cr.ty = cxRequestHttp + + reqProto := i.GetHttpRequest() + cr.attrs["method"] = starlark.String(reqProto.Method) + cr.attrs["url"] = starlark.String(reqProto.Url) + cr.attrs["content"] = starlark.String(reqProto.Body) + cr.protoHeaders = reqProto.Headers + if reqProto.Body != nil { + cr.protoBodyDecoded = reqProto.BodyDecoded + } + + default: + panic(fmt.Errorf("unhandled output %T: %+v", x, i)) + } + return +} + +var _ starlark.HasAttrs = (*cxRequestAfterResponse)(nil) + +func (m *cxRequestAfterResponse) Hash() (uint32, error) { + return 0, fmt.Errorf("unhashable: %s", m.Type()) +} +func (m *cxRequestAfterResponse) String() string { return "request_after_response" } +func (m *cxRequestAfterResponse) Truth() starlark.Bool { return true } +func (m *cxRequestAfterResponse) Type() string { return m.ty } + +func (m *cxRequestAfterResponse) Freeze() { + m.attrs.Freeze() + if m.body != nil { + m.body.Freeze() + } + if m.headers != nil { + m.headers.Freeze() + } +} + +func (m *cxRequestAfterResponse) AttrNames() []string { + if m.attrnames == nil { + names := append(m.attrs.Keys(), "headers") + if m.protoBodyDecoded != nil { + names = append(names, "body") + } + sort.Strings(names) + m.attrnames = names + } + return m.attrnames +} + +func (m *cxRequestAfterResponse) Attr(name string) (starlark.Value, error) { + switch { + case name == "body" && m.protoBodyDecoded != nil: + if m.body == nil { + m.body = starlarkvalue.FromProtoValue(m.protoBodyDecoded) + m.body.Freeze() + } + return m.body, nil + + case name == "headers": + if m.headers == nil { + var err error + if m.headers, err = headerPairs(m.protoHeaders); err != nil { + return nil, err + } + m.headers.Freeze() + } + return m.headers, nil + + default: + if v := m.attrs[name]; v != nil { + return v, nil + } + return nil, nil // no such method + } +} diff --git a/pkg/runtime/cx_before_request.go b/pkg/runtime/cx_request_before_request.go similarity index 70% rename from pkg/runtime/cx_before_request.go rename to pkg/runtime/cx_request_before_request.go index 3153122a..2b3acf3d 100644 --- a/pkg/runtime/cx_before_request.go +++ b/pkg/runtime/cx_request_before_request.go @@ -17,25 +17,25 @@ const ( cxRequestHttp = "http_request" ) -// cxBeforeRequest is the `ctx.request` starlark value accessible before executing a call -type cxBeforeRequest struct { +// cxRequestBeforeRequest is the `ctx.request` starlark value accessible before executing a call +type cxRequestBeforeRequest struct { ty string method, url starlark.String - headers *ctxHeader + headers *cxHead body *structpb.Value //FIXME: starlark.Value + test that edits .body (as num and as dict/list, and as set) } -func newCxBeforeRequest(input *fm.Srv_Call_Input) *cxBeforeRequest { +func newCxRequestBeforeRequest(input *fm.Srv_Call_Input) *cxRequestBeforeRequest { switch x := input.GetInput().(type) { case *fm.Srv_Call_Input_HttpRequest_: r := input.GetHttpRequest() - return &cxBeforeRequest{ + return &cxRequestBeforeRequest{ ty: cxRequestHttp, method: starlark.String(r.GetMethod()), url: starlark.String(r.GetUrl()), - headers: newCtxHeader(r.GetHeaders()), + headers: newcxHead(r.GetHeaders()), //content: absent as encoding will only happen later body: r.GetBody(), } @@ -45,7 +45,7 @@ func newCxBeforeRequest(input *fm.Srv_Call_Input) *cxBeforeRequest { } } -func (cr *cxBeforeRequest) IntoProto(err error) *fm.Clt_CallRequestRaw { +func (cr *cxRequestBeforeRequest) IntoProto(err error) *fm.Clt_CallRequestRaw { var reason []string if err != nil { reason = strings.Split(err.Error(), "\n") @@ -87,19 +87,21 @@ func (cr *cxBeforeRequest) IntoProto(err error) *fm.Clt_CallRequestRaw { } } -var _ starlark.HasAttrs = (*cxBeforeRequest)(nil) +var _ starlark.HasAttrs = (*cxRequestBeforeRequest)(nil) -func (cr *cxBeforeRequest) Hash() (uint32, error) { return 0, fmt.Errorf("unhashable: %s", cr.Type()) } -func (cr *cxBeforeRequest) String() string { return "ctx_request" } -func (cr *cxBeforeRequest) Truth() starlark.Bool { return true } -func (cr *cxBeforeRequest) Type() string { return cr.ty } +func (cr *cxRequestBeforeRequest) Hash() (uint32, error) { + return 0, fmt.Errorf("unhashable: %s", cr.Type()) +} +func (cr *cxRequestBeforeRequest) String() string { return "request_before_request" } +func (cr *cxRequestBeforeRequest) Truth() starlark.Bool { return true } +func (cr *cxRequestBeforeRequest) Type() string { return cr.ty } -func (cr *cxBeforeRequest) Freeze() { +func (cr *cxRequestBeforeRequest) Freeze() { // cr.body.Freeze() FIXME cr.headers.Freeze() } -func (cr *cxBeforeRequest) AttrNames() []string { +func (cr *cxRequestBeforeRequest) AttrNames() []string { return []string{ // Keep 'em sorted "body", "headers", @@ -108,7 +110,7 @@ func (cr *cxBeforeRequest) AttrNames() []string { } } -func (cr *cxBeforeRequest) Attr(name string) (starlark.Value, error) { +func (cr *cxRequestBeforeRequest) Attr(name string) (starlark.Value, error) { switch name { case "body": var body starlark.Value = starlark.None diff --git a/pkg/runtime/cx_response_after_response.go b/pkg/runtime/cx_response_after_response.go new file mode 100644 index 00000000..82329c3d --- /dev/null +++ b/pkg/runtime/cx_response_after_response.go @@ -0,0 +1,116 @@ +package runtime + +import ( + "fmt" + "sort" + + "go.starlark.net/starlark" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/FuzzyMonkeyCo/monkey/pkg/internal/fm" + "github.com/FuzzyMonkeyCo/monkey/pkg/starlarkvalue" +) + +const ( + cxResponseHttp = "http_response" +) + +// cxResponseAfterResponse is the `ctx.response` starlark value accessible after executing a call +type cxResponseAfterResponse struct { + ty string + + attrs starlark.StringDict + attrnames []string + + protoBodyDecoded *structpb.Value + body starlark.Value + + protoHeaders []*fm.HeaderPair + headers starlark.Value //FIXME: cxHeaders + Freeze +} + +func newCxResponseAfterResponse(o *fm.Clt_CallResponseRaw_Output) (cr *cxResponseAfterResponse) { + cr = &cxResponseAfterResponse{ + attrs: make(starlark.StringDict, 5), + } + switch x := o.GetOutput().(type) { + + case *fm.Clt_CallResponseRaw_Output_HttpResponse_: + cr.ty = cxResponseHttp + + repProto := o.GetHttpResponse() + cr.attrs["status_code"] = starlark.MakeUint(uint(repProto.StatusCode)) + cr.attrs["reason"] = starlark.String(repProto.Reason) + cr.attrs["content"] = starlark.String(repProto.Body) + cr.attrs["elapsed_ns"] = starlark.MakeInt64(repProto.ElapsedNs) + cr.attrs["elapsed_ms"] = starlark.MakeInt64(repProto.ElapsedNs / 1.e6) + // "error": repProto.Error Checks make this unreachable + // "history" :: []Rep (redirects)? + cr.protoHeaders = repProto.Headers + if repProto.Body != nil { + cr.protoBodyDecoded = repProto.BodyDecoded + } + + default: + panic(fmt.Errorf("unhandled output %T: %+v", x, o)) + } + return +} + +var _ starlark.HasAttrs = (*cxResponseAfterResponse)(nil) + +func (m *cxResponseAfterResponse) Hash() (uint32, error) { + return 0, fmt.Errorf("unhashable: %s", m.Type()) +} +func (m *cxResponseAfterResponse) String() string { return "response_after_response" } +func (m *cxResponseAfterResponse) Truth() starlark.Bool { return true } +func (m *cxResponseAfterResponse) Type() string { return m.ty } + +func (m *cxResponseAfterResponse) Freeze() { + m.attrs.Freeze() + if m.body != nil { + m.body.Freeze() + } + if m.headers != nil { + m.headers.Freeze() + } +} + +func (m *cxResponseAfterResponse) AttrNames() []string { + if m.attrnames == nil { + names := append(m.attrs.Keys(), "headers") + if m.protoBodyDecoded != nil { + names = append(names, "body") + } + sort.Strings(names) + m.attrnames = names + } + return m.attrnames +} + +func (m *cxResponseAfterResponse) Attr(name string) (starlark.Value, error) { + switch { + case name == "body" && m.protoBodyDecoded != nil: + if m.body == nil { + m.body = starlarkvalue.FromProtoValue(m.protoBodyDecoded) + m.body.Freeze() + } + return m.body, nil + + case name == "headers": + if m.headers == nil { + var err error + if m.headers, err = headerPairs(m.protoHeaders); err != nil { + return nil, err + } + m.headers.Freeze() + } + return m.headers, nil + + default: + if v := m.attrs[name]; v != nil { + return v, nil + } + return nil, nil // no such method + } +}