diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cb9d254..7f3f5c8 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.22.0" + ".": "0.23.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 0c474bb..135345a 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 80 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8a37652fa586b8932466d16285359a89988505f850787f8257d0c4c7053da173.yml -openapi_spec_hash: 042765a113f6d08109e8146b302323ec -config_hash: 113f1e5bc3567628a5d51c70bc00969d +configured_endpoints: 82 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-dac11bdb857e700a8c39d183e753ddd1ebaaca69fd9fc5ee57d6b56b70b00e6e.yml +openapi_spec_hash: 78fbc50dd0b61cdc87564fbea278ee23 +config_hash: a4b4d14bdf6af723b235a6981977627c diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abbe96..c17fef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 0.23.0 (2025-12-16) + +Full Changelog: [v0.22.0...v0.23.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.22.0...v0.23.0) + +### Features + +* **encoder:** support bracket encoding form-data object members ([7d11b85](https://github.com/onkernel/kernel-go-sdk/commit/7d11b85f517bfa5875440c516de735009fbd05a0)) +* enhance agent authentication API with new endpoints and request… ([7f2d67a](https://github.com/onkernel/kernel-go-sdk/commit/7f2d67aeba880e8d35b084ffa6f0c2364f88378a)) +* Enhance AuthAgent model with last_auth_check_at field ([a3cb1e1](https://github.com/onkernel/kernel-go-sdk/commit/a3cb1e13b7700f1ccce3393411a08f822e216d1f)) + + +### Bug Fixes + +* **client:** copy over change to params names to ExecuteNewRequeest ([92fd6c1](https://github.com/onkernel/kernel-go-sdk/commit/92fd6c10cb2c451f9381d3969c2aa80b121addfe)) +* **mcp:** correct code tool API endpoint ([563016a](https://github.com/onkernel/kernel-go-sdk/commit/563016aabd8010af7bf2d1db0b7436f31b0fd23e)) +* rename param to avoid collision ([a20c158](https://github.com/onkernel/kernel-go-sdk/commit/a20c1588a19c48cfa6503af80c0ba6ad4add76f7)) + + +### Chores + +* elide duplicate aliases ([9eb4ec3](https://github.com/onkernel/kernel-go-sdk/commit/9eb4ec3048ab55e534e84de255ca0f2d22d5f233)) +* **internal:** codegen related update ([839598d](https://github.com/onkernel/kernel-go-sdk/commit/839598d5a188b64afe16e1df16915c94f822b309)) + ## 0.22.0 (2025-12-06) Full Changelog: [v0.21.0...v0.22.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.21.0...v0.22.0) diff --git a/README.md b/README.md index 43fd6a5..ab51c2e 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/onkernel/kernel-go-sdk@v0.22.0' +go get -u 'github.com/onkernel/kernel-go-sdk@v0.23.0' ``` diff --git a/agentauth.go b/agentauth.go index 16311d1..b2c534f 100644 --- a/agentauth.go +++ b/agentauth.go @@ -4,15 +4,20 @@ package kernel import ( "context" + "encoding/json" "errors" "fmt" "net/http" + "net/url" "slices" "time" "github.com/onkernel/kernel-go-sdk/internal/apijson" + "github.com/onkernel/kernel-go-sdk/internal/apiquery" + shimjson "github.com/onkernel/kernel-go-sdk/internal/encoding/json" "github.com/onkernel/kernel-go-sdk/internal/requestconfig" "github.com/onkernel/kernel-go-sdk/option" + "github.com/onkernel/kernel-go-sdk/packages/pagination" "github.com/onkernel/kernel-go-sdk/packages/param" "github.com/onkernel/kernel-go-sdk/packages/respjson" ) @@ -38,6 +43,17 @@ func NewAgentAuthService(opts ...option.RequestOption) (r AgentAuthService) { return } +// Creates a new auth agent for the specified domain and profile combination, or +// returns an existing one if it already exists. This is idempotent - calling with +// the same domain and profile will return the same agent. Does NOT start an +// invocation - use POST /agents/auth/invocations to start an auth flow. +func (r *AgentAuthService) New(ctx context.Context, body AgentAuthNewParams, opts ...option.RequestOption) (res *AuthAgent, err error) { + opts = slices.Concat(r.Options, opts) + path := "agents/auth" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + // Retrieve an auth agent by its ID. Returns the current authentication status of // the managed profile. func (r *AgentAuthService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *AuthAgent, err error) { @@ -51,14 +67,27 @@ func (r *AgentAuthService) Get(ctx context.Context, id string, opts ...option.Re return } -// Creates a browser session and returns a handoff code for the hosted flow. Uses -// standard API key or JWT authentication (not the JWT returned by the exchange -// endpoint). -func (r *AgentAuthService) Start(ctx context.Context, body AgentAuthStartParams, opts ...option.RequestOption) (res *AgentAuthStartResponse, err error) { +// List auth agents with optional filters for profile_name and target_domain. +func (r *AgentAuthService) List(ctx context.Context, query AgentAuthListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[AuthAgent], err error) { + var raw *http.Response opts = slices.Concat(r.Options, opts) - path := "agents/auth/start" - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return + opts = append([]option.RequestOption{option.WithResponseInto(&raw)}, opts...) + path := "agents/auth" + cfg, err := requestconfig.NewRequestConfig(ctx, http.MethodGet, path, query, &res, opts...) + if err != nil { + return nil, err + } + err = cfg.Execute() + if err != nil { + return nil, err + } + res.SetPageConfig(cfg, raw) + return res, nil +} + +// List auth agents with optional filters for profile_name and target_domain. +func (r *AgentAuthService) ListAutoPaging(ctx context.Context, query AgentAuthListParams, opts ...option.RequestOption) *pagination.OffsetPaginationAutoPager[AuthAgent] { + return pagination.NewOffsetPaginationAutoPager(r.List(ctx, query, opts...)) } // Response from discover endpoint matching AuthBlueprint schema @@ -133,36 +162,6 @@ const ( AgentAuthInvocationResponseStatusCanceled AgentAuthInvocationResponseStatus = "CANCELED" ) -// Response from starting an agent authentication invocation -type AgentAuthStartResponse struct { - // Unique identifier for the auth agent managing this domain/profile - AuthAgentID string `json:"auth_agent_id,required"` - // When the handoff code expires - ExpiresAt time.Time `json:"expires_at,required" format:"date-time"` - // One-time code for handoff - HandoffCode string `json:"handoff_code,required"` - // URL to redirect user to - HostedURL string `json:"hosted_url,required" format:"uri"` - // Unique identifier for the invocation - InvocationID string `json:"invocation_id,required"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - AuthAgentID respjson.Field - ExpiresAt respjson.Field - HandoffCode respjson.Field - HostedURL respjson.Field - InvocationID respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r AgentAuthStartResponse) RawJSON() string { return r.JSON.raw } -func (r *AgentAuthStartResponse) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - // Response from submit endpoint matching SubmitResult schema type AgentAuthSubmitResponse struct { // Whether submission succeeded @@ -213,14 +212,17 @@ type AuthAgent struct { // // Any of "AUTHENTICATED", "NEEDS_AUTH". Status AuthAgentStatus `json:"status,required"` + // When the last authentication check was performed + LastAuthCheckAt time.Time `json:"last_auth_check_at" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - Domain respjson.Field - ProfileName respjson.Field - Status respjson.Field - ExtraFields map[string]respjson.Field - raw string + ID respjson.Field + Domain respjson.Field + ProfileName respjson.Field + Status respjson.Field + LastAuthCheckAt respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -238,6 +240,89 @@ const ( AuthAgentStatusNeedsAuth AuthAgentStatus = "NEEDS_AUTH" ) +// Request to create or find an auth agent +// +// The properties ProfileName, TargetDomain are required. +type AuthAgentCreateRequestParam struct { + // Name of the profile to use for this auth agent + ProfileName string `json:"profile_name,required"` + // Target domain for authentication + TargetDomain string `json:"target_domain,required"` + // Optional login page URL. If provided, will be stored on the agent and used to + // skip discovery in future invocations. + LoginURL param.Opt[string] `json:"login_url,omitzero" format:"uri"` + // Optional proxy configuration + Proxy AuthAgentCreateRequestProxyParam `json:"proxy,omitzero"` + paramObj +} + +func (r AuthAgentCreateRequestParam) MarshalJSON() (data []byte, err error) { + type shadow AuthAgentCreateRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *AuthAgentCreateRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Optional proxy configuration +type AuthAgentCreateRequestProxyParam struct { + // ID of the proxy to use + ProxyID param.Opt[string] `json:"proxy_id,omitzero"` + paramObj +} + +func (r AuthAgentCreateRequestProxyParam) MarshalJSON() (data []byte, err error) { + type shadow AuthAgentCreateRequestProxyParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *AuthAgentCreateRequestProxyParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Request to create an invocation for an existing auth agent +// +// The property AuthAgentID is required. +type AuthAgentInvocationCreateRequestParam struct { + // ID of the auth agent to create an invocation for + AuthAgentID string `json:"auth_agent_id,required"` + paramObj +} + +func (r AuthAgentInvocationCreateRequestParam) MarshalJSON() (data []byte, err error) { + type shadow AuthAgentInvocationCreateRequestParam + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *AuthAgentInvocationCreateRequestParam) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Response from creating an auth agent invocation +type AuthAgentInvocationCreateResponse struct { + // When the handoff code expires + ExpiresAt time.Time `json:"expires_at,required" format:"date-time"` + // One-time code for handoff + HandoffCode string `json:"handoff_code,required"` + // URL to redirect user to + HostedURL string `json:"hosted_url,required" format:"uri"` + // Unique identifier for the invocation + InvocationID string `json:"invocation_id,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + ExpiresAt respjson.Field + HandoffCode respjson.Field + HostedURL respjson.Field + InvocationID respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r AuthAgentInvocationCreateResponse) RawJSON() string { return r.JSON.raw } +func (r *AuthAgentInvocationCreateResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + // A discovered form field type DiscoveredField struct { // Field label @@ -286,40 +371,35 @@ const ( DiscoveredFieldTypeCode DiscoveredFieldType = "code" ) -type AgentAuthStartParams struct { - // Name of the profile to use for this flow - ProfileName string `json:"profile_name,required"` - // Target domain for authentication - TargetDomain string `json:"target_domain,required"` - // Optional logo URL for the application - AppLogoURL param.Opt[string] `json:"app_logo_url,omitzero" format:"uri"` - // Optional login page URL. If provided, will be stored on the agent and used to - // skip Phase 1 discovery in future invocations. - LoginURL param.Opt[string] `json:"login_url,omitzero" format:"uri"` - // Optional proxy configuration - Proxy AgentAuthStartParamsProxy `json:"proxy,omitzero"` +type AgentAuthNewParams struct { + // Request to create or find an auth agent + AuthAgentCreateRequest AuthAgentCreateRequestParam paramObj } -func (r AgentAuthStartParams) MarshalJSON() (data []byte, err error) { - type shadow AgentAuthStartParams - return param.MarshalObject(r, (*shadow)(&r)) +func (r AgentAuthNewParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.AuthAgentCreateRequest) } -func (r *AgentAuthStartParams) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) +func (r *AgentAuthNewParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.AuthAgentCreateRequest) } -// Optional proxy configuration -type AgentAuthStartParamsProxy struct { - // ID of the proxy to use - ProxyID param.Opt[string] `json:"proxy_id,omitzero"` +type AgentAuthListParams struct { + // Maximum number of results to return + Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` + // Number of results to skip + Offset param.Opt[int64] `query:"offset,omitzero" json:"-"` + // Filter by profile name + ProfileName param.Opt[string] `query:"profile_name,omitzero" json:"-"` + // Filter by target domain + TargetDomain param.Opt[string] `query:"target_domain,omitzero" json:"-"` paramObj } -func (r AgentAuthStartParamsProxy) MarshalJSON() (data []byte, err error) { - type shadow AgentAuthStartParamsProxy - return param.MarshalObject(r, (*shadow)(&r)) -} -func (r *AgentAuthStartParamsProxy) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) +// URLQuery serializes [AgentAuthListParams]'s query parameters as `url.Values`. +func (r AgentAuthListParams) URLQuery() (v url.Values, err error) { + return apiquery.MarshalWithSettings(r, apiquery.QuerySettings{ + ArrayFormat: apiquery.ArrayQueryFormatComma, + NestedFormat: apiquery.NestedQueryFormatBrackets, + }) } diff --git a/agentauth_test.go b/agentauth_test.go index aa21286..7cc8517 100644 --- a/agentauth_test.go +++ b/agentauth_test.go @@ -13,6 +13,38 @@ import ( "github.com/onkernel/kernel-go-sdk/option" ) +func TestAgentAuthNewWithOptionalParams(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Agents.Auth.New(context.TODO(), kernel.AgentAuthNewParams{ + AuthAgentCreateRequest: kernel.AuthAgentCreateRequestParam{ + ProfileName: "user-123", + TargetDomain: "netflix.com", + LoginURL: kernel.String("https://netflix.com/login"), + Proxy: kernel.AuthAgentCreateRequestProxyParam{ + ProxyID: kernel.String("proxy_id"), + }, + }, + }) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestAgentAuthGet(t *testing.T) { t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" @@ -36,7 +68,7 @@ func TestAgentAuthGet(t *testing.T) { } } -func TestAgentAuthStartWithOptionalParams(t *testing.T) { +func TestAgentAuthListWithOptionalParams(t *testing.T) { t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -49,14 +81,11 @@ func TestAgentAuthStartWithOptionalParams(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Agents.Auth.Start(context.TODO(), kernel.AgentAuthStartParams{ - ProfileName: "auth-abc123", - TargetDomain: "doordash.com", - AppLogoURL: kernel.String("https://example.com/logo.png"), - LoginURL: kernel.String("https://doordash.com/account/login"), - Proxy: kernel.AgentAuthStartParamsProxy{ - ProxyID: kernel.String("proxy_id"), - }, + _, err := client.Agents.Auth.List(context.TODO(), kernel.AgentAuthListParams{ + Limit: kernel.Int(100), + Offset: kernel.Int(0), + ProfileName: kernel.String("profile_name"), + TargetDomain: kernel.String("target_domain"), }) if err != nil { var apierr *kernel.Error diff --git a/agentauthinvocation.go b/agentauthinvocation.go index e9b8ccb..3a12d03 100644 --- a/agentauthinvocation.go +++ b/agentauthinvocation.go @@ -4,12 +4,14 @@ package kernel import ( "context" + "encoding/json" "errors" "fmt" "net/http" "slices" "github.com/onkernel/kernel-go-sdk/internal/apijson" + shimjson "github.com/onkernel/kernel-go-sdk/internal/encoding/json" "github.com/onkernel/kernel-go-sdk/internal/requestconfig" "github.com/onkernel/kernel-go-sdk/option" "github.com/onkernel/kernel-go-sdk/packages/param" @@ -35,6 +37,16 @@ func NewAgentAuthInvocationService(opts ...option.RequestOption) (r AgentAuthInv return } +// Creates a new authentication invocation for the specified auth agent. This +// starts the auth flow and returns a hosted URL for the user to complete +// authentication. +func (r *AgentAuthInvocationService) New(ctx context.Context, body AgentAuthInvocationNewParams, opts ...option.RequestOption) (res *AuthAgentInvocationCreateResponse, err error) { + opts = slices.Concat(r.Options, opts) + path := "agents/auth/invocations" + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) + return +} + // Returns invocation details including app_name and target_domain. Uses the JWT // returned by the exchange endpoint, or standard API key or JWT authentication. func (r *AgentAuthInvocationService) Get(ctx context.Context, invocationID string, opts ...option.RequestOption) (res *AgentAuthInvocationResponse, err error) { @@ -109,6 +121,19 @@ func (r *AgentAuthInvocationExchangeResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type AgentAuthInvocationNewParams struct { + // Request to create an invocation for an existing auth agent + AuthAgentInvocationCreateRequest AuthAgentInvocationCreateRequestParam + paramObj +} + +func (r AgentAuthInvocationNewParams) MarshalJSON() (data []byte, err error) { + return shimjson.Marshal(r.AuthAgentInvocationCreateRequest) +} +func (r *AgentAuthInvocationNewParams) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &r.AuthAgentInvocationCreateRequest) +} + type AgentAuthInvocationDiscoverParams struct { // Optional login page URL. If provided, will override the stored login URL for // this discovery invocation and skip Phase 1 discovery. diff --git a/agentauthinvocation_test.go b/agentauthinvocation_test.go index 367ff15..d89d3a8 100644 --- a/agentauthinvocation_test.go +++ b/agentauthinvocation_test.go @@ -13,6 +13,33 @@ import ( "github.com/onkernel/kernel-go-sdk/option" ) +func TestAgentAuthInvocationNew(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Agents.Auth.Invocations.New(context.TODO(), kernel.AgentAuthInvocationNewParams{ + AuthAgentInvocationCreateRequest: kernel.AuthAgentInvocationCreateRequestParam{ + AuthAgentID: "abc123xyz", + }, + }) + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + func TestAgentAuthInvocationGet(t *testing.T) { t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" diff --git a/api.md b/api.md index 80ddb73..b83ecc8 100644 --- a/api.md +++ b/api.md @@ -120,7 +120,7 @@ Methods: - client.Browsers.Fs.SetFilePermissions(ctx context.Context, id string, body kernel.BrowserFSetFilePermissionsParams) error - client.Browsers.Fs.Upload(ctx context.Context, id string, body kernel.BrowserFUploadParams) error - client.Browsers.Fs.UploadZip(ctx context.Context, id string, body kernel.BrowserFUploadZipParams) error -- client.Browsers.Fs.WriteFile(ctx context.Context, id string, contents io.Reader, body kernel.BrowserFWriteFileParams) error +- client.Browsers.Fs.WriteFile(ctx context.Context, id string, contents io.Reader, params kernel.BrowserFWriteFileParams) error ### Watch @@ -258,19 +258,25 @@ Methods: ## Auth +Params Types: + +- kernel.AuthAgentCreateRequestParam +- kernel.AuthAgentInvocationCreateRequestParam + Response Types: - kernel.AgentAuthDiscoverResponse - kernel.AgentAuthInvocationResponse -- kernel.AgentAuthStartResponse - kernel.AgentAuthSubmitResponse - kernel.AuthAgent +- kernel.AuthAgentInvocationCreateResponse - kernel.DiscoveredField Methods: +- client.Agents.Auth.New(ctx context.Context, body kernel.AgentAuthNewParams) (kernel.AuthAgent, error) - client.Agents.Auth.Get(ctx context.Context, id string) (kernel.AuthAgent, error) -- client.Agents.Auth.Start(ctx context.Context, body kernel.AgentAuthStartParams) (kernel.AgentAuthStartResponse, error) +- client.Agents.Auth.List(ctx context.Context, query kernel.AgentAuthListParams) (pagination.OffsetPagination[kernel.AuthAgent], error) ### Invocations @@ -280,6 +286,7 @@ Response Types: Methods: +- client.Agents.Auth.Invocations.New(ctx context.Context, body kernel.AgentAuthInvocationNewParams) (kernel.AuthAgentInvocationCreateResponse, error) - client.Agents.Auth.Invocations.Get(ctx context.Context, invocationID string) (kernel.AgentAuthInvocationResponse, error) - client.Agents.Auth.Invocations.Discover(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationDiscoverParams) (kernel.AgentAuthDiscoverResponse, error) - client.Agents.Auth.Invocations.Exchange(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationExchangeParams) (kernel.AgentAuthInvocationExchangeResponse, error) diff --git a/browserf.go b/browserf.go index 4d1c499..ec67a04 100644 --- a/browserf.go +++ b/browserf.go @@ -186,7 +186,7 @@ func (r *BrowserFService) UploadZip(ctx context.Context, id string, body Browser } // Write or create a file -func (r *BrowserFService) WriteFile(ctx context.Context, id string, contents io.Reader, body BrowserFWriteFileParams, opts ...option.RequestOption) (err error) { +func (r *BrowserFService) WriteFile(ctx context.Context, id string, contents io.Reader, params BrowserFWriteFileParams, opts ...option.RequestOption) (err error) { opts = slices.Concat(r.Options, opts) opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*"), option.WithRequestBody("application/octet-stream", contents)}, opts...) if id == "" { @@ -194,7 +194,7 @@ func (r *BrowserFService) WriteFile(ctx context.Context, id string, contents io. return } path := fmt.Sprintf("browsers/%s/fs/write_file", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, body, nil, opts...) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPut, path, params, nil, opts...) return } diff --git a/internal/apiform/encoder.go b/internal/apiform/encoder.go index 2002c4b..851e9c0 100644 --- a/internal/apiform/encoder.go +++ b/internal/apiform/encoder.go @@ -60,6 +60,7 @@ type encoderField struct { type encoderEntry struct { reflect.Type dateFormat string + arrayFmt string root bool } @@ -77,6 +78,7 @@ func (e *encoder) typeEncoder(t reflect.Type) encoderFunc { entry := encoderEntry{ Type: t, dateFormat: e.dateFormat, + arrayFmt: e.arrayFmt, root: e.root, } @@ -178,34 +180,9 @@ func (e *encoder) newPrimitiveTypeEncoder(t reflect.Type) encoderFunc { } } -func arrayKeyEncoder(arrayFmt string) func(string, int) string { - var keyFn func(string, int) string - switch arrayFmt { - case "comma", "repeat": - keyFn = func(k string, _ int) string { return k } - case "brackets": - keyFn = func(key string, _ int) string { return key + "[]" } - case "indices:dots": - keyFn = func(k string, i int) string { - if k == "" { - return strconv.Itoa(i) - } - return k + "." + strconv.Itoa(i) - } - case "indices:brackets": - keyFn = func(k string, i int) string { - if k == "" { - return strconv.Itoa(i) - } - return k + "[" + strconv.Itoa(i) + "]" - } - } - return keyFn -} - func (e *encoder) newArrayTypeEncoder(t reflect.Type) encoderFunc { itemEncoder := e.typeEncoder(t.Elem()) - keyFn := arrayKeyEncoder(e.arrayFmt) + keyFn := e.arrayKeyEncoder() return func(key string, v reflect.Value, writer *multipart.Writer) error { if keyFn == nil { return fmt.Errorf("apiform: unsupported array format") @@ -303,13 +280,10 @@ func (e *encoder) newStructTypeEncoder(t reflect.Type) encoderFunc { }) return func(key string, value reflect.Value, writer *multipart.Writer) error { - if key != "" { - key = key + "." - } - + keyFn := e.objKeyEncoder(key) for _, ef := range encoderFields { field := value.FieldByIndex(ef.idx) - err := ef.fn(key+ef.tag.name, field, writer) + err := ef.fn(keyFn(ef.tag.name), field, writer) if err != nil { return err } @@ -405,6 +379,43 @@ func (e *encoder) newReaderTypeEncoder() encoderFunc { } } +func (e encoder) arrayKeyEncoder() func(string, int) string { + var keyFn func(string, int) string + switch e.arrayFmt { + case "comma", "repeat": + keyFn = func(k string, _ int) string { return k } + case "brackets": + keyFn = func(key string, _ int) string { return key + "[]" } + case "indices:dots": + keyFn = func(k string, i int) string { + if k == "" { + return strconv.Itoa(i) + } + return k + "." + strconv.Itoa(i) + } + case "indices:brackets": + keyFn = func(k string, i int) string { + if k == "" { + return strconv.Itoa(i) + } + return k + "[" + strconv.Itoa(i) + "]" + } + } + return keyFn +} + +func (e encoder) objKeyEncoder(parent string) func(string) string { + if parent == "" { + return func(child string) string { return child } + } + switch e.arrayFmt { + case "brackets": + return func(child string) string { return parent + "[" + child + "]" } + default: + return func(child string) string { return parent + "." + child } + } +} + // Given a []byte of json (may either be an empty object or an object that already contains entries) // encode all of the entries in the map to the json byte array. func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipart.Writer) error { @@ -413,10 +424,6 @@ func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipar value reflect.Value } - if key != "" { - key = key + "." - } - pairs := []mapPair{} iter := v.MapRange() @@ -434,8 +441,9 @@ func (e *encoder) encodeMapEntries(key string, v reflect.Value, writer *multipar }) elementEncoder := e.typeEncoder(v.Type().Elem()) + keyFn := e.objKeyEncoder(key) for _, p := range pairs { - err := elementEncoder(key+string(p.key), p.value, writer) + err := elementEncoder(keyFn(p.key), p.value, writer) if err != nil { return err } diff --git a/internal/apiform/form_test.go b/internal/apiform/form_test.go index e91b9ea..5d56066 100644 --- a/internal/apiform/form_test.go +++ b/internal/apiform/form_test.go @@ -123,6 +123,18 @@ type StructUnion struct { param.APIUnion } +type MultipartMarshalerParent struct { + Middle MultipartMarshalerMiddleNext `form:"middle"` +} + +type MultipartMarshalerMiddleNext struct { + MiddleNext MultipartMarshalerMiddle `form:"middleNext"` +} + +type MultipartMarshalerMiddle struct { + Child int `form:"child"` +} + var tests = map[string]struct { buf string val any @@ -366,6 +378,19 @@ true }, }, }, + "recursive_struct,brackets": { + `--xxx +Content-Disposition: form-data; name="child[name]" + +Alex +--xxx +Content-Disposition: form-data; name="name" + +Robert +--xxx-- +`, + Recursive{Name: "Robert", Child: &Recursive{Name: "Alex"}}, + }, "recursive_struct": { `--xxx @@ -529,6 +554,30 @@ Content-Disposition: form-data; name="union" Union: UnionTime(time.Date(2010, 05, 23, 0, 0, 0, 0, time.UTC)), }, }, + "deeply-nested-struct,brackets": { + `--xxx +Content-Disposition: form-data; name="middle[middleNext][child]" + +10 +--xxx-- +`, + MultipartMarshalerParent{ + Middle: MultipartMarshalerMiddleNext{ + MiddleNext: MultipartMarshalerMiddle{ + Child: 10, + }, + }, + }, + }, + "deeply-nested-map,brackets": { + `--xxx +Content-Disposition: form-data; name="middle[middleNext][child]" + +10 +--xxx-- +`, + map[string]any{"middle": map[string]any{"middleNext": map[string]any{"child": 10}}}, + }, } func TestEncode(t *testing.T) { @@ -553,7 +602,7 @@ func TestEncode(t *testing.T) { } raw := buf.Bytes() if string(raw) != strings.ReplaceAll(test.buf, "\n", "\r\n") { - t.Errorf("expected %+#v to serialize to '%s' but got '%s'", test.val, test.buf, string(raw)) + t.Errorf("expected %+#v to serialize to '%s' but got '%s' (with format %s)", test.val, test.buf, string(raw), arrayFmt) } }) } diff --git a/internal/version.go b/internal/version.go index e482b6b..834f803 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.22.0" // x-release-please-version +const PackageVersion = "0.23.0" // x-release-please-version