From a6c6588d42a6981b65f5144d033f040afc29a959 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 13 Dec 2025 20:38:48 +0000 Subject: [PATCH 1/2] chore(internal): codegen related update --- go.mod | 2 +- pkg/cmd/cmd.go | 14 +- pkg/cmd/util.go | 149 ++++++++++++++++++++ pkg/jsonflag/json_flag.go | 248 ++++++++++++++++++++++++++++++++++ pkg/jsonflag/mutation.go | 104 ++++++++++++++ pkg/jsonflag/mutation_test.go | 37 +++++ 6 files changed, 546 insertions(+), 8 deletions(-) create mode 100644 pkg/cmd/util.go create mode 100644 pkg/jsonflag/json_flag.go create mode 100644 pkg/jsonflag/mutation.go create mode 100644 pkg/jsonflag/mutation_test.go diff --git a/go.mod b/go.mod index d7f661d..1fb7a42 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/onkernel/hypeman-go v0.8.0 github.com/tidwall/gjson v1.18.0 github.com/tidwall/pretty v1.2.1 + github.com/tidwall/sjson v1.2.5 github.com/urfave/cli-docs/v3 v3.0.0-alpha6 github.com/urfave/cli/v3 v3.3.2 golang.org/x/sys v0.38.0 @@ -60,7 +61,6 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/sjson v1.2.5 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 603d0d4..fbf546d 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -74,13 +74,13 @@ func init() { &runCmd, &psCmd, &logsCmd, - &rmCmd, - &stopCmd, - &startCmd, - &standbyCmd, - &restoreCmd, - &ingressCmd, - { + &rmCmd, + &stopCmd, + &startCmd, + &standbyCmd, + &restoreCmd, + &ingressCmd, + { Name: "health", Category: "API RESOURCE", Commands: []*cli.Command{ diff --git a/pkg/cmd/util.go b/pkg/cmd/util.go new file mode 100644 index 0000000..5a17ff1 --- /dev/null +++ b/pkg/cmd/util.go @@ -0,0 +1,149 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +package cmd + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "os" + "reflect" + "strings" + + "github.com/onkernel/hypeman-go/option" + + "github.com/tidwall/sjson" + "github.com/urfave/cli/v3" +) + + +type fileReader struct { + Value io.Reader + Base64Encoded bool +} + +func (f *fileReader) Set(filename string) error { + reader, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to read file %q: %w", filename, err) + } + f.Value = reader + return nil +} + +func (f *fileReader) String() string { + if f.Value == nil { + return "" + } + buf := new(bytes.Buffer) + buf.ReadFrom(f.Value) + if f.Base64Encoded { + return base64.StdEncoding.EncodeToString(buf.Bytes()) + } + return buf.String() +} + +func (f *fileReader) Get() any { + return f.String() +} + +func unmarshalWithReaders(data []byte, v any) error { + var fields map[string]json.RawMessage + if err := json.Unmarshal(data, &fields); err != nil { + return err + } + + rv := reflect.ValueOf(v).Elem() + rt := rv.Type() + + for i := 0; i < rv.NumField(); i++ { + fv := rv.Field(i) + ft := rt.Field(i) + + jsonKey := ft.Tag.Get("json") + if jsonKey == "" { + jsonKey = ft.Name + } else if idx := strings.Index(jsonKey, ","); idx != -1 { + jsonKey = jsonKey[:idx] + } + + rawVal, ok := fields[jsonKey] + if !ok { + continue + } + + if ft.Type == reflect.TypeOf((*io.Reader)(nil)).Elem() { + var s string + if err := json.Unmarshal(rawVal, &s); err != nil { + return fmt.Errorf("field %s: %w", ft.Name, err) + } + fv.Set(reflect.ValueOf(strings.NewReader(s))) + } else { + ptr := fv.Addr().Interface() + if err := json.Unmarshal(rawVal, ptr); err != nil { + return fmt.Errorf("field %s: %w", ft.Name, err) + } + } + } + + return nil +} + +func unmarshalStdinWithFlags(cmd *cli.Command, flags map[string]string, target any) error { + var data []byte + if isInputPiped() { + var err error + if data, err = io.ReadAll(os.Stdin); err != nil { + return err + } + } + + // Merge CLI flags into the body + for flag, path := range flags { + if cmd.IsSet(flag) { + var err error + data, err = sjson.SetBytes(data, path, cmd.Value(flag)) + if err != nil { + return err + } + } + } + + if data != nil { + if err := unmarshalWithReaders(data, target); err != nil { + return fmt.Errorf("failed to unmarshal JSON: %w", err) + } + } + + return nil +} + +func debugMiddleware(debug bool) option.Middleware { + return func(r *http.Request, mn option.MiddlewareNext) (*http.Response, error) { + if debug { + logger := log.Default() + + if reqBytes, err := httputil.DumpRequest(r, true); err == nil { + logger.Printf("Request Content:\n%s\n", reqBytes) + } + + resp, err := mn(r) + if err != nil { + return resp, err + } + + if respBytes, err := httputil.DumpResponse(resp, true); err == nil { + logger.Printf("Response Content:\n%s\n", respBytes) + } + + return resp, err + } + + return mn(r) + } +} diff --git a/pkg/jsonflag/json_flag.go b/pkg/jsonflag/json_flag.go new file mode 100644 index 0000000..605f883 --- /dev/null +++ b/pkg/jsonflag/json_flag.go @@ -0,0 +1,248 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "time" + + "github.com/urfave/cli/v3" +) + +type JSONConfig struct { + Kind MutationKind + Path string + // For boolean flags that set a specific value when present + SetValue any +} + +type JSONValueCreator[T any] struct{} + +func (c JSONValueCreator[T]) Create(val T, dest *T, config JSONConfig) cli.Value { + *dest = val + return &jsonValue[T]{ + destination: dest, + config: config, + } +} + +func (c JSONValueCreator[T]) ToString(val T) string { + switch v := any(val).(type) { + case string: + if v == "" { + return v + } + return fmt.Sprintf("%q", v) + case bool: + return strconv.FormatBool(v) + case int: + return strconv.Itoa(v) + case float64: + return strconv.FormatFloat(v, 'g', -1, 64) + case time.Time: + return v.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", v) + } +} + +type jsonValue[T any] struct { + destination *T + config JSONConfig +} + +func (v *jsonValue[T]) Set(val string) error { + var parsed T + var err error + + // If SetValue is configured, use that value instead of parsing the input + if v.config.SetValue != nil { + // For boolean flags with SetValue, register the configured value + if _, isBool := any(parsed).(bool); isBool { + globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(true).(T) // Set the flag itself to true + return nil + } + // For any flags with SetValue, register the configured value + globalRegistry.Mutate(v.config.Kind, v.config.Path, v.config.SetValue) + *v.destination = any(v.config.SetValue).(T) + return nil + } + + switch any(parsed).(type) { + case string: + parsed = any(val).(T) + case bool: + boolVal, parseErr := strconv.ParseBool(val) + if parseErr != nil { + return fmt.Errorf("invalid boolean value %q: %w", val, parseErr) + } + parsed = any(boolVal).(T) + case int: + intVal, parseErr := strconv.Atoi(val) + if parseErr != nil { + return fmt.Errorf("invalid integer value %q: %w", val, parseErr) + } + parsed = any(intVal).(T) + case float64: + floatVal, parseErr := strconv.ParseFloat(val, 64) + if parseErr != nil { + return fmt.Errorf("invalid float value %q: %w", val, parseErr) + } + parsed = any(floatVal).(T) + case time.Time: + // Try common datetime formats + formats := []string{ + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + "2006-01-02", + "15:04:05", + "15:04", + } + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid datetime value %q: %w", val, parseErr) + } + parsed = any(timeVal).(T) + case any: + // For `any`, store the string value directly + parsed = any(val).(T) + default: + return fmt.Errorf("unsupported type for JSON flag") + } + + *v.destination = parsed + globalRegistry.Mutate(v.config.Kind, v.config.Path, parsed) + return err +} + +func (v *jsonValue[T]) Get() any { + if v.destination != nil { + return *v.destination + } + var zero T + return zero +} + +func (v *jsonValue[T]) String() string { + if v.destination != nil { + switch val := any(*v.destination).(type) { + case string: + return val + case bool: + return strconv.FormatBool(val) + case int: + return strconv.Itoa(val) + case float64: + return strconv.FormatFloat(val, 'g', -1, 64) + case time.Time: + return val.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", val) + } + } + var zero T + switch any(zero).(type) { + case string: + return "" + case bool: + return "false" + case int: + return "0" + case float64: + return "0" + case time.Time: + return "" + default: + return fmt.Sprintf("%v", zero) + } +} + +func (v *jsonValue[T]) IsBoolFlag() bool { + return v.config.SetValue != nil +} + +// JSONDateValueCreator is a specialized creator for date-only values +type JSONDateValueCreator struct{} + +func (c JSONDateValueCreator) Create(val time.Time, dest *time.Time, config JSONConfig) cli.Value { + *dest = val + return &jsonDateValue{ + destination: dest, + config: config, + } +} + +func (c JSONDateValueCreator) ToString(val time.Time) string { + return val.Format("2006-01-02") +} + +type jsonDateValue struct { + destination *time.Time + config JSONConfig +} + +func (v *jsonDateValue) Set(val string) error { + // Try date-only formats first, then fall back to datetime formats + formats := []string{ + "2006-01-02", + "01/02/2006", + "Jan 2, 2006", + "January 2, 2006", + "2-Jan-2006", + time.RFC3339, + "2006-01-02T15:04:05Z07:00", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", + } + + var timeVal time.Time + var parseErr error + for _, format := range formats { + timeVal, parseErr = time.Parse(format, val) + if parseErr == nil { + break + } + } + if parseErr != nil { + return fmt.Errorf("invalid date value %q: %w", val, parseErr) + } + + *v.destination = timeVal + globalRegistry.Mutate(v.config.Kind, v.config.Path, timeVal.Format("2006-01-02")) + return nil +} + +func (v *jsonDateValue) Get() any { + if v.destination != nil { + return *v.destination + } + return time.Time{} +} + +func (v *jsonDateValue) String() string { + if v.destination != nil { + return v.destination.Format("2006-01-02") + } + return "" +} + +func (v *jsonDateValue) IsBoolFlag() bool { + return false +} + +type JSONStringFlag = cli.FlagBase[string, JSONConfig, JSONValueCreator[string]] +type JSONBoolFlag = cli.FlagBase[bool, JSONConfig, JSONValueCreator[bool]] +type JSONIntFlag = cli.FlagBase[int, JSONConfig, JSONValueCreator[int]] +type JSONFloatFlag = cli.FlagBase[float64, JSONConfig, JSONValueCreator[float64]] +type JSONDatetimeFlag = cli.FlagBase[time.Time, JSONConfig, JSONValueCreator[time.Time]] +type JSONDateFlag = cli.FlagBase[time.Time, JSONConfig, JSONDateValueCreator] +type JSONAnyFlag = cli.FlagBase[any, JSONConfig, JSONValueCreator[any]] diff --git a/pkg/jsonflag/mutation.go b/pkg/jsonflag/mutation.go new file mode 100644 index 0000000..46c115b --- /dev/null +++ b/pkg/jsonflag/mutation.go @@ -0,0 +1,104 @@ +package jsonflag + +import ( + "fmt" + "strconv" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type MutationKind string + +const ( + Body MutationKind = "body" + Query MutationKind = "query" + Header MutationKind = "header" +) + +type Mutation struct { + Kind MutationKind + Path string + Value any +} + +type registry struct { + mutations []Mutation +} + +var globalRegistry = ®istry{} + +func (r *registry) Mutate(kind MutationKind, path string, value any) { + r.mutations = append(r.mutations, Mutation{ + Kind: kind, + Path: path, + Value: value, + }) +} + +func (r *registry) Apply(body, query, header []byte) ([]byte, []byte, []byte, error) { + var err error + + for _, mutation := range r.mutations { + switch mutation.Kind { + case Body: + body, err = jsonSet(body, mutation.Path, mutation.Value) + case Query: + query, err = jsonSet(query, mutation.Path, mutation.Value) + case Header: + header, err = jsonSet(header, mutation.Path, mutation.Value) + } + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to apply mutation %s.%s: %w", mutation.Kind, mutation.Path, err) + } + } + + return body, query, header, nil +} + +func (r *registry) Clear() { + r.mutations = nil +} + +func (r *registry) List() []Mutation { + result := make([]Mutation, len(r.mutations)) + copy(result, r.mutations) + return result +} + +// Mutate adds a mutation that will be applied to the specified kind of data +func Mutate(kind MutationKind, path string, value any) { + globalRegistry.Mutate(kind, path, value) +} + +// ApplyMutations applies all registered mutations to the provided JSON data +func ApplyMutations(body, query, header []byte) ([]byte, []byte, []byte, error) { + return globalRegistry.Apply(body, query, header) +} + +// ClearMutations removes all registered mutations from the global registry +func ClearMutations() { + globalRegistry.Clear() +} + +// ListMutations returns a copy of all currently registered mutations +func ListMutations() []Mutation { + return globalRegistry.List() +} + +func jsonSet(json []byte, path string, value any) ([]byte, error) { + keys := strings.Split(path, ".") + path = "" + for _, key := range keys { + if key == "#" { + key = strconv.Itoa(len(gjson.GetBytes(json, path).Array()) - 1) + } + + if len(path) > 0 { + path += "." + } + path += key + } + return sjson.SetBytes(json, path, value) +} diff --git a/pkg/jsonflag/mutation_test.go b/pkg/jsonflag/mutation_test.go new file mode 100644 index 0000000..e87e518 --- /dev/null +++ b/pkg/jsonflag/mutation_test.go @@ -0,0 +1,37 @@ +package jsonflag + +import ( + "testing" +) + +func TestApply(t *testing.T) { + ClearMutations() + + Mutate(Body, "name", "test") + Mutate(Query, "page", 1) + Mutate(Header, "authorization", "Bearer token") + + body, query, header, err := ApplyMutations( + []byte(`{}`), + []byte(`{}`), + []byte(`{}`), + ) + + if err != nil { + t.Fatalf("Failed to apply mutations: %v", err) + } + + expectedBody := `{"name":"test"}` + expectedQuery := `{"page":1}` + expectedHeader := `{"authorization":"Bearer token"}` + + if string(body) != expectedBody { + t.Errorf("Body mismatch. Expected: %s, Got: %s", expectedBody, string(body)) + } + if string(query) != expectedQuery { + t.Errorf("Query mismatch. Expected: %s, Got: %s", expectedQuery, string(query)) + } + if string(header) != expectedHeader { + t.Errorf("Header mismatch. Expected: %s, Got: %s", expectedHeader, string(header)) + } +} From cc35fa118f98b71f1fc17160a9fdb340d3231f07 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 19:28:36 +0000 Subject: [PATCH 2/2] release: 0.7.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 13 +++++++++++++ pkg/cmd/version.go | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ac03171..1b77f50 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.1" + ".": "0.7.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6975c5f..e1e0a6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.7.0 (2025-12-23) + +Full Changelog: [v0.6.1...v0.7.0](https://github.com/onkernel/hypeman-cli/compare/v0.6.1...v0.7.0) + +### Features + +* add cp command for file copy to/from instances ([#18](https://github.com/onkernel/hypeman-cli/issues/18)) ([f67ad7b](https://github.com/onkernel/hypeman-cli/commit/f67ad7bcb6fbbe0a9409574fababab862da87840)) + + +### Chores + +* **internal:** codegen related update ([a6c6588](https://github.com/onkernel/hypeman-cli/commit/a6c6588d42a6981b65f5144d033f040afc29a959)) + ## 0.6.1 (2025-12-11) Full Changelog: [v0.6.0...v0.6.1](https://github.com/onkernel/hypeman-cli/compare/v0.6.0...v0.6.1) diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 69c4e47..f37c8ca 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -2,4 +2,4 @@ package cmd -const Version = "0.6.1" // x-release-please-version +const Version = "0.7.0" // x-release-please-version