From d5e1ad9087aecd6b67369b9ebbeb633ad808c129 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 06:14:20 +0000 Subject: [PATCH 1/4] feat(client): add escape hatch for null slice & maps --- internal/encoding/json/encode.go | 13 +-- internal/encoding/json/sentinel/null.go | 61 ++++++------- .../encoding/json/sentinel/sentinel_test.go | 85 ++++++++++--------- internal/paramutil/sentinel.go | 31 ------- packages/param/null.go | 19 +++++ packages/param/null_test.go | 49 +++++++++++ packages/param/param.go | 16 +++- 7 files changed, 153 insertions(+), 121 deletions(-) delete mode 100644 internal/paramutil/sentinel.go create mode 100644 packages/param/null.go create mode 100644 packages/param/null_test.go diff --git a/internal/encoding/json/encode.go b/internal/encoding/json/encode.go index e80e536..053c709 100644 --- a/internal/encoding/json/encode.go +++ b/internal/encoding/json/encode.go @@ -776,7 +776,7 @@ type mapEncoder struct { } func (me mapEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) { - if v.IsNil() { + if v.IsNil() /* EDIT(begin) */ || sentinel.IsValueNull(v) /* EDIT(end) */ { e.WriteString("null") return } @@ -855,7 +855,7 @@ type sliceEncoder struct { } func (se sliceEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) { - if v.IsNil() { + if v.IsNil() /* EDIT(begin) */ || sentinel.IsValueNull(v) /* EDIT(end) */ { e.WriteString("null") return } @@ -916,14 +916,7 @@ type ptrEncoder struct { } func (pe ptrEncoder) encode(e *encodeState, v reflect.Value, opts encOpts) { - // EDIT(begin) - // - // if v.IsNil() { - // e.WriteString("null") - // return - // } - - if v.IsNil() || sentinel.IsValueNullPtr(v) || sentinel.IsValueNullSlice(v) { + if v.IsNil() { e.WriteString("null") return } diff --git a/internal/encoding/json/sentinel/null.go b/internal/encoding/json/sentinel/null.go index a7ed3b7..32216c4 100644 --- a/internal/encoding/json/sentinel/null.go +++ b/internal/encoding/json/sentinel/null.go @@ -6,52 +6,41 @@ import ( "sync" ) -var nullPtrsCache sync.Map // map[reflect.Type]*T - -func NullPtr[T any]() *T { - t := shims.TypeFor[T]() - ptr, loaded := nullPtrsCache.Load(t) // avoid premature allocation - if !loaded { - ptr, _ = nullPtrsCache.LoadOrStore(t, new(T)) - } - return (ptr.(*T)) +type cacheEntry struct { + x any + ptr uintptr + kind reflect.Kind } -var nullSlicesCache sync.Map // map[reflect.Type][]T +var nullCache sync.Map // map[reflect.Type]cacheEntry -func NullSlice[T any]() []T { +func NewNullSentinel[T any](mk func() T) T { t := shims.TypeFor[T]() - slice, loaded := nullSlicesCache.Load(t) // avoid premature allocation + entry, loaded := nullCache.Load(t) // avoid premature allocation if !loaded { - slice, _ = nullSlicesCache.LoadOrStore(t, []T{}) + x := mk() + ptr := reflect.ValueOf(x).Pointer() + entry, _ = nullCache.LoadOrStore(t, cacheEntry{x, ptr, t.Kind()}) } - return slice.([]T) + return entry.(cacheEntry).x.(T) } -func IsNullPtr[T any](ptr *T) bool { - nullptr, ok := nullPtrsCache.Load(shims.TypeFor[T]()) - return ok && ptr == nullptr.(*T) -} - -func IsNullSlice[T any](slice []T) bool { - nullSlice, ok := nullSlicesCache.Load(shims.TypeFor[T]()) - return ok && reflect.ValueOf(slice).Pointer() == reflect.ValueOf(nullSlice).Pointer() -} - -// internal only -func IsValueNullPtr(v reflect.Value) bool { - if v.Kind() != reflect.Ptr { - return false +// for internal use only +func IsValueNull(v reflect.Value) bool { + switch v.Kind() { + case reflect.Map, reflect.Slice: + null, ok := nullCache.Load(v.Type()) + return ok && v.Pointer() == null.(cacheEntry).ptr } - nullptr, ok := nullPtrsCache.Load(v.Type().Elem()) - return ok && v.Pointer() == reflect.ValueOf(nullptr).Pointer() + return false } -// internal only -func IsValueNullSlice(v reflect.Value) bool { - if v.Kind() != reflect.Slice { - return false +func IsNull[T any](v T) bool { + t := shims.TypeFor[T]() + switch t.Kind() { + case reflect.Map, reflect.Slice: + null, ok := nullCache.Load(t) + return ok && reflect.ValueOf(v).Pointer() == null.(cacheEntry).ptr } - nullSlice, ok := nullSlicesCache.Load(v.Type().Elem()) - return ok && v.Pointer() == reflect.ValueOf(nullSlice).Pointer() + return false } diff --git a/internal/encoding/json/sentinel/sentinel_test.go b/internal/encoding/json/sentinel/sentinel_test.go index 4ecaa8e..623d50e 100644 --- a/internal/encoding/json/sentinel/sentinel_test.go +++ b/internal/encoding/json/sentinel/sentinel_test.go @@ -2,6 +2,7 @@ package sentinel_test import ( "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel" + "github.com/onkernel/kernel-go-sdk/packages/param" "reflect" "slices" "testing" @@ -15,25 +16,19 @@ type Pair struct { func TestNullSlice(t *testing.T) { var nilSlice []int = nil var nonNilSlice []int = []int{1, 2, 3} - var nullSlice []int = sentinel.NullSlice[int]() + var nullSlice []int = param.NullSlice[[]int]() cases := map[string]Pair{ - "nilSlice": {sentinel.IsNullSlice(nilSlice), false}, - "nullSlice": {sentinel.IsNullSlice(nullSlice), true}, - "newNullSlice": {sentinel.IsNullSlice(sentinel.NullSlice[int]()), true}, + "nilSlice": {sentinel.IsNull(nilSlice), false}, + "nullSlice": {sentinel.IsNull(nullSlice), true}, + "newNullSlice": {sentinel.IsNull(param.NullSlice[[]int]()), true}, "lenNullSlice": {len(nullSlice) == 0, true}, - "nilSliceValue": {sentinel.IsValueNullSlice(reflect.ValueOf(nilSlice)), false}, - "nullSliceValue": {sentinel.IsValueNullSlice(reflect.ValueOf(nullSlice)), true}, + "nilSliceValue": {sentinel.IsValueNull(reflect.ValueOf(nilSlice)), false}, + "nullSliceValue": {sentinel.IsValueNull(reflect.ValueOf(nullSlice)), true}, "compareSlices": {slices.Compare(nilSlice, nullSlice) == 0, true}, "compareNonNilSlices": {slices.Compare(nonNilSlice, nullSlice) == 0, false}, } - nilSlice = append(nullSlice, 12) - cases["append_result"] = Pair{sentinel.IsNullSlice(nilSlice), false} - cases["mutated_result"] = Pair{sentinel.IsNullSlice(nullSlice), true} - cases["append_result_len"] = Pair{len(nilSlice) == 1, true} - cases["append_null_slice_len"] = Pair{len(nullSlice) == 0, true} - for name, c := range cases { t.Run(name, func(t *testing.T) { got, want := c.got, c.want @@ -44,37 +39,20 @@ func TestNullSlice(t *testing.T) { } } -func TestNullPtr(t *testing.T) { - var s *string = nil - var i *int = nil - var slice *[]int = nil - - var nullptrStr *string = sentinel.NullPtr[string]() - var nullptrInt *int = sentinel.NullPtr[int]() - var nullptrSlice *[]int = sentinel.NullPtr[[]int]() - - if *nullptrStr != "" { - t.Errorf("Failed to safely deref") - } - if *nullptrInt != 0 { - t.Errorf("Failed to safely deref") - } - if len(*nullptrSlice) != 0 { - t.Errorf("Failed to safely deref") - } +func TestNullMap(t *testing.T) { + var nilMap map[string]int = nil + var nonNilMap map[string]int = map[string]int{"a": 1, "b": 2} + var nullMap map[string]int = param.NullMap[map[string]int]() cases := map[string]Pair{ - "nilStr": {sentinel.IsNullPtr(s), false}, - "nullStr": {sentinel.IsNullPtr(nullptrStr), true}, - - "nilInt": {sentinel.IsNullPtr(i), false}, - "nullInt": {sentinel.IsNullPtr(nullptrInt), true}, - - "nilSlice": {sentinel.IsNullPtr(slice), false}, - "nullSlice": {sentinel.IsNullPtr(nullptrSlice), true}, - - "nilValuePtr": {sentinel.IsValueNullPtr(reflect.ValueOf(i)), false}, - "nullValuePtr": {sentinel.IsValueNullPtr(reflect.ValueOf(nullptrInt)), true}, + "nilMap": {sentinel.IsNull(nilMap), false}, + "nullMap": {sentinel.IsNull(nullMap), true}, + "newNullMap": {sentinel.IsNull(param.NullMap[map[string]int]()), true}, + "lenNullMap": {len(nullMap) == 0, true}, + "nilMapValue": {sentinel.IsValueNull(reflect.ValueOf(nilMap)), false}, + "nullMapValue": {sentinel.IsValueNull(reflect.ValueOf(nullMap)), true}, + "compareMaps": {reflect.DeepEqual(nilMap, nullMap), false}, + "compareNonNilMaps": {reflect.DeepEqual(nonNilMap, nullMap), false}, } for name, test := range cases { @@ -86,3 +64,28 @@ func TestNullPtr(t *testing.T) { }) } } + +func TestIsNullRepeated(t *testing.T) { + // Test for slices + nullSlice1 := param.NullSlice[[]int]() + nullSlice2 := param.NullSlice[[]int]() + if !sentinel.IsNull(nullSlice1) { + t.Errorf("IsNull(nullSlice1) = false, want true") + } + if !sentinel.IsNull(nullSlice2) { + t.Errorf("IsNull(nullSlice2) = false, want true") + } + if !sentinel.IsNull(nullSlice1) || !sentinel.IsNull(nullSlice2) { + t.Errorf("IsNull should return true for all NullSlice instances") + } + + // Test for maps + nullMap1 := param.NullMap[map[string]int]() + nullMap2 := param.NullMap[map[string]int]() + if !sentinel.IsNull(nullMap1) { + t.Errorf("IsNull(nullMap1) = false, want true") + } + if !sentinel.IsNull(nullMap2) { + t.Errorf("IsNull(nullMap2) = false, want true") + } +} diff --git a/internal/paramutil/sentinel.go b/internal/paramutil/sentinel.go deleted file mode 100644 index a2aed25..0000000 --- a/internal/paramutil/sentinel.go +++ /dev/null @@ -1,31 +0,0 @@ -package paramutil - -import ( - "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel" -) - -// NullPtr returns a pointer to the zero value of the type T. -// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null. -// -// It is unspecified behavior to mutate the value pointed to by the returned pointer. -func NullPtr[T any]() *T { - return sentinel.NullPtr[T]() -} - -// IsNullPtr returns true if the pointer was created by [NullPtr]. -func IsNullPtr[T any](ptr *T) bool { - return sentinel.IsNullPtr(ptr) -} - -// NullSlice returns a non-nil slice with a length of 0. -// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null. -// -// It is undefined behavior to mutate the slice returned by [NullSlice]. -func NullSlice[T any]() []T { - return sentinel.NullSlice[T]() -} - -// IsNullSlice returns true if the slice was created by [NullSlice]. -func IsNullSlice[T any](slice []T) bool { - return sentinel.IsNullSlice(slice) -} diff --git a/packages/param/null.go b/packages/param/null.go new file mode 100644 index 0000000..7a4afe9 --- /dev/null +++ b/packages/param/null.go @@ -0,0 +1,19 @@ +package param + +import "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel" + +// NullMap returns a non-nil map with a length of 0. +// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null. +// +// It is unspecified behavior to mutate the slice returned by [NullSlice]. +func NullMap[MapT ~map[string]T, T any]() MapT { + return sentinel.NewNullSentinel(func() MapT { return make(MapT, 1) }) +} + +// NullSlice returns a non-nil slice with a length of 0. +// When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null. +// +// It is unspecified behavior to mutate the slice returned by [NullSlice]. +func NullSlice[SliceT ~[]T, T any]() SliceT { + return sentinel.NewNullSentinel(func() SliceT { return make(SliceT, 0, 1) }) +} diff --git a/packages/param/null_test.go b/packages/param/null_test.go new file mode 100644 index 0000000..0ecd68e --- /dev/null +++ b/packages/param/null_test.go @@ -0,0 +1,49 @@ +package param_test + +import ( + "encoding/json" + "github.com/onkernel/kernel-go-sdk/packages/param" + "testing" +) + +type Nullables struct { + Slice []int `json:"slice,omitzero"` + Map map[string]int `json:"map,omitzero"` + param.APIObject +} + +func (n Nullables) MarshalJSON() ([]byte, error) { + type shadow Nullables + return param.MarshalObject(n, (*shadow)(&n)) +} + +func TestNullMarshal(t *testing.T) { + bytes, err := json.Marshal(Nullables{}) + if err != nil { + t.Fatalf("json error %v", err.Error()) + } + if string(bytes) != `{}` { + t.Fatalf("expected empty object, got %s", string(bytes)) + } + + obj := Nullables{ + Slice: param.NullSlice[[]int](), + Map: param.NullMap[map[string]int](), + } + bytes, err = json.Marshal(obj) + + if !param.IsNull(obj.Slice) { + t.Fatal("failed null check") + } + if !param.IsNull(obj.Map) { + t.Fatal("failed null check") + } + + if err != nil { + t.Fatalf("json error %v", err.Error()) + } + exp := `{"slice":null,"map":null}` + if string(bytes) != exp { + t.Fatalf("expected %s, got %s", exp, string(bytes)) + } +} diff --git a/packages/param/param.go b/packages/param/param.go index c1bc099..2ad956f 100644 --- a/packages/param/param.go +++ b/packages/param/param.go @@ -2,6 +2,7 @@ package param import ( "encoding/json" + "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel" "reflect" ) @@ -58,12 +59,21 @@ func IsOmitted(v any) bool { // IsNull returns true if v was set to the JSON value null. // -// To set a param to null use [NullStruct] or [Null] +// To set a param to null use [NullStruct], [Null], [NullMap], or [NullSlice] // depending on the type of v. // // IsNull returns false if the value is omitted. -func IsNull(v ParamNullable) bool { - return v.null() +func IsNull[T any](v T) bool { + if nullable, ok := any(v).(ParamNullable); ok { + return nullable.null() + } + + switch reflect.TypeOf(v).Kind() { + case reflect.Slice, reflect.Map: + return sentinel.IsNull(v) + } + + return false } // ParamNullable encapsulates all structs in parameters, From a62b9647386501f43e70ad876dc5c0271c4c4709 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 21 Jun 2025 06:55:43 +0000 Subject: [PATCH 2/4] chore: fix documentation of null map --- packages/param/null.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/param/null.go b/packages/param/null.go index 7a4afe9..9281a4b 100644 --- a/packages/param/null.go +++ b/packages/param/null.go @@ -5,7 +5,7 @@ import "github.com/onkernel/kernel-go-sdk/internal/encoding/json/sentinel" // NullMap returns a non-nil map with a length of 0. // When used with [MarshalObject] or [MarshalUnion], it will be marshaled as null. // -// It is unspecified behavior to mutate the slice returned by [NullSlice]. +// It is unspecified behavior to mutate the map returned by [NullMap]. func NullMap[MapT ~map[string]T, T any]() MapT { return sentinel.NewNullSentinel(func() MapT { return make(MapT, 1) }) } From dc72c81a2e45918d595c6c00843b1a1d0efffdd0 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:49:35 +0000 Subject: [PATCH 3/4] feat(api): add `since` parameter to deployment logs endpoint --- .stats.yml | 6 +++--- aliases.go | 5 +++++ api.md | 3 ++- app.go | 3 +++ appdeployment.go | 13 +++++++++++-- client_test.go | 7 ++++++- deployment.go | 30 ++++++++++++++++++++++++++---- invocation.go | 13 +++++++++++-- shared/constant/constants.go | 3 +++ shared/shared.go | 24 ++++++++++++++++++++++++ 10 files changed, 94 insertions(+), 13 deletions(-) diff --git a/.stats.yml b/.stats.yml index 4a84456..b296d07 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 16 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-b019e469425a59061f37c5fdc7a131a5291c66134ef0627db4f06bb1f4af0b15.yml -openapi_spec_hash: f66a3c2efddb168db9539ba2507b10b8 -config_hash: aae6721b2be9ec8565dfc8f7eadfe105 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-2aec229ccf91f7c1ac95aa675ea2a59bd61af9e363a22c3b49677992f1eeb16a.yml +openapi_spec_hash: c80cd5d52a79cd5366a76d4a825bd27a +config_hash: b8e1fff080fbaa22656ab0a57b591777 diff --git a/aliases.go b/aliases.go index 3295d1b..e0267f2 100644 --- a/aliases.go +++ b/aliases.go @@ -27,6 +27,11 @@ type ErrorEvent = shared.ErrorEvent // This is an alias to an internal type. type ErrorModel = shared.ErrorModel +// Heartbeat event sent periodically to keep SSE connection alive. +// +// This is an alias to an internal type. +type HeartbeatEvent = shared.HeartbeatEvent + // A log entry from the application. // // This is an alias to an internal type. diff --git a/api.md b/api.md index e1bf732..2a53bb8 100644 --- a/api.md +++ b/api.md @@ -3,6 +3,7 @@ - shared.ErrorDetail - shared.ErrorEvent - shared.ErrorModel +- shared.HeartbeatEvent - shared.LogEvent # Deployments @@ -18,7 +19,7 @@ Methods: - client.Deployments.New(ctx context.Context, body kernel.DeploymentNewParams) (kernel.DeploymentNewResponse, error) - client.Deployments.Get(ctx context.Context, id string) (kernel.DeploymentGetResponse, error) -- client.Deployments.Follow(ctx context.Context, id string) (kernel.DeploymentFollowResponseUnion, error) +- client.Deployments.Follow(ctx context.Context, id string, query kernel.DeploymentFollowParams) (kernel.DeploymentFollowResponseUnion, error) # Apps diff --git a/app.go b/app.go index a38ef15..b469553 100644 --- a/app.go +++ b/app.go @@ -51,6 +51,8 @@ type AppListResponse struct { ID string `json:"id,required"` // Name of the application AppName string `json:"app_name,required"` + // Deployment ID + Deployment string `json:"deployment,required"` // Deployment region code Region constant.AwsUsEast1a `json:"region,required"` // Version label for the application @@ -61,6 +63,7 @@ type AppListResponse struct { JSON struct { ID respjson.Field AppName respjson.Field + Deployment respjson.Field Region respjson.Field Version respjson.Field EnvVars respjson.Field diff --git a/appdeployment.go b/appdeployment.go index 0199bd0..692bc62 100644 --- a/appdeployment.go +++ b/appdeployment.go @@ -147,14 +147,15 @@ const ( // AppDeploymentFollowResponseUnion contains all possible properties and values // from [AppDeploymentFollowResponseState], -// [AppDeploymentFollowResponseStateUpdate], [shared.LogEvent]. +// [AppDeploymentFollowResponseStateUpdate], [shared.LogEvent], +// [shared.HeartbeatEvent]. // // Use the [AppDeploymentFollowResponseUnion.AsAny] method to switch on the // variant. // // Use the methods beginning with 'As' to cast the union to one of its variants. type AppDeploymentFollowResponseUnion struct { - // Any of "state", "state_update", "log". + // Any of "state", "state_update", "log", "sse_heartbeat". Event string `json:"event"` State string `json:"state"` Timestamp time.Time `json:"timestamp"` @@ -185,6 +186,7 @@ func (AppDeploymentFollowResponseStateUpdate) ImplAppDeploymentFollowResponseUni // case kernel.AppDeploymentFollowResponseState: // case kernel.AppDeploymentFollowResponseStateUpdate: // case shared.LogEvent: +// case shared.HeartbeatEvent: // default: // fmt.Errorf("no variant present") // } @@ -196,6 +198,8 @@ func (u AppDeploymentFollowResponseUnion) AsAny() anyAppDeploymentFollowResponse return u.AsStateUpdate() case "log": return u.AsLog() + case "sse_heartbeat": + return u.AsSseHeartbeat() } return nil } @@ -215,6 +219,11 @@ func (u AppDeploymentFollowResponseUnion) AsLog() (v shared.LogEvent) { return } +func (u AppDeploymentFollowResponseUnion) AsSseHeartbeat() (v shared.HeartbeatEvent) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + // Returns the unmodified JSON received from the API func (u AppDeploymentFollowResponseUnion) RawJSON() string { return u.JSON.raw } diff --git a/client_test.go b/client_test.go index 6abf48c..96b8cb9 100644 --- a/client_test.go +++ b/client_test.go @@ -311,7 +311,11 @@ func TestContextDeadlineStreaming(t *testing.T) { }, }), ) - stream := client.Deployments.FollowStreaming(deadlineCtx, "id") + stream := client.Deployments.FollowStreaming( + deadlineCtx, + "id", + kernel.DeploymentFollowParams{}, + ) for stream.Next() { _ = stream.Current() } @@ -359,6 +363,7 @@ func TestContextDeadlineStreamingWithRequestTimeout(t *testing.T) { stream := client.Deployments.FollowStreaming( context.Background(), "id", + kernel.DeploymentFollowParams{}, option.WithRequestTimeout((100 * time.Millisecond)), ) for stream.Next() { diff --git a/deployment.go b/deployment.go index 70422aa..26f1d23 100644 --- a/deployment.go +++ b/deployment.go @@ -11,10 +11,12 @@ import ( "io" "mime/multipart" "net/http" + "net/url" "time" "github.com/onkernel/kernel-go-sdk/internal/apiform" "github.com/onkernel/kernel-go-sdk/internal/apijson" + "github.com/onkernel/kernel-go-sdk/internal/apiquery" "github.com/onkernel/kernel-go-sdk/internal/requestconfig" "github.com/onkernel/kernel-go-sdk/option" "github.com/onkernel/kernel-go-sdk/packages/param" @@ -66,7 +68,7 @@ func (r *DeploymentService) Get(ctx context.Context, id string, opts ...option.R // Establishes a Server-Sent Events (SSE) stream that delivers real-time logs and // status updates for a deployment. The stream terminates automatically once the // deployment reaches a terminal state. -func (r *DeploymentService) FollowStreaming(ctx context.Context, id string, opts ...option.RequestOption) (stream *ssestream.Stream[DeploymentFollowResponseUnion]) { +func (r *DeploymentService) FollowStreaming(ctx context.Context, id string, query DeploymentFollowParams, opts ...option.RequestOption) (stream *ssestream.Stream[DeploymentFollowResponseUnion]) { var ( raw *http.Response err error @@ -78,7 +80,7 @@ func (r *DeploymentService) FollowStreaming(ctx context.Context, id string, opts return } path := fmt.Sprintf("deployments/%s/events", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &raw, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, query, &raw, opts...) return ssestream.NewStream[DeploymentFollowResponseUnion](ssestream.NewDecoder(raw), err) } @@ -253,13 +255,14 @@ const ( // DeploymentFollowResponseUnion contains all possible properties and values from // [shared.LogEvent], [DeploymentStateEvent], -// [DeploymentFollowResponseAppVersionSummaryEvent], [shared.ErrorEvent]. +// [DeploymentFollowResponseAppVersionSummaryEvent], [shared.ErrorEvent], +// [shared.HeartbeatEvent]. // // Use the [DeploymentFollowResponseUnion.AsAny] method to switch on the variant. // // Use the methods beginning with 'As' to cast the union to one of its variants. type DeploymentFollowResponseUnion struct { - // Any of "log", "deployment_state", nil, nil. + // Any of "log", "deployment_state", nil, nil, "sse_heartbeat". Event string `json:"event"` // This field is from variant [shared.LogEvent]. Message string `json:"message"` @@ -316,6 +319,11 @@ func (u DeploymentFollowResponseUnion) AsErrorEvent() (v shared.ErrorEvent) { return } +func (u DeploymentFollowResponseUnion) AsSseHeartbeat() (v shared.HeartbeatEvent) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + // Returns the unmodified JSON received from the API func (u DeploymentFollowResponseUnion) RawJSON() string { return u.JSON.raw } @@ -423,3 +431,17 @@ type DeploymentNewParamsRegion string const ( DeploymentNewParamsRegionAwsUsEast1a DeploymentNewParamsRegion = "aws.us-east-1a" ) + +type DeploymentFollowParams struct { + // Show logs since the given time (RFC timestamps or durations like 5m). + Since param.Opt[string] `query:"since,omitzero" json:"-"` + paramObj +} + +// URLQuery serializes [DeploymentFollowParams]'s query parameters as `url.Values`. +func (r DeploymentFollowParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) +} diff --git a/invocation.go b/invocation.go index f74b814..5dc72ae 100644 --- a/invocation.go +++ b/invocation.go @@ -321,13 +321,14 @@ const ( ) // InvocationFollowResponseUnion contains all possible properties and values from -// [shared.LogEvent], [InvocationStateEvent], [shared.ErrorEvent]. +// [shared.LogEvent], [InvocationStateEvent], [shared.ErrorEvent], +// [shared.HeartbeatEvent]. // // Use the [InvocationFollowResponseUnion.AsAny] method to switch on the variant. // // Use the methods beginning with 'As' to cast the union to one of its variants. type InvocationFollowResponseUnion struct { - // Any of "log", "invocation_state", "error". + // Any of "log", "invocation_state", "error", "sse_heartbeat". Event string `json:"event"` // This field is from variant [shared.LogEvent]. Message string `json:"message"` @@ -361,6 +362,7 @@ func (InvocationStateEvent) ImplInvocationFollowResponseUnion() {} // case shared.LogEvent: // case kernel.InvocationStateEvent: // case shared.ErrorEvent: +// case shared.HeartbeatEvent: // default: // fmt.Errorf("no variant present") // } @@ -372,6 +374,8 @@ func (u InvocationFollowResponseUnion) AsAny() anyInvocationFollowResponse { return u.AsInvocationState() case "error": return u.AsError() + case "sse_heartbeat": + return u.AsSseHeartbeat() } return nil } @@ -391,6 +395,11 @@ func (u InvocationFollowResponseUnion) AsError() (v shared.ErrorEvent) { return } +func (u InvocationFollowResponseUnion) AsSseHeartbeat() (v shared.HeartbeatEvent) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + // Returns the unmodified JSON received from the API func (u InvocationFollowResponseUnion) RawJSON() string { return u.JSON.raw } diff --git a/shared/constant/constants.go b/shared/constant/constants.go index 2b9f076..d1e41c7 100644 --- a/shared/constant/constants.go +++ b/shared/constant/constants.go @@ -24,6 +24,7 @@ type DeploymentState string // Always "deployment_state" type Error string // Always "error" type InvocationState string // Always "invocation_state" type Log string // Always "log" +type SseHeartbeat string // Always "sse_heartbeat" type State string // Always "state" type StateUpdate string // Always "state_update" @@ -33,6 +34,7 @@ func (c DeploymentState) Default() DeploymentState { return "deployment_stat func (c Error) Default() Error { return "error" } func (c InvocationState) Default() InvocationState { return "invocation_state" } func (c Log) Default() Log { return "log" } +func (c SseHeartbeat) Default() SseHeartbeat { return "sse_heartbeat" } func (c State) Default() State { return "state" } func (c StateUpdate) Default() StateUpdate { return "state_update" } @@ -42,6 +44,7 @@ func (c DeploymentState) MarshalJSON() ([]byte, error) { return marshalString( func (c Error) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c InvocationState) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c Log) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c SseHeartbeat) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c State) MarshalJSON() ([]byte, error) { return marshalString(c) } func (c StateUpdate) MarshalJSON() ([]byte, error) { return marshalString(c) } diff --git a/shared/shared.go b/shared/shared.go index 4b63e7c..0115c3c 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -87,6 +87,30 @@ func (r *ErrorModel) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// Heartbeat event sent periodically to keep SSE connection alive. +type HeartbeatEvent struct { + // Event type identifier (always "sse_heartbeat"). + Event constant.SseHeartbeat `json:"event,required"` + // Time the heartbeat was sent. + Timestamp time.Time `json:"timestamp,required" format:"date-time"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Event respjson.Field + Timestamp respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r HeartbeatEvent) RawJSON() string { return r.JSON.raw } +func (r *HeartbeatEvent) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +func (HeartbeatEvent) ImplAppDeploymentFollowResponseUnion() {} +func (HeartbeatEvent) ImplInvocationFollowResponseUnion() {} + // A log entry from the application. type LogEvent struct { // Event type identifier (always "log"). From 86cafea8ea3772f4857fa1719ed2f9e0a1999143 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:51:33 +0000 Subject: [PATCH 4/4] release: 0.6.2 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 14 ++++++++++++++ README.md | 2 +- internal/version.go | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ac03171..e3778b2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.6.1" + ".": "0.6.2" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a9bece6..d38e6f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 0.6.2 (2025-06-24) + +Full Changelog: [v0.6.1...v0.6.2](https://github.com/onkernel/kernel-go-sdk/compare/v0.6.1...v0.6.2) + +### Features + +* **api:** add `since` parameter to deployment logs endpoint ([dc72c81](https://github.com/onkernel/kernel-go-sdk/commit/dc72c81a2e45918d595c6c00843b1a1d0efffdd0)) +* **client:** add escape hatch for null slice & maps ([d5e1ad9](https://github.com/onkernel/kernel-go-sdk/commit/d5e1ad9087aecd6b67369b9ebbeb633ad808c129)) + + +### Chores + +* fix documentation of null map ([a62b964](https://github.com/onkernel/kernel-go-sdk/commit/a62b9647386501f43e70ad876dc5c0271c4c4709)) + ## 0.6.1 (2025-06-18) Full Changelog: [v0.6.0...v0.6.1](https://github.com/onkernel/kernel-go-sdk/compare/v0.6.0...v0.6.1) diff --git a/README.md b/README.md index bef30e9..3058e0b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Or to pin the version: ```sh -go get -u 'github.com/onkernel/kernel-go-sdk@v0.6.1' +go get -u 'github.com/onkernel/kernel-go-sdk@v0.6.2' ``` diff --git a/internal/version.go b/internal/version.go index 6dfc989..01e812b 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.6.1" // x-release-please-version +const PackageVersion = "0.6.2" // x-release-please-version