-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: generic api request function #218
Changes from all commits
ae51430
590d286
1caa249
ab73842
0fd2c6b
16349f9
d2b53da
54a686e
f86822f
3407c9c
862ae41
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,9 @@ package resources | |
import ( | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
"github.com/spf13/viper" | ||
|
@@ -12,6 +14,9 @@ import ( | |
"ldcli/cmd/cliflags" | ||
"ldcli/cmd/validators" | ||
"ldcli/internal/analytics" | ||
"ldcli/internal/errors" | ||
"ldcli/internal/output" | ||
"ldcli/internal/resources" | ||
) | ||
|
||
func NewResourceCmd(parentCmd *cobra.Command, analyticsTracker analytics.Tracker, resourceName, shortDescription, longDescription string) *cobra.Command { | ||
|
@@ -35,13 +40,14 @@ func NewResourceCmd(parentCmd *cobra.Command, analyticsTracker analytics.Tracker | |
} | ||
|
||
type OperationData struct { | ||
Short string | ||
Long string | ||
Use string | ||
Params []Param | ||
HTTPMethod string | ||
RequiresBody bool | ||
Path string | ||
Short string | ||
Long string | ||
Use string | ||
Params []Param | ||
HTTPMethod string | ||
RequiresBody bool | ||
Path string | ||
SupportsSemanticPatch bool // TBD on how to actually determine from openapi spec | ||
} | ||
|
||
type Param struct { | ||
|
@@ -54,7 +60,7 @@ type Param struct { | |
|
||
type OperationCmd struct { | ||
OperationData | ||
client *http.Client | ||
client resources.Client | ||
cmd *cobra.Command | ||
} | ||
|
||
|
@@ -71,8 +77,17 @@ func (op *OperationCmd) initFlags() error { | |
} | ||
} | ||
|
||
if op.SupportsSemanticPatch { | ||
op.cmd.Flags().Bool("semantic-patch", false, "Perform a semantic patch request") | ||
err := viper.BindPFlag("semantic-patch", op.cmd.Flags().Lookup("semantic-patch")) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
for _, p := range op.Params { | ||
shorthand := fmt.Sprintf(p.Name[0:1]) // todo: how do we handle potential dupes | ||
// TODO: consider handling these all as strings | ||
switch p.Type { | ||
case "string": | ||
op.cmd.Flags().StringP(p.Name, shorthand, "", p.Description) | ||
|
@@ -97,41 +112,76 @@ func (op *OperationCmd) initFlags() error { | |
return nil | ||
} | ||
|
||
func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { | ||
paramVals := map[string]interface{}{} | ||
func buildURLWithParams(baseURI, path string, urlParams []string) string { | ||
s := make([]interface{}, len(urlParams)) | ||
for i, v := range urlParams { | ||
s[i] = v | ||
} | ||
|
||
re := regexp.MustCompile(`{\w+}`) | ||
format := re.ReplaceAllString(path, "%s") | ||
|
||
return baseURI + fmt.Sprintf(format, s...) | ||
} | ||
|
||
func (op *OperationCmd) makeRequest(cmd *cobra.Command, args []string) error { | ||
var data interface{} | ||
if op.RequiresBody { | ||
var data interface{} | ||
// TODO: why does viper.GetString(cliflags.DataFlag) not work? | ||
err := json.Unmarshal([]byte(cmd.Flags().Lookup(cliflags.DataFlag).Value.String()), &data) | ||
if err != nil { | ||
return err | ||
} | ||
paramVals[cliflags.DataFlag] = data | ||
} | ||
jsonData, err := json.Marshal(data) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
query := url.Values{} | ||
var urlParms []string | ||
for _, p := range op.Params { | ||
var val interface{} | ||
switch p.Type { | ||
case "string": | ||
val = viper.GetString(p.Name) | ||
case "boolean": | ||
val = viper.GetBool(p.Name) | ||
case "int": | ||
val = viper.GetInt(p.Name) | ||
Comment on lines
-114
to
-121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i think it's actually ok to just grab all of these as string since we're just passing them into the query param or url anyway |
||
val := viper.GetString(p.Name) | ||
if val != "" { | ||
switch p.In { | ||
case "path": | ||
urlParms = append(urlParms, val) | ||
case "query": | ||
query.Add(p.Name, val) | ||
} | ||
} | ||
} | ||
|
||
if val != nil { | ||
paramVals[p.Name] = val | ||
} | ||
path := buildURLWithParams(viper.GetString(cliflags.BaseURIFlag), op.Path, urlParms) | ||
|
||
contentType := "application/json" | ||
if viper.GetBool("semantic-patch") { | ||
contentType += "; domain-model=launchdarkly.semanticpatch" | ||
} | ||
|
||
res, err := op.client.MakeRequest( | ||
viper.GetString(cliflags.AccessTokenFlag), | ||
strings.ToUpper(op.HTTPMethod), | ||
path, | ||
contentType, | ||
query, | ||
jsonData, | ||
) | ||
if err != nil { | ||
return errors.NewError(output.CmdOutputError(viper.GetString(cliflags.OutputFlag), err)) | ||
} | ||
|
||
output, err := output.CmdOutput("get", viper.GetString(cliflags.OutputFlag), res) | ||
if err != nil { | ||
return errors.NewError(err.Error()) | ||
} | ||
|
||
fmt.Fprintf(cmd.OutOrStdout(), "would be making a %s request to %s here, with args: %s\n", op.HTTPMethod, op.Path, paramVals) | ||
fmt.Fprintf(cmd.OutOrStdout(), output+"\n") | ||
|
||
return nil | ||
} | ||
|
||
func NewOperationCmd(parentCmd *cobra.Command, client *http.Client, op OperationData) *cobra.Command { | ||
func NewOperationCmd(parentCmd *cobra.Command, client resources.Client, op OperationData) *cobra.Command { | ||
opCmd := OperationCmd{ | ||
OperationData: op, | ||
client: client, | ||
|
@@ -143,7 +193,6 @@ func NewOperationCmd(parentCmd *cobra.Command, client *http.Client, op Operation | |
RunE: opCmd.makeRequest, | ||
Short: op.Short, | ||
Use: op.Use, | ||
//TODO: add tracking here | ||
} | ||
|
||
opCmd.cmd = cmd | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package resources | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"ldcli/internal/errors" | ||
) | ||
|
||
type Client interface { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. made a new interface for testing but had an issue with passing in data to the mock client, will try to tackle in follow up PR |
||
MakeRequest(accessToken, method, path, contentType string, query url.Values, data []byte) ([]byte, error) | ||
} | ||
|
||
type ResourcesClient struct { | ||
cliVersion string | ||
} | ||
|
||
var _ Client = ResourcesClient{} | ||
|
||
func NewClient(cliVersion string) ResourcesClient { | ||
return ResourcesClient{cliVersion: cliVersion} | ||
} | ||
|
||
func (c ResourcesClient) MakeRequest(accessToken, method, path, contentType string, query url.Values, data []byte) ([]byte, error) { | ||
client := http.Client{Timeout: 3 * time.Second} | ||
|
||
req, _ := http.NewRequest(method, path, bytes.NewReader(data)) | ||
req.Header.Add("Authorization", accessToken) | ||
req.Header.Add("Content-type", contentType) | ||
req.Header.Set("User-Agent", fmt.Sprintf("launchdarkly-cli/v%s", c.cliVersion)) | ||
req.URL.RawQuery = query.Encode() | ||
|
||
res, err := client.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
body, err := io.ReadAll(res.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer res.Body.Close() | ||
|
||
if res.StatusCode >= 400 { | ||
return body, errors.NewAPIError(body, nil, nil) | ||
} | ||
|
||
return body, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changes here are just due to moving the files