Skip to content

Commit

Permalink
Merge pull request #31 from CrowdStrike/stream_api
Browse files Browse the repository at this point in the history
chore: refactor types out into separate named files
  • Loading branch information
jsteenb2 authored Aug 15, 2024
2 parents 3310327 + 23b27ee commit 9d21731
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 179 deletions.
102 changes: 102 additions & 0 deletions handler_fns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package fdk

import (
"context"
"encoding/json"
"net/http"
)

// HandlerFn wraps a function to return a handler. Similar to the http.HandlerFunc.
type HandlerFn func(ctx context.Context, r Request) Response

// Handle is the request/response lifecycle handler.
func (h HandlerFn) Handle(ctx context.Context, r Request) Response {
return h(ctx, r)
}

// HandleFnOf provides a means to translate the incoming requests to the destination body type.
// This normalizes the sad path and provides the caller with a zero fuss request to work with. Reducing
// json boilerplate for what is essentially the same operation on different types.
func HandleFnOf[T any](fn func(context.Context, RequestOf[T]) Response) Handler {
return HandlerFn(func(ctx context.Context, r Request) Response {
var v T
if err := json.Unmarshal(r.Body, &v); err != nil {
return Response{Errors: []APIError{{Code: http.StatusBadRequest, Message: "failed to unmarshal payload: " + err.Error()}}}
}

return fn(ctx, RequestOf[T]{
Body: v,
Context: r.Context,
Params: r.Params,
URL: r.URL,
Method: r.Method,
AccessToken: r.AccessToken,
TraceID: r.TraceID,
})
})
}

// HandlerFnOfOK provides a means to translate the incoming requests to the destination body type
// and execute validation on that type. This normalizes the sad path for both the unmarshalling of
// the request body and the validation of that request type using its OK() method.
func HandlerFnOfOK[T interface{ OK() []APIError }](fn func(context.Context, RequestOf[T]) Response) Handler {
return HandleFnOf(func(ctx context.Context, r RequestOf[T]) Response {
if errs := r.Body.OK(); len(errs) > 0 {
return ErrResp(errs...)
}
return fn(ctx, r)
})
}

// WorkflowCtx is the Request.Context field when integrating a function with Falcon Fusion workflow.
type WorkflowCtx struct {
ActivityExecID string `json:"activity_execution_id"`
AppID string `json:"app_id"`
CID string `json:"cid"`
OwnerCID string `json:"owner_cid"`
DefinitionID string `json:"definition_id,omitempty"`
DefinitionVersion int `json:"definition_version,omitempty"`
ExecutionID string `json:"execution_id,omitempty"`
Activity struct {
ID string `json:"id"`
Name string `json:"name"`
NodeID string `json:"node_id"`
} `json:"activity"`
Trigger struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
} `json:"trigger"`
}

// HandleWorkflow provides a means to create a handler with workflow integration. This function
// does not have an opinion on the request body but does expect a workflow integration. Typically,
// this is useful for DELETE/GET handlers.
func HandleWorkflow(fn func(context.Context, Request, WorkflowCtx) Response) Handler {
return HandlerFn(func(ctx context.Context, r Request) Response {
var w WorkflowCtx
if err := json.Unmarshal(r.Context, &w); err != nil {
return Response{Errors: []APIError{{Code: http.StatusBadRequest, Message: "failed to unmarshal workflow context: " + err.Error()}}}
}

return fn(ctx, r, w)
})
}

// HandleWorkflowOf provides a means to create a handler with Workflow integration. This
// function is useful when you expect a request body and have workflow integrations. Typically, this
// is with PATCH/POST/PUT handlers.
func HandleWorkflowOf[T any](fn func(context.Context, RequestOf[T], WorkflowCtx) Response) Handler {
return HandleWorkflow(func(ctx context.Context, r Request, workflowCtx WorkflowCtx) Response {
next := HandleFnOf(func(ctx context.Context, r RequestOf[T]) Response {
return fn(ctx, r, workflowCtx)
})
return next.Handle(ctx, r)
})
}

// ErrHandler creates a new handler to respond with only errors.
func ErrHandler(errs ...APIError) Handler {
return HandlerFn(func(ctx context.Context, r Request) Response {
return ErrResp(errs...)
})
}
31 changes: 31 additions & 0 deletions request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fdk

import (
"encoding/json"
"net/http"
"net/url"
)

type (
// Request defines a request structure that is given to the runner. The Body is set to
// json.RawMessage, to enable decoration/middleware.
Request RequestOf[json.RawMessage]

// RequestOf provides a generic body we can target our unmarshaling into.
RequestOf[T any] struct {
FnID string
FnVersion int

Body T

Context json.RawMessage
Params struct {
Header http.Header
Query url.Values
}
URL string
Method string
AccessToken string
TraceID string
}
)
66 changes: 66 additions & 0 deletions response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package fdk

import (
"encoding/json"
"fmt"
"net/http"
)

// Response is the domain type for the response.
type Response struct {
Body json.Marshaler
Code int
Errors []APIError
Header http.Header
}

// StatusCode returns the response status code. When a Response.Code is not
// set and errors exist, then the highest code on the errors is returned.
func (r Response) StatusCode() int {
code := r.Code
if code == 0 && len(r.Errors) > 0 {
for _, e := range r.Errors {
if e.Code > code {
code = e.Code
}
}
}
if code == 0 {
code = http.StatusOK
}
return code
}

// APIError defines a error that is shared back to the caller.
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}

// Error provides a human readable error message.
func (a APIError) Error() string {
return fmt.Sprintf("[%d] %s", a.Code, a.Message)
}

// JSON jsonifies the input to valid json upon request marshaling.
func JSON(v any) json.Marshaler {
return jsoned{v: v}
}

type jsoned struct {
v any
}

func (j jsoned) MarshalJSON() ([]byte, error) {
return json.Marshal(j.v)
}

// ErrResp creates a sad path errors only response.
//
// Note: the highest status code from the errors will be used for the response
// status if no status code is set on the response.
func ErrResp(errs ...APIError) Response {
resp := Response{Errors: errs}
resp.Code = resp.StatusCode()
return resp
}
Loading

0 comments on commit 9d21731

Please sign in to comment.