Files
hetty/pkg/sender/sender.go

284 lines
7.9 KiB
Go
Raw Normal View History

2022-02-22 14:10:39 +01:00
package sender
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"time"
connect "connectrpc.com/connect"
"github.com/oklog/ulid/v2"
"google.golang.org/protobuf/proto"
2022-02-22 14:10:39 +01:00
"github.com/dstotijn/hetty/pkg/filter"
httppb "github.com/dstotijn/hetty/pkg/http"
2022-02-22 14:10:39 +01:00
"github.com/dstotijn/hetty/pkg/reqlog"
"github.com/dstotijn/hetty/pkg/scope"
)
var defaultHTTPClient = &http.Client{
Transport: &HTTPTransport{},
Timeout: 30 * time.Second,
}
var (
ErrProjectIDMustBeSet = errors.New("sender: project ID must be set")
ErrRequestNotFound = errors.New("sender: request not found")
)
type Service struct {
activeProjectID string
reqsFilter *RequestsFilter
2022-02-22 14:10:39 +01:00
scope *scope.Scope
repo Repository
reqLogSvc *reqlog.Service
2022-02-22 14:10:39 +01:00
httpClient *http.Client
}
type Config struct {
Scope *scope.Scope
Repository Repository
ReqLogService *reqlog.Service
2022-02-22 14:10:39 +01:00
HTTPClient *http.Client
}
type SendError struct {
err error
}
func NewService(cfg Config) *Service {
svc := &Service{
2022-02-22 14:10:39 +01:00
repo: cfg.Repository,
reqLogSvc: cfg.ReqLogService,
httpClient: defaultHTTPClient,
scope: cfg.Scope,
}
if cfg.HTTPClient != nil {
svc.httpClient = cfg.HTTPClient
}
return svc
}
func (svc *Service) GetRequestByID(ctx context.Context, req *connect.Request[GetRequestByIDRequest]) (*connect.Response[GetRequestByIDResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
2022-02-22 14:10:39 +01:00
senderReq, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, req.Msg.RequestId)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
}
2022-02-22 14:10:39 +01:00
return &connect.Response[GetRequestByIDResponse]{
Msg: &GetRequestByIDResponse{Request: senderReq},
}, nil
2022-02-22 14:10:39 +01:00
}
func (svc *Service) ListRequests(ctx context.Context, req *connect.Request[ListRequestsRequest]) (*connect.Response[ListRequestsResponse], error) {
reqs, err := svc.repo.FindSenderRequests(ctx, svc.activeProjectID, svc.filterRequest)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find requests: %w", err))
2022-02-22 14:10:39 +01:00
}
return &connect.Response[ListRequestsResponse]{
Msg: &ListRequestsResponse{Requests: reqs},
}, nil
2022-02-22 14:10:39 +01:00
}
func (svc *Service) filterRequest(req *Request) (bool, error) {
if svc.reqsFilter.OnlyInScope {
if svc.scope != nil && !req.MatchScope(svc.scope) {
return false, nil
}
}
if svc.reqsFilter.SearchExpr == "" {
return true, nil
}
expr, err := filter.ParseQuery(svc.reqsFilter.SearchExpr)
if err != nil {
return false, fmt.Errorf("failed to parse search expression: %w", err)
}
match, err := req.Matches(expr)
if err != nil {
return false, fmt.Errorf("failed to match search expression for sender request (id: %v): %w",
req.Id, err,
)
}
return match, nil
2022-02-22 14:10:39 +01:00
}
func (svc *Service) CreateOrUpdateRequest(ctx context.Context, req *connect.Request[CreateOrUpdateRequestRequest]) (*connect.Response[CreateOrUpdateRequestResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
2022-02-22 14:10:39 +01:00
}
r := proto.Clone(req.Msg.Request).(*Request)
if r == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request is nil"))
2022-02-22 14:10:39 +01:00
}
if r.HttpRequest == nil {
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("sender: request.http_request is nil"))
}
2022-02-22 14:10:39 +01:00
if r.Id == "" {
r.Id = ulid.Make().String()
2022-02-22 14:10:39 +01:00
}
r.ProjectId = svc.activeProjectID
if r.HttpRequest.Method == httppb.Method_METHOD_UNSPECIFIED {
r.HttpRequest.Method = httppb.Method_METHOD_GET
2022-02-22 14:10:39 +01:00
}
if r.HttpRequest.Protocol == httppb.Protocol_PROTOCOL_UNSPECIFIED {
r.HttpRequest.Protocol = httppb.Protocol_PROTOCOL_HTTP20
2022-02-22 14:10:39 +01:00
}
err := svc.repo.StoreSenderRequest(ctx, r)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
2022-02-22 14:10:39 +01:00
}
return &connect.Response[CreateOrUpdateRequestResponse]{
Msg: &CreateOrUpdateRequestResponse{
Request: r,
},
}, nil
2022-02-22 14:10:39 +01:00
}
func (svc *Service) CloneFromRequestLog(ctx context.Context, req *connect.Request[CloneFromRequestLogRequest]) (*connect.Response[CloneFromRequestLogResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
2022-02-22 14:10:39 +01:00
}
reqLog, err := svc.reqLogSvc.FindRequestLogByID(ctx, req.Msg.RequestLogId)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to find request log: %w", err))
2022-02-22 14:10:39 +01:00
}
clonedReqLog := proto.Clone(reqLog).(*reqlog.HttpRequestLog)
senderReq := &Request{
Id: ulid.Make().String(),
ProjectId: svc.activeProjectID,
SourceRequestLogId: clonedReqLog.Id,
HttpRequest: clonedReqLog.Request,
2022-02-22 14:10:39 +01:00
}
err = svc.repo.StoreSenderRequest(ctx, senderReq)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store request: %w", err))
2022-02-22 14:10:39 +01:00
}
return &connect.Response[CloneFromRequestLogResponse]{Msg: &CloneFromRequestLogResponse{
Request: senderReq,
}}, nil
2022-02-22 14:10:39 +01:00
}
func (svc *Service) SetRequestsFilter(filter *RequestsFilter) {
svc.reqsFilter = filter
2022-02-22 14:10:39 +01:00
}
func (svc *Service) RequestsFilter() *RequestsFilter {
return svc.reqsFilter
2022-02-22 14:10:39 +01:00
}
func (svc *Service) SendRequest(ctx context.Context, connReq *connect.Request[SendRequestRequest]) (*connect.Response[SendRequestResponse], error) {
req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, connReq.Msg.RequestId)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("sender: failed to find request: %w", err))
2022-02-22 14:10:39 +01:00
}
httpReq, err := parseHTTPRequest(ctx, req)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to parse HTTP request: %w", err))
2022-02-22 14:10:39 +01:00
}
httpRes, err := svc.sendHTTPRequest(httpReq)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: could not send HTTP request: %w", err))
2022-02-22 14:10:39 +01:00
}
req.HttpResponse = httpRes
2025-01-13 23:15:18 +01:00
err = svc.repo.StoreSenderRequest(ctx, req)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to store sender response log: %w", err))
2022-02-22 14:10:39 +01:00
}
return &connect.Response[SendRequestResponse]{
Msg: &SendRequestResponse{
Request: req,
},
}, nil
2022-02-22 14:10:39 +01:00
}
func parseHTTPRequest(ctx context.Context, req *Request) (*http.Request, error) {
ctx = context.WithValue(ctx, protoCtxKey{}, req.GetHttpRequest().GetProtocol())
2022-02-22 14:10:39 +01:00
httpReq, err := http.NewRequestWithContext(ctx,
req.GetHttpRequest().GetMethod().String(),
req.GetHttpRequest().GetUrl(),
bytes.NewReader(req.GetHttpRequest().GetBody()),
)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, fmt.Errorf("failed to construct HTTP request: %w", err)
}
for _, header := range req.GetHttpRequest().GetHeaders() {
httpReq.Header.Add(header.Key, header.Value)
2022-02-22 14:10:39 +01:00
}
return httpReq, nil
}
func (svc *Service) sendHTTPRequest(httpReq *http.Request) (*httppb.Response, error) {
2022-02-22 14:10:39 +01:00
res, err := svc.httpClient.Do(httpReq)
if err != nil {
return nil, &SendError{err}
2022-02-22 14:10:39 +01:00
}
defer res.Body.Close()
resLog, err := httppb.ParseHTTPResponse(res)
2022-02-22 14:10:39 +01:00
if err != nil {
return nil, fmt.Errorf("failed to parse http response: %w", err)
2022-02-22 14:10:39 +01:00
}
return resLog, err
}
func (svc *Service) SetActiveProjectID(id string) {
2022-02-22 14:10:39 +01:00
svc.activeProjectID = id
}
func (svc *Service) DeleteRequests(ctx context.Context, req *connect.Request[DeleteRequestsRequest]) (*connect.Response[DeleteRequestsResponse], error) {
if svc.activeProjectID == "" {
return nil, connect.NewError(connect.CodeFailedPrecondition, ErrProjectIDMustBeSet)
}
err := svc.repo.DeleteSenderRequests(ctx, svc.activeProjectID)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("sender: failed to delete requests: %w", err))
}
return &connect.Response[DeleteRequestsResponse]{}, nil
2022-02-22 14:10:39 +01:00
}
func (e SendError) Error() string {
return fmt.Sprintf("failed to send HTTP request: %v", e.err)
}
func (e SendError) Unwrap() error {
return e.err
}