From 169fadb015798cb5f379858fcb493562abd3a0d7 Mon Sep 17 00:00:00 2001 From: Rahul De Date: Mon, 16 Dec 2024 21:07:13 +0000 Subject: [PATCH] [path params] interpolate path params in handler data --- README.md | 11 +++++------ lib.go | 40 ++++++++++++++++++++++++++++++++++++++-- lib_test.go | 24 ++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 22e2fac..32e174a 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,11 @@ What if you can influence the CLI behaviour from the server? This enables you to ### Status -Experimental, in dev flux and looking for design/usage feedback! +Alpha, stabilising API and looking for design/usage feedback! -### TODO: -- Interpolate the HTTP paths when sending to the handler with the path params -- Support more of the OpenAPI types and their checks. eg arrays, enums, objects, multi types etc -- Type checking request bodies +### Ideally support: +- More of the OpenAPI types and their checks. eg arrays, enums, objects, multi types etc +- Type checking request bodies of certain MIME types ### Installation @@ -32,7 +31,7 @@ climate allows the server to influence the CLI behaviour by using OpenAPI's [ext Overall, the way it works: - Each operation is converted to a Cobra command - Each parameter is converted to a flag with its corresponding type -- Request bodies are a flag as of now, subject to change. Name defaults to `climate-data` unless specified via `x-cli-name` +- As of now, request bodies are a flag and treated as a string regardless of MIME type. Name defaults to `climate-data` unless specified via `x-cli-name`. All subject to change - The provided handlers are attached to each command, grouped and attached to the rootCmd Influenced by some of the ideas behind [restish](https://rest.sh/) it uses the following extensions as of now: diff --git a/lib.go b/lib.go index 0567978..e7fdb1d 100644 --- a/lib.go +++ b/lib.go @@ -4,6 +4,7 @@ import ( "fmt" "log/slog" "os" + "regexp" "github.com/pb33f/libopenapi" v3 "github.com/pb33f/libopenapi/datamodel/high/v3" @@ -31,7 +32,7 @@ type ParamMeta struct { // Data passed into each handler type HandlerData struct { Method string // the HTTP method - Path string // the parameterised path. currently non interpolated + Path string // the path with the path params filled in PathParams []ParamMeta // List of path params QueryParams []ParamMeta // List of query params HeaderParams []ParamMeta // List of header params @@ -92,6 +93,8 @@ func addParams(cmd *cobra.Command, op *v3.Operation, handlerData *HandlerData) { t := String if schema != nil { t = OpenAPIType(schema.Type[0]) + } else { + slog.Warn("No type set for param, defaulting to string", "param", param.Name, "id", op.OperationId) } switch t { @@ -159,6 +162,36 @@ func addRequestBody(cmd *cobra.Command, op *v3.Operation, handlerData *HandlerDa return nil } +func interpolatePath(cmd *cobra.Command, h *HandlerData) error { + for _, param := range h.PathParams { + pattern, err := regexp.Compile(fmt.Sprintf("({%s})+", param.Name)) + if err != nil { + return err + } + + var value string + flags := cmd.Flags() + + switch param.Type { + case String: + value, _ = flags.GetString(param.Name) + case Integer: + v, _ := flags.GetInt(param.Name) + value = fmt.Sprintf("%d", v) + case Number: + v, _ := flags.GetFloat64(param.Name) + value = fmt.Sprintf("%g", v) + case Boolean: + v, _ := flags.GetBool(param.Name) + value = fmt.Sprintf("%t", v) + } + + h.Path = pattern.ReplaceAllString(h.Path, value) + } + + return nil +} + // Loads and verifies an OpenAPI spec frpm an array of bytes func LoadV3(data []byte) (*libopenapi.DocumentModel[v3.Document], error) { document, err := libopenapi.NewDocument(data) @@ -219,7 +252,10 @@ func BootstrapV3(rootCmd *cobra.Command, model libopenapi.DocumentModel[v3.Docum cmd.Short = op.Summary } cmd.Run = func(opts *cobra.Command, args []string) { - // TODO: Interpolate path + if err := interpolatePath(&cmd, &hData); err != nil { + slog.Error("Error interpolating path", "err", err) + } + handler(opts, args, hData) } diff --git a/lib_test.go b/lib_test.go index 4bac43c..3f7c59d 100644 --- a/lib_test.go +++ b/lib_test.go @@ -15,6 +15,30 @@ func TestLoadFileV3(t *testing.T) { } } +func TestInterpolatePath(t *testing.T) { + cmd := cobra.Command{} + hData := HandlerData{ + Method: "get", + Path: "/path/{foo}/to/{bar}/with/{baz}/and/{quxx}/together", + PathParams: []ParamMeta{ + {Name: "foo", Type: String}, + {Name: "bar", Type: Integer}, + {Name: "baz", Type: Number}, + {Name: "quxx", Type: Boolean}, + }, + } + + cmd.Flags().String("foo", "yes", "foo usage") + cmd.Flags().Int("bar", 420, "bar usage") + cmd.Flags().Float64("baz", 420.69, "baz usage") + cmd.Flags().Bool("quxx", false, "quxx usage") + + err := interpolatePath(&cmd, &hData) + assert.NoError(t, err) + + assert.Equal(t, hData.Path, "/path/yes/to/420/with/420.69/and/false/together") +} + func assertCmdTree(t *testing.T, cmd *cobra.Command, assertConf map[string]map[string]any, prefix string) { fmt.Println("Checking cmd level " + prefix)