From 5ab95904f1a1d1a0fb4a950d1f046b8d603b78c6 Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Mon, 18 Nov 2024 17:22:45 -0500 Subject: [PATCH] feat(go): Add inlineFileProperties configuration (#5202) --- generators/go/cmd/fern-go-fiber/main.go | 1 + generators/go/cmd/fern-go-model/main.go | 1 + generators/go/cmd/fern-go-sdk/main.go | 1 + generators/go/internal/cmd/cmd.go | 3 + generators/go/internal/generator/config.go | 3 + generators/go/internal/generator/fiber.go | 2 +- .../go/internal/generator/file_writer.go | 5 + generators/go/internal/generator/generator.go | 62 ++- generators/go/internal/generator/sdk.go | 116 ++++-- .../generator/sdk/internal/multipart.go | 25 +- .../generator/sdk/internal/multipart_test.go | 19 +- .../sdk/basic/fixtures/internal/multipart.go | 195 --------- .../basic/fixtures/internal/multipart_test.go | 251 ----------- .../sdk/bearer/fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../sdk/bytes/fixtures/internal/multipart.go | 195 --------- .../bytes/fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../sdk/cycle/fixtures/internal/multipart.go | 195 --------- .../cycle/fixtures/internal/multipart_test.go | 251 ----------- .../default/fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../sdk/docs/fixtures/internal/multipart.go | 195 --------- .../docs/fixtures/internal/multipart_test.go | 251 ----------- .../download/fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../sdk/empty/fixtures/internal/multipart.go | 195 --------- .../empty/fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../sdk/error/fixtures/internal/multipart.go | 195 --------- .../error/fixtures/internal/multipart_test.go | 251 ----------- .../headers/fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../mergent/fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../packages/fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../fixtures/internal/multipart.go | 195 --------- .../fixtures/internal/multipart_test.go | 251 ----------- .../sdk/root/fixtures/internal/multipart.go | 195 --------- .../root/fixtures/internal/multipart_test.go | 251 ----------- .../sdk/upload/fixtures/file/client.go | 6 +- .../sdk/upload/fixtures/internal/multipart.go | 25 +- .../fixtures/internal/multipart_test.go | 19 +- generators/go/sdk/versions.yml | 15 + seed/go-sdk/alias/internal/multipart.go | 195 --------- seed/go-sdk/alias/internal/multipart_test.go | 251 ----------- seed/go-sdk/any-auth/internal/multipart.go | 195 --------- .../any-auth/internal/multipart_test.go | 251 ----------- .../api-wide-base-path/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/audiences/internal/multipart.go | 195 --------- .../audiences/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/basic-auth/internal/multipart.go | 195 --------- .../basic-auth/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/bytes/internal/multipart.go | 195 --------- seed/go-sdk/bytes/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../circular-references/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/custom-auth/internal/multipart.go | 195 --------- .../custom-auth/internal/multipart_test.go | 251 ----------- seed/go-sdk/enum/internal/multipart.go | 195 --------- seed/go-sdk/enum/internal/multipart_test.go | 251 ----------- .../error-property/internal/multipart.go | 195 --------- .../error-property/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../no-custom-config/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/extends/internal/multipart.go | 195 --------- .../go-sdk/extends/internal/multipart_test.go | 251 ----------- .../extra-properties/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../file-download/internal/multipart.go | 195 --------- .../file-download/internal/multipart_test.go | 251 ----------- .../.github/workflows/ci.yml | 0 .../.mock/definition/api.yml | 0 .../.mock/definition/service.yml | 0 .../.mock/fern.config.json | 0 .../.mock/generators.yml | 0 .../inline-file-properties/client/client.go | 34 ++ .../client/client_test.go | 45 ++ .../core/api_error.go | 0 .../{ => inline-file-properties}/core/http.go | 0 .../core/request_option.go | 108 +++++ .../inline-file-properties/file_param.go | 41 ++ .../file-upload/inline-file-properties/go.mod | 9 + .../{ => inline-file-properties}/go.sum | 0 .../inline-file-properties/internal/caller.go | 242 +++++++++++ .../internal/caller_test.go | 391 ++++++++++++++++++ .../internal/extra_properties.go | 0 .../internal/extra_properties_test.go | 0 .../internal/http.go | 0 .../internal/multipart.go | 25 +- .../internal/multipart_test.go | 19 +- .../internal/query.go | 0 .../internal/query_test.go | 0 .../internal/retrier.go | 0 .../internal/retrier_test.go | 211 ++++++++++ .../internal/stringer.go | 0 .../internal/time.go | 0 .../option/request_option.go | 64 +++ .../inline-file-properties/pointer.go | 132 ++++++ .../inline-file-properties/service.go | 116 ++++++ .../inline-file-properties/service/client.go | 280 +++++++++++++ .../snippet-templates.json | 0 .../{ => inline-file-properties}/snippet.json | 0 seed/go-sdk/file-upload/internal/multipart.go | 195 --------- .../file-upload/internal/multipart_test.go | 251 ----------- .../no-custom-config/.github/workflows/ci.yml | 27 ++ .../no-custom-config/.mock/definition/api.yml | 1 + .../.mock/definition/service.yml | 78 ++++ .../no-custom-config/.mock/fern.config.json | 1 + .../no-custom-config/.mock/generators.yml | 1 + .../{ => no-custom-config}/client/client.go | 0 .../client/client_test.go | 0 .../no-custom-config/core/api_error.go | 42 ++ .../file-upload/no-custom-config/core/http.go | 8 + .../core/request_option.go | 0 .../{ => no-custom-config}/file_param.go | 0 .../file-upload/{ => no-custom-config}/go.mod | 0 .../file-upload/no-custom-config/go.sum | 14 + .../{ => no-custom-config}/internal/caller.go | 0 .../internal/caller_test.go | 0 .../internal/extra_properties.go | 141 +++++++ .../internal/extra_properties_test.go | 228 ++++++++++ .../no-custom-config/internal/http.go | 37 ++ .../no-custom-config}/internal/multipart.go | 25 +- .../internal/multipart_test.go | 19 +- .../no-custom-config/internal/query.go | 231 +++++++++++ .../no-custom-config/internal/query_test.go | 187 +++++++++ .../no-custom-config/internal/retrier.go | 165 ++++++++ .../internal/retrier_test.go | 0 .../no-custom-config/internal/stringer.go | 13 + .../no-custom-config/internal/time.go | 137 ++++++ .../option/request_option.go | 0 .../{ => no-custom-config}/pointer.go | 0 .../{ => no-custom-config}/service.go | 0 .../{ => no-custom-config}/service/client.go | 8 +- .../no-custom-config/snippet-templates.json} | 0 .../file-upload/no-custom-config/snippet.json | 0 seed/go-sdk/folders/internal/multipart.go | 195 --------- .../go-sdk/folders/internal/multipart_test.go | 251 ----------- .../go-content-type/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../idempotency-headers/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/imdb/internal/multipart.go | 195 --------- seed/go-sdk/imdb/internal/multipart_test.go | 251 ----------- seed/go-sdk/license/internal/multipart.go | 195 --------- .../go-sdk/license/internal/multipart_test.go | 251 ----------- seed/go-sdk/literal/internal/multipart.go | 195 --------- .../go-sdk/literal/internal/multipart_test.go | 251 ----------- seed/go-sdk/mixed-case/internal/multipart.go | 195 --------- .../mixed-case/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../multi-line-docs/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../no-environment/internal/multipart.go | 195 --------- .../no-environment/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/object/internal/multipart.go | 195 --------- seed/go-sdk/object/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/optional/internal/multipart.go | 195 --------- .../optional/internal/multipart_test.go | 251 ----------- seed/go-sdk/package-yml/internal/multipart.go | 195 --------- .../package-yml/internal/multipart_test.go | 251 ----------- seed/go-sdk/pagination/internal/multipart.go | 195 --------- .../pagination/internal/multipart_test.go | 251 ----------- seed/go-sdk/plain-text/internal/multipart.go | 195 --------- .../plain-text/internal/multipart_test.go | 251 ----------- .../query-parameters/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../response-property/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/seed.yml | 13 +- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../server-sent-events/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/simple-fhir/internal/multipart.go | 195 --------- .../simple-fhir/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/streaming/internal/multipart.go | 195 --------- .../streaming/internal/multipart_test.go | 251 ----------- .../internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/unions/internal/multipart.go | 195 --------- seed/go-sdk/unions/internal/multipart_test.go | 251 ----------- seed/go-sdk/unknown/internal/multipart.go | 195 --------- .../go-sdk/unknown/internal/multipart_test.go | 251 ----------- seed/go-sdk/validation/internal/multipart.go | 195 --------- .../validation/internal/multipart_test.go | 251 ----------- seed/go-sdk/variables/internal/multipart.go | 195 --------- .../variables/internal/multipart_test.go | 251 ----------- .../version-no-default/internal/multipart.go | 195 --------- .../internal/multipart_test.go | 251 ----------- seed/go-sdk/version/internal/multipart.go | 195 --------- .../go-sdk/version/internal/multipart_test.go | 251 ----------- seed/go-sdk/websocket/internal/multipart.go | 195 --------- .../websocket/internal/multipart_test.go | 251 ----------- 264 files changed, 3282 insertions(+), 41146 deletions(-) delete mode 100644 generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/default/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/default/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/download/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/download/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/error/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/error/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart_test.go delete mode 100644 generators/go/internal/testdata/sdk/root/fixtures/internal/multipart.go delete mode 100644 generators/go/internal/testdata/sdk/root/fixtures/internal/multipart_test.go delete mode 100644 seed/go-sdk/alias/internal/multipart.go delete mode 100644 seed/go-sdk/alias/internal/multipart_test.go delete mode 100644 seed/go-sdk/any-auth/internal/multipart.go delete mode 100644 seed/go-sdk/any-auth/internal/multipart_test.go delete mode 100644 seed/go-sdk/api-wide-base-path/internal/multipart.go delete mode 100644 seed/go-sdk/api-wide-base-path/internal/multipart_test.go delete mode 100644 seed/go-sdk/audiences/internal/multipart.go delete mode 100644 seed/go-sdk/audiences/internal/multipart_test.go delete mode 100644 seed/go-sdk/auth-environment-variables/internal/multipart.go delete mode 100644 seed/go-sdk/auth-environment-variables/internal/multipart_test.go delete mode 100644 seed/go-sdk/basic-auth-environment-variables/internal/multipart.go delete mode 100644 seed/go-sdk/basic-auth-environment-variables/internal/multipart_test.go delete mode 100644 seed/go-sdk/basic-auth/internal/multipart.go delete mode 100644 seed/go-sdk/basic-auth/internal/multipart_test.go delete mode 100644 seed/go-sdk/bearer-token-environment-variable/internal/multipart.go delete mode 100644 seed/go-sdk/bearer-token-environment-variable/internal/multipart_test.go delete mode 100644 seed/go-sdk/bytes/internal/multipart.go delete mode 100644 seed/go-sdk/bytes/internal/multipart_test.go delete mode 100644 seed/go-sdk/circular-references-advanced/internal/multipart.go delete mode 100644 seed/go-sdk/circular-references-advanced/internal/multipart_test.go delete mode 100644 seed/go-sdk/circular-references/internal/multipart.go delete mode 100644 seed/go-sdk/circular-references/internal/multipart_test.go delete mode 100644 seed/go-sdk/cross-package-type-names/internal/multipart.go delete mode 100644 seed/go-sdk/cross-package-type-names/internal/multipart_test.go delete mode 100644 seed/go-sdk/custom-auth/internal/multipart.go delete mode 100644 seed/go-sdk/custom-auth/internal/multipart_test.go delete mode 100644 seed/go-sdk/enum/internal/multipart.go delete mode 100644 seed/go-sdk/enum/internal/multipart_test.go delete mode 100644 seed/go-sdk/error-property/internal/multipart.go delete mode 100644 seed/go-sdk/error-property/internal/multipart_test.go delete mode 100644 seed/go-sdk/examples/always-send-required-properties/internal/multipart.go delete mode 100644 seed/go-sdk/examples/always-send-required-properties/internal/multipart_test.go delete mode 100644 seed/go-sdk/examples/exported-client-name/internal/multipart.go delete mode 100644 seed/go-sdk/examples/exported-client-name/internal/multipart_test.go delete mode 100644 seed/go-sdk/examples/no-custom-config/internal/multipart.go delete mode 100644 seed/go-sdk/examples/no-custom-config/internal/multipart_test.go delete mode 100644 seed/go-sdk/extends/internal/multipart.go delete mode 100644 seed/go-sdk/extends/internal/multipart_test.go delete mode 100644 seed/go-sdk/extra-properties/internal/multipart.go delete mode 100644 seed/go-sdk/extra-properties/internal/multipart_test.go delete mode 100644 seed/go-sdk/file-download/internal/multipart.go delete mode 100644 seed/go-sdk/file-download/internal/multipart_test.go rename seed/go-sdk/file-upload/{ => inline-file-properties}/.github/workflows/ci.yml (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/.mock/definition/api.yml (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/.mock/definition/service.yml (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/.mock/fern.config.json (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/.mock/generators.yml (100%) create mode 100644 seed/go-sdk/file-upload/inline-file-properties/client/client.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/client/client_test.go rename seed/go-sdk/file-upload/{ => inline-file-properties}/core/api_error.go (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/core/http.go (100%) create mode 100644 seed/go-sdk/file-upload/inline-file-properties/core/request_option.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/file_param.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/go.mod rename seed/go-sdk/file-upload/{ => inline-file-properties}/go.sum (100%) create mode 100644 seed/go-sdk/file-upload/inline-file-properties/internal/caller.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/internal/caller_test.go rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/extra_properties.go (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/extra_properties_test.go (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/http.go (100%) rename {generators/go/internal/testdata/sdk/auth/fixtures => seed/go-sdk/file-upload/inline-file-properties}/internal/multipart.go (88%) rename {generators/go/internal/testdata/sdk/bearer-token-name/fixtures => seed/go-sdk/file-upload/inline-file-properties}/internal/multipart_test.go (92%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/query.go (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/query_test.go (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/retrier.go (100%) create mode 100644 seed/go-sdk/file-upload/inline-file-properties/internal/retrier_test.go rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/stringer.go (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/internal/time.go (100%) create mode 100644 seed/go-sdk/file-upload/inline-file-properties/option/request_option.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/pointer.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/service.go create mode 100644 seed/go-sdk/file-upload/inline-file-properties/service/client.go rename seed/go-sdk/file-upload/{ => inline-file-properties}/snippet-templates.json (100%) rename seed/go-sdk/file-upload/{ => inline-file-properties}/snippet.json (100%) delete mode 100644 seed/go-sdk/file-upload/internal/multipart.go delete mode 100644 seed/go-sdk/file-upload/internal/multipart_test.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/.github/workflows/ci.yml create mode 100644 seed/go-sdk/file-upload/no-custom-config/.mock/definition/api.yml create mode 100644 seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml create mode 100644 seed/go-sdk/file-upload/no-custom-config/.mock/fern.config.json create mode 100644 seed/go-sdk/file-upload/no-custom-config/.mock/generators.yml rename seed/go-sdk/file-upload/{ => no-custom-config}/client/client.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/client/client_test.go (100%) create mode 100644 seed/go-sdk/file-upload/no-custom-config/core/api_error.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/core/http.go rename seed/go-sdk/file-upload/{ => no-custom-config}/core/request_option.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/file_param.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/go.mod (100%) create mode 100644 seed/go-sdk/file-upload/no-custom-config/go.sum rename seed/go-sdk/file-upload/{ => no-custom-config}/internal/caller.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/internal/caller_test.go (100%) create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/extra_properties.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/extra_properties_test.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/http.go rename {generators/go/internal/testdata/sdk/bearer-token-name/fixtures => seed/go-sdk/file-upload/no-custom-config}/internal/multipart.go (88%) rename {generators/go/internal/testdata/sdk/auth/fixtures => seed/go-sdk/file-upload/no-custom-config}/internal/multipart_test.go (92%) create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/query.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/query_test.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/retrier.go rename seed/go-sdk/file-upload/{ => no-custom-config}/internal/retrier_test.go (100%) create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/stringer.go create mode 100644 seed/go-sdk/file-upload/no-custom-config/internal/time.go rename seed/go-sdk/file-upload/{ => no-custom-config}/option/request_option.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/pointer.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/service.go (100%) rename seed/go-sdk/file-upload/{ => no-custom-config}/service/client.go (94%) rename seed/go-sdk/{trace => file-upload/no-custom-config/snippet-templates.json} (100%) create mode 100644 seed/go-sdk/file-upload/no-custom-config/snippet.json delete mode 100644 seed/go-sdk/folders/internal/multipart.go delete mode 100644 seed/go-sdk/folders/internal/multipart_test.go delete mode 100644 seed/go-sdk/go-content-type/internal/multipart.go delete mode 100644 seed/go-sdk/go-content-type/internal/multipart_test.go delete mode 100644 seed/go-sdk/idempotency-headers/internal/multipart.go delete mode 100644 seed/go-sdk/idempotency-headers/internal/multipart_test.go delete mode 100644 seed/go-sdk/imdb/internal/multipart.go delete mode 100644 seed/go-sdk/imdb/internal/multipart_test.go delete mode 100644 seed/go-sdk/license/internal/multipart.go delete mode 100644 seed/go-sdk/license/internal/multipart_test.go delete mode 100644 seed/go-sdk/literal/internal/multipart.go delete mode 100644 seed/go-sdk/literal/internal/multipart_test.go delete mode 100644 seed/go-sdk/mixed-case/internal/multipart.go delete mode 100644 seed/go-sdk/mixed-case/internal/multipart_test.go delete mode 100644 seed/go-sdk/mixed-file-directory/internal/multipart.go delete mode 100644 seed/go-sdk/mixed-file-directory/internal/multipart_test.go delete mode 100644 seed/go-sdk/multi-line-docs/internal/multipart.go delete mode 100644 seed/go-sdk/multi-line-docs/internal/multipart_test.go delete mode 100644 seed/go-sdk/multi-url-environment-no-default/internal/multipart.go delete mode 100644 seed/go-sdk/multi-url-environment-no-default/internal/multipart_test.go delete mode 100644 seed/go-sdk/multi-url-environment/internal/multipart.go delete mode 100644 seed/go-sdk/multi-url-environment/internal/multipart_test.go delete mode 100644 seed/go-sdk/no-environment/internal/multipart.go delete mode 100644 seed/go-sdk/no-environment/internal/multipart_test.go delete mode 100644 seed/go-sdk/oauth-client-credentials-default/internal/multipart.go delete mode 100644 seed/go-sdk/oauth-client-credentials-default/internal/multipart_test.go delete mode 100644 seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart.go delete mode 100644 seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart_test.go delete mode 100644 seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart.go delete mode 100644 seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart_test.go delete mode 100644 seed/go-sdk/oauth-client-credentials/internal/multipart.go delete mode 100644 seed/go-sdk/oauth-client-credentials/internal/multipart_test.go delete mode 100644 seed/go-sdk/object/internal/multipart.go delete mode 100644 seed/go-sdk/object/internal/multipart_test.go delete mode 100644 seed/go-sdk/objects-with-imports/internal/multipart.go delete mode 100644 seed/go-sdk/objects-with-imports/internal/multipart_test.go delete mode 100644 seed/go-sdk/optional/internal/multipart.go delete mode 100644 seed/go-sdk/optional/internal/multipart_test.go delete mode 100644 seed/go-sdk/package-yml/internal/multipart.go delete mode 100644 seed/go-sdk/package-yml/internal/multipart_test.go delete mode 100644 seed/go-sdk/pagination/internal/multipart.go delete mode 100644 seed/go-sdk/pagination/internal/multipart_test.go delete mode 100644 seed/go-sdk/plain-text/internal/multipart.go delete mode 100644 seed/go-sdk/plain-text/internal/multipart_test.go delete mode 100644 seed/go-sdk/query-parameters/internal/multipart.go delete mode 100644 seed/go-sdk/query-parameters/internal/multipart_test.go delete mode 100644 seed/go-sdk/response-property/internal/multipart.go delete mode 100644 seed/go-sdk/response-property/internal/multipart_test.go delete mode 100644 seed/go-sdk/server-sent-event-examples/internal/multipart.go delete mode 100644 seed/go-sdk/server-sent-event-examples/internal/multipart_test.go delete mode 100644 seed/go-sdk/server-sent-events/internal/multipart.go delete mode 100644 seed/go-sdk/server-sent-events/internal/multipart_test.go delete mode 100644 seed/go-sdk/simple-fhir/internal/multipart.go delete mode 100644 seed/go-sdk/simple-fhir/internal/multipart_test.go delete mode 100644 seed/go-sdk/single-url-environment-default/internal/multipart.go delete mode 100644 seed/go-sdk/single-url-environment-default/internal/multipart_test.go delete mode 100644 seed/go-sdk/single-url-environment-no-default/internal/multipart.go delete mode 100644 seed/go-sdk/single-url-environment-no-default/internal/multipart_test.go delete mode 100644 seed/go-sdk/streaming/internal/multipart.go delete mode 100644 seed/go-sdk/streaming/internal/multipart_test.go delete mode 100644 seed/go-sdk/undiscriminated-unions/internal/multipart.go delete mode 100644 seed/go-sdk/undiscriminated-unions/internal/multipart_test.go delete mode 100644 seed/go-sdk/unions/internal/multipart.go delete mode 100644 seed/go-sdk/unions/internal/multipart_test.go delete mode 100644 seed/go-sdk/unknown/internal/multipart.go delete mode 100644 seed/go-sdk/unknown/internal/multipart_test.go delete mode 100644 seed/go-sdk/validation/internal/multipart.go delete mode 100644 seed/go-sdk/validation/internal/multipart_test.go delete mode 100644 seed/go-sdk/variables/internal/multipart.go delete mode 100644 seed/go-sdk/variables/internal/multipart_test.go delete mode 100644 seed/go-sdk/version-no-default/internal/multipart.go delete mode 100644 seed/go-sdk/version-no-default/internal/multipart_test.go delete mode 100644 seed/go-sdk/version/internal/multipart.go delete mode 100644 seed/go-sdk/version/internal/multipart_test.go delete mode 100644 seed/go-sdk/websocket/internal/multipart.go delete mode 100644 seed/go-sdk/websocket/internal/multipart_test.go diff --git a/generators/go/cmd/fern-go-fiber/main.go b/generators/go/cmd/fern-go-fiber/main.go index 8bbdec2101a..9d2233eb95c 100644 --- a/generators/go/cmd/fern-go-fiber/main.go +++ b/generators/go/cmd/fern-go-fiber/main.go @@ -29,6 +29,7 @@ func run(config *cmd.Config, coordinator *coordinator.Client) ([]*generator.File includeReadme, config.Whitelabel, config.AlwaysSendRequiredProperties, + config.InlineFileProperties, config.Organization, config.Version, config.IrFilepath, diff --git a/generators/go/cmd/fern-go-model/main.go b/generators/go/cmd/fern-go-model/main.go index efb90e34058..1c0a05596a8 100644 --- a/generators/go/cmd/fern-go-model/main.go +++ b/generators/go/cmd/fern-go-model/main.go @@ -29,6 +29,7 @@ func run(config *cmd.Config, coordinator *coordinator.Client) ([]*generator.File includeReadme, config.Whitelabel, config.AlwaysSendRequiredProperties, + config.InlineFileProperties, config.Organization, config.Version, config.IrFilepath, diff --git a/generators/go/cmd/fern-go-sdk/main.go b/generators/go/cmd/fern-go-sdk/main.go index f85695ffbc0..886bd69742f 100644 --- a/generators/go/cmd/fern-go-sdk/main.go +++ b/generators/go/cmd/fern-go-sdk/main.go @@ -29,6 +29,7 @@ func run(config *cmd.Config, coordinator *coordinator.Client) ([]*generator.File includeReadme, config.Whitelabel, config.AlwaysSendRequiredProperties, + config.InlineFileProperties, config.Organization, config.Version, config.IrFilepath, diff --git a/generators/go/internal/cmd/cmd.go b/generators/go/internal/cmd/cmd.go index 924ae62f4cb..8b66bb2a485 100644 --- a/generators/go/internal/cmd/cmd.go +++ b/generators/go/internal/cmd/cmd.go @@ -56,6 +56,7 @@ type Config struct { IncludeLegacyClientOptions bool Whitelabel bool AlwaysSendRequiredProperties bool + InlineFileProperties bool Organization string CoordinatorURL string CoordinatorTaskID string @@ -200,6 +201,7 @@ func newConfig(configFilename string) (*Config, error) { return &Config{ DryRun: config.DryRun, + InlineFileProperties: customConfig.InlineFileProperties, IncludeLegacyClientOptions: customConfig.IncludeLegacyClientOptions, EnableExplicitNull: customConfig.EnableExplicitNull, Organization: config.Organization, @@ -255,6 +257,7 @@ func readConfig(configFilename string) (*generatorexec.GeneratorConfig, error) { type customConfig struct { EnableExplicitNull bool `json:"enableExplicitNull,omitempty"` + InlineFileProperties bool `json:"inlineFileProperties,omitempty"` IncludeLegacyClientOptions bool `json:"includeLegacyClientOptions,omitempty"` AlwaysSendRequiredProperties bool `json:"alwaysSendRequiredProperties,omitempty"` ImportPath string `json:"importPath,omitempty"` diff --git a/generators/go/internal/generator/config.go b/generators/go/internal/generator/config.go index 4770b6287bd..725c13881be 100644 --- a/generators/go/internal/generator/config.go +++ b/generators/go/internal/generator/config.go @@ -20,6 +20,7 @@ type Config struct { IncludeReadme bool Whitelabel bool AlwaysSendRequiredProperties bool + InlineFileProperties bool Organization string Version string IRFilepath string @@ -57,6 +58,7 @@ func NewConfig( includeReadme bool, whitelabel bool, alwaysSendRequiredProperties bool, + inlineFileProperties bool, organization string, version string, irFilepath string, @@ -79,6 +81,7 @@ func NewConfig( Organization: organization, Whitelabel: whitelabel, AlwaysSendRequiredProperties: alwaysSendRequiredProperties, + InlineFileProperties: inlineFileProperties, Version: version, IRFilepath: irFilepath, SnippetFilepath: snippetFilepath, diff --git a/generators/go/internal/generator/fiber.go b/generators/go/internal/generator/fiber.go index da1882e662f..f6201e5031e 100644 --- a/generators/go/internal/generator/fiber.go +++ b/generators/go/internal/generator/fiber.go @@ -70,7 +70,7 @@ func (f *fileWriter) WriteFiberRequestType(fernFilepath *ir.FernFilepath, endpoi } return nil } - requestBody, err := requestBodyToFieldDeclaration(endpoint.RequestBody, f, importPath, bodyField, includeGenericOptionals) + requestBody, err := requestBodyToFieldDeclaration(endpoint.RequestBody, f, importPath, bodyField, includeGenericOptionals, false /* inlineFileProperties */) if err != nil { return err } diff --git a/generators/go/internal/generator/file_writer.go b/generators/go/internal/generator/file_writer.go index 8175b206738..ef9b8795769 100644 --- a/generators/go/internal/generator/file_writer.go +++ b/generators/go/internal/generator/file_writer.go @@ -33,6 +33,7 @@ type fileWriter struct { baseImportPath string whitelabel bool alwaysSendRequiredProperties bool + inlineFileProperties bool unionVersion UnionVersion scope *gospec.Scope types map[ir.TypeId]*ir.TypeDeclaration @@ -49,6 +50,7 @@ func newFileWriter( baseImportPath string, whitelabel bool, alwaysSendRequiredProperties bool, + inlineFileProperties bool, unionVersion UnionVersion, types map[ir.TypeId]*ir.TypeDeclaration, errors map[ir.ErrorId]*ir.ErrorDeclaration, @@ -89,6 +91,7 @@ func newFileWriter( baseImportPath: baseImportPath, whitelabel: whitelabel, alwaysSendRequiredProperties: alwaysSendRequiredProperties, + inlineFileProperties: inlineFileProperties, unionVersion: unionVersion, scope: scope, types: types, @@ -125,6 +128,7 @@ func (f *fileWriter) File() (*File, error) { formatted, err := removeUnusedImports(f.filename, append(header.buffer.Bytes(), f.buffer.Bytes()...)) if err != nil { + fmt.Println(string(append(header.buffer.Bytes(), f.buffer.Bytes()...))) return nil, err } @@ -163,6 +167,7 @@ func (f *fileWriter) clone() *fileWriter { f.baseImportPath, f.whitelabel, f.alwaysSendRequiredProperties, + f.inlineFileProperties, f.unionVersion, f.types, f.errors, diff --git a/generators/go/internal/generator/generator.go b/generators/go/internal/generator/generator.go index aa794b7ce5a..bc94402872c 100644 --- a/generators/go/internal/generator/generator.go +++ b/generators/go/internal/generator/generator.go @@ -132,7 +132,7 @@ func (g *Generator) Generate(mode Mode) ([]*File, error) { } func (g *Generator) generateModelTypes(ir *fernir.IntermediateRepresentation, mode Mode, rootPackageName string) ([]*File, error) { - fileInfoToTypes, err := fileInfoToTypes(rootPackageName, ir.Types, ir.Services, ir.ServiceTypeReferenceInfo) + fileInfoToTypes, err := fileInfoToTypes(rootPackageName, ir.Types, ir.Services, ir.ServiceTypeReferenceInfo, g.config.InlineFileProperties) if err != nil { return nil, err } @@ -144,6 +144,7 @@ func (g *Generator) generateModelTypes(ir *fernir.IntermediateRepresentation, mo g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -167,6 +168,7 @@ func (g *Generator) generateModelTypes(ir *fernir.IntermediateRepresentation, mo typeToGenerate.Service.Headers, ir.IdempotencyHeaders, g.config.EnableExplicitNull, + g.config.InlineFileProperties, ); err != nil { return nil, err } @@ -251,6 +253,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( "", g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, nil, nil, @@ -270,6 +273,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( "", g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, nil, nil, @@ -312,6 +316,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -341,6 +346,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -364,6 +370,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -392,6 +399,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -412,6 +420,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -435,6 +444,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -457,6 +467,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -480,13 +491,15 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( files = append(files, newFileParamFile(g.coordinator, rootPackageName, generatedNames)) files = append(files, newHttpCoreFile(g.coordinator)) files = append(files, newHttpInternalFile(g.coordinator)) - files = append(files, newMultipartFile(g.coordinator)) - files = append(files, newMultipartTestFile(g.coordinator)) files = append(files, newPointerFile(g.coordinator, rootPackageName, generatedNames)) files = append(files, newQueryFile(g.coordinator)) files = append(files, newQueryTestFile(g.coordinator)) files = append(files, newRetrierFile(g.coordinator, g.config.ImportPath)) files = append(files, newRetrierTestFile(g.coordinator, g.config.ImportPath)) + if needsFileUploadHelpers(ir) { + files = append(files, newMultipartFile(g.coordinator)) + files = append(files, newMultipartTestFile(g.coordinator)) + } if ir.SdkConfig.HasStreamingEndpoints { files = append(files, newStreamFile(g.coordinator)) files = append(files, newStreamerFile(g.coordinator, g.config.ImportPath)) @@ -508,6 +521,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -661,6 +675,7 @@ func (g *Generator) generateService( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -677,6 +692,7 @@ func (g *Generator) generateService( ir.ErrorDiscriminationStrategy, originalFernFilepath, rootClientInstantiation, + g.config.InlineFileProperties, ) if err != nil { return nil, nil, err @@ -705,6 +721,7 @@ func (g *Generator) generateServiceWithoutEndpoints( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -721,6 +738,7 @@ func (g *Generator) generateServiceWithoutEndpoints( ir.ErrorDiscriminationStrategy, originalFernFilepath, rootClientInstantiation, + g.config.InlineFileProperties, ); err != nil { return nil, err } @@ -743,6 +761,7 @@ func (g *Generator) generateRootServiceWithoutEndpoints( g.config.ImportPath, g.config.Whitelabel, g.config.AlwaysSendRequiredProperties, + g.config.InlineFileProperties, g.config.UnionVersion, ir.Types, ir.Errors, @@ -759,6 +778,7 @@ func (g *Generator) generateRootServiceWithoutEndpoints( ir.ErrorDiscriminationStrategy, fernFilepath, rootClientInstantiation, + g.config.InlineFileProperties, ) if err != nil { return nil, nil, err @@ -995,6 +1015,7 @@ func newClientTestFile( baseImportPath, false, false, + false, UnionVersionUnspecified, nil, nil, @@ -1352,12 +1373,12 @@ func generatedPackagesFromIR(ir *fernir.IntermediateRepresentation) map[string]s } // shouldSkipRequestType returns true if the request type should not be generated. -func shouldSkipRequestType(irEndpoint *fernir.HttpEndpoint) bool { +func shouldSkipRequestType(irEndpoint *fernir.HttpEndpoint, inlineFileProperties bool) bool { if irEndpoint.SdkRequest == nil || irEndpoint.SdkRequest.Shape == nil || irEndpoint.SdkRequest.Shape.Wrapper == nil { // This endpoint doesn't have any in-lined request types that need to be generated. return true } - return !needsRequestParameter(irEndpoint) + return !needsRequestParameter(irEndpoint, inlineFileProperties) } // fileUploadHasBodyProperties returns true if the file upload request has at least @@ -1376,6 +1397,22 @@ func fileUploadHasBodyProperties(fileUpload *fernir.FileUploadRequest) bool { return false } +// fileUploadHasFileProperties returns true if the file upload request has at least +// one file property. +func fileUploadHasFileProperties(fileUpload *fernir.FileUploadRequest) bool { + if fileUpload == nil { + return false + } + // If this request is a file upload, there must be at least one body property + // in order for us to generate the in-lined request type. + for _, property := range fileUpload.Properties { + if property.File != nil { + return true + } + } + return false +} + func packagePathForDocs(fernFilepath *fernir.FernFilepath) []string { var packages []string for _, packageName := range fernFilepath.PackagePath { @@ -1416,11 +1453,12 @@ func fileInfoToTypes( irTypes map[fernir.TypeId]*fernir.TypeDeclaration, irServices map[fernir.ServiceId]*fernir.HttpService, irServiceTypeReferenceInfo *fernir.ServiceTypeReferenceInfo, + inlineFileProperties bool, ) (map[fileInfo][]*typeToGenerate, error) { result := make(map[fileInfo][]*typeToGenerate) for _, irService := range irServices { for _, irEndpoint := range irService.Endpoints { - if shouldSkipRequestType(irEndpoint) { + if shouldSkipRequestType(irEndpoint, inlineFileProperties) { continue } fileInfo := fileInfoForType(rootPackageName, irService.Name.FernFilepath) @@ -1677,6 +1715,18 @@ func needsPaginationHelpers(ir *fernir.IntermediateRepresentation) bool { return false } +// needsFileUploadHelpers returns true if at least endpoint specifies a file upload. +func needsFileUploadHelpers(ir *fernir.IntermediateRepresentation) bool { + for _, irService := range ir.Services { + for _, irEndpoint := range irService.Endpoints { + if irEndpoint.RequestBody != nil && irEndpoint.RequestBody.FileUpload != nil { + return true + } + } + } + return false +} + func isReservedFilename(filename string) bool { _, ok := reservedFilenames[filename] return ok diff --git a/generators/go/internal/generator/sdk.go b/generators/go/internal/generator/sdk.go index 3732847e89a..6f209eb3ed1 100644 --- a/generators/go/internal/generator/sdk.go +++ b/generators/go/internal/generator/sdk.go @@ -898,6 +898,7 @@ func (f *fileWriter) WriteClient( errorDiscriminationStrategy *ir.ErrorDiscriminationStrategy, fernFilepath *ir.FernFilepath, rootClientInstantiation *ast.AssignStmt, + inlineFileProperties bool, ) (*GeneratedClient, error) { var ( clientName = "Client" @@ -911,7 +912,7 @@ func (f *fileWriter) WriteClient( // Reformat the endpoint data into a structure that's suitable for code generation. var endpoints []*endpoint for _, irEndpoint := range irEndpoints { - endpoint, err := f.endpointFromIR(fernFilepath, irEndpoint, environmentsConfig, serviceHeaders, idempotencyHeaders, receiver) + endpoint, err := f.endpointFromIR(fernFilepath, irEndpoint, environmentsConfig, serviceHeaders, idempotencyHeaders, inlineFileProperties, receiver) if err != nil { return nil, err } @@ -1141,11 +1142,6 @@ func (f *fileWriter) WriteClient( f.P() } - // Prepare a response variable. - if endpoint.ResponseType != "" && endpoint.StreamingInfo == nil && endpoint.PaginationInfo == nil { - f.P(fmt.Sprintf(endpoint.ResponseInitializerFormat, endpoint.ResponseType)) - } - if len(endpoint.FileProperties) > 0 || len(endpoint.FileBodyProperties) > 0 { f.P("writer := internal.NewMultipartWriter()") for _, fileProperty := range endpoint.FileProperties { @@ -1153,20 +1149,13 @@ func (f *fileWriter) WriteClient( if err != nil { return nil, err } - var ( - fileVariable = filePropertyInfo.Key.Name.CamelCase.SafeName - contentTypeVariableName = filePropertyInfo.Key.Name.CamelCase.SafeName + "ContentType" - ) + fileVariable := getFileVariableName(endpoint, filePropertyInfo, inlineFileProperties) if filePropertyInfo.IsArray { // We don't care whether the file array is optional or not; the range // handles that for us. f.P("for _, f := range ", fileVariable, "{") if filePropertyInfo.ContentType != "" { - f.P(contentTypeVariableName, " := \"", filePropertyInfo.ContentType, "\"") - f.P("if contentTyped, ok := f.(internal.ContentTyped); ok {") - f.P(contentTypeVariableName, " = contentTyped.ContentType()") - f.P("}") - f.P("if err := writer.WriteFile(\"", filePropertyInfo.Key.WireValue, "\", f, internal.WithMultipartContentType(", contentTypeVariableName, ")); err != nil {") + f.P("if err := writer.WriteFile(\"", filePropertyInfo.Key.WireValue, "\", f, internal.WithDefaultContentType(\"", filePropertyInfo.ContentType, "\")); err != nil {") f.P("return ", endpoint.ErrorReturnValues) f.P("}") } else { @@ -1180,11 +1169,7 @@ func (f *fileWriter) WriteClient( f.P("if ", fileVariable, " != nil {") } if filePropertyInfo.ContentType != "" { - f.P(contentTypeVariableName, " := \"", filePropertyInfo.ContentType, "\"") - f.P("if contentTyped, ok := ", fileVariable, ".(internal.ContentTyped); ok {") - f.P(contentTypeVariableName, " = contentTyped.ContentType()") - f.P("}") - f.P("if err := writer.WriteFile(\"", filePropertyInfo.Key.WireValue, "\", ", fileVariable, ", internal.WithMultipartContentType(", contentTypeVariableName, ")); err != nil {") + f.P("if err := writer.WriteFile(\"", filePropertyInfo.Key.WireValue, "\", ", fileVariable, ", internal.WithDefaultContentType(\"", filePropertyInfo.ContentType, "\")); err != nil {") f.P("return ", endpoint.ErrorReturnValues) f.P("}") } else { @@ -1218,7 +1203,7 @@ func (f *fileWriter) WriteClient( if valueTypeFormat.IsPrimitive { f.P(`if err := writer.WriteField("`, fileBodyProperty.Name.WireValue, `", fmt.Sprintf("%v", `, field, ")); err != nil {") } else if fileBodyProperty.ContentType != nil { - f.P(`if err := writer.WriteJSON("`, fileBodyProperty.Name.WireValue, `", `, field, `, internal.WithMultipartContentType("`, *fileBodyProperty.ContentType, `")); err != nil {`) + f.P(`if err := writer.WriteJSON("`, fileBodyProperty.Name.WireValue, `", `, field, `, internal.WithDefaultContentType("`, *fileBodyProperty.ContentType, `")); err != nil {`) } else { f.P(`if err := writer.WriteJSON("`, fileBodyProperty.Name.WireValue, `", `, field, "); err != nil {") } @@ -1247,6 +1232,11 @@ func (f *fileWriter) WriteClient( f.P() } + // Prepare a response variable. + if endpoint.ResponseType != "" && endpoint.StreamingInfo == nil && endpoint.PaginationInfo == nil { + f.P(fmt.Sprintf(endpoint.ResponseInitializerFormat, endpoint.ResponseType)) + } + // Issue the request. if endpoint.StreamingInfo != nil { streamingInfo := endpoint.StreamingInfo @@ -1437,6 +1427,18 @@ func (f *fileWriter) WriteClient( ) } +func getFileVariableName( + endpoint *endpoint, + filePropertyInfo *filePropertyInfo, + inlineFileProperties bool, +) string { + if inlineFileProperties { + // The file property is part of the in-lined request type. + return endpoint.RequestParameterName + "." + filePropertyInfo.Key.Name.PascalCase.UnsafeName + } + return filePropertyInfo.Key.Name.CamelCase.SafeName +} + type streamingInfo struct { Delimiter string Prefix string @@ -1899,7 +1901,7 @@ func getEndpointParameters( ) } - if !shouldSkipRequestType(endpoint) { + if !shouldSkipRequestType(endpoint, f.inlineFileProperties) { fields = append( fields, exampleRequestBodyToFields(f, endpoint, example.Request)..., @@ -2137,6 +2139,7 @@ func (f *fileWriter) endpointFromIR( irEnvironmentsConfig *ir.EnvironmentsConfig, serviceHeaders []*ir.HttpHeader, idempotencyHeaders []*ir.HttpHeader, + inlineFileProperties bool, receiver string, ) (*endpoint, error) { importPath := fernFilepathToImportPath(f.baseImportPath, fernFilepath) @@ -2204,18 +2207,21 @@ func (f *fileWriter) endpointFromIR( if err != nil { return nil, err } - parameterName := filePropertyInfo.Key.Name.CamelCase.SafeName - parameterType := "io.Reader" - if filePropertyInfo.IsArray { - parameterType = "[]io.Reader" - } - signatureParameters = append( - signatureParameters, - &signatureParameter{ - parameter: fmt.Sprintf("%s %s", parameterName, parameterType), - }, - ) fileProperties = append(fileProperties, fileUploadProperty.File) + + if !inlineFileProperties { + parameterName := filePropertyInfo.Key.Name.CamelCase.SafeName + parameterType := "io.Reader" + if filePropertyInfo.IsArray { + parameterType = "[]io.Reader" + } + signatureParameters = append( + signatureParameters, + &signatureParameter{ + parameter: fmt.Sprintf("%s %s", parameterName, parameterType), + }, + ) + } } if fileUploadProperty.BodyProperty != nil { fileBodyProperties = append(fileBodyProperties, fileUploadProperty.BodyProperty) @@ -2234,7 +2240,7 @@ func (f *fileWriter) endpointFromIR( headers = []*ir.HttpHeader{} ) if irEndpoint.SdkRequest != nil { - if needsRequestParameter(irEndpoint) { + if needsRequestParameter(irEndpoint, inlineFileProperties) { var requestType string requestParameterName = irEndpoint.SdkRequest.RequestParameterName.CamelCase.SafeName if requestBody := irEndpoint.SdkRequest.Shape.JustRequestBody; requestBody != nil { @@ -2584,6 +2590,7 @@ func (f *fileWriter) WriteRequestType( serviceHeaders []*ir.HttpHeader, idempotencyHeaders []*ir.HttpHeader, includeGenericOptionals bool, + inlineFileProperties bool, ) error { var ( // At this point, we've already verified that the given endpoint's request @@ -2644,7 +2651,7 @@ func (f *fileWriter) WriteRequestType( } return nil } - requestBody, err := requestBodyToFieldDeclaration(endpoint.RequestBody, f, importPath, bodyField, includeGenericOptionals) + requestBody, err := requestBodyToFieldDeclaration(endpoint.RequestBody, f, importPath, bodyField, includeGenericOptionals, inlineFileProperties) if err != nil { return err } @@ -2961,6 +2968,7 @@ func requestBodyToFieldDeclaration( importPath string, bodyField string, includeGenericOptionals bool, + inlineFileProperties bool, ) (*requestBody, error) { visitor := &requestBodyVisitor{ bodyField: bodyField, @@ -2970,6 +2978,7 @@ func requestBodyToFieldDeclaration( types: writer.types, writer: writer, includeGenericOptionals: includeGenericOptionals, + inlineFileProperties: inlineFileProperties, } if err := body.Accept(visitor); err != nil { return nil, err @@ -2995,6 +3004,7 @@ type requestBodyVisitor struct { // Configurable includeGenericOptionals bool + inlineFileProperties bool } func (r *requestBodyVisitor) VisitInlinedRequestBody(inlinedRequestBody *ir.InlinedRequestBody) error { @@ -3030,18 +3040,40 @@ func (r *requestBodyVisitor) VisitReference(reference *ir.HttpRequestBodyReferen } func (r *requestBodyVisitor) VisitFileUpload(fileUpload *ir.FileUploadRequest) error { - var bodyProperties []*ir.FileUploadBodyProperty + var ( + bodyProperties []*ir.FileUploadBodyProperty + fileProperties []*ir.FileProperty + ) for _, property := range fileUpload.Properties { if bodyProperty := property.BodyProperty; bodyProperty != nil { bodyProperties = append(bodyProperties, bodyProperty) } + if r.inlineFileProperties { + // File properties are only part of the in-lined request if explicitly + // configured. + if fileProperty := property.File; fileProperty != nil { + fileProperties = append(fileProperties, fileProperty) + } + } } - if len(bodyProperties) == 0 { + if len(bodyProperties) == 0 && len(fileProperties) == 0 { // We only want to create a separate request type if the file upload request - // has any body properties that aren't the file itself. The file is specified - // as a positional parameter. + // has any properties. By default, the file parameter is not part of the + // in-lined request type. return nil } + for _, fileProperty := range fileProperties { + filePropertyInfo, err := filePropertyToInfo(fileProperty) + if err != nil { + return err + } + parameterName := filePropertyInfo.Key.Name.PascalCase.UnsafeName + parameterType := "io.Reader" + if filePropertyInfo.IsArray { + parameterType = "[]io.Reader" + } + r.writer.P(parameterName, " ", parameterType, " `json:\"-\" url:\"-\"`") + } typeVisitor := &typeVisitor{ typeName: fileUpload.Name.PascalCase.UnsafeName, baseImportPath: r.baseImportPath, @@ -3260,7 +3292,7 @@ func typeReferenceFromStreamingResponse( // needsRequestParameter returns true if the endpoint needs a request parameter in its // function signature. -func needsRequestParameter(endpoint *ir.HttpEndpoint) bool { +func needsRequestParameter(endpoint *ir.HttpEndpoint, inlineFileProperties bool) bool { if endpoint.SdkRequest == nil { return false } @@ -3271,7 +3303,9 @@ func needsRequestParameter(endpoint *ir.HttpEndpoint) bool { return true } if endpoint.RequestBody != nil { - return endpoint.RequestBody.FileUpload == nil || fileUploadHasBodyProperties(endpoint.RequestBody.FileUpload) + return endpoint.RequestBody.FileUpload == nil || + fileUploadHasBodyProperties(endpoint.RequestBody.FileUpload) || + (inlineFileProperties && fileUploadHasFileProperties(endpoint.RequestBody.FileUpload)) } return true } diff --git a/generators/go/internal/generator/sdk/internal/multipart.go b/generators/go/internal/generator/sdk/internal/multipart.go index bb58a62ef57..67a2229dc86 100644 --- a/generators/go/internal/generator/sdk/internal/multipart.go +++ b/generators/go/internal/generator/sdk/internal/multipart.go @@ -23,10 +23,14 @@ type ContentTyped interface { // WriteMultipartOption adapts the behavior of the multipart writer. type WriteMultipartOption func(*writeMultipartOptions) -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { +// WithDefaultContentType sets the default Content-Type for the part +// written to the MultipartWriter. +// +// Note that if the part is a FileParam, the file's Content-Type takes +// precedence over the value provided here. +func WithDefaultContentType(contentType string) WriteMultipartOption { return func(options *writeMultipartOptions) { - options.contentType = contentType + options.defaultContentType = contentType } } @@ -62,7 +66,7 @@ func (w *MultipartWriter) WriteFile( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) + return w.writeFile(field, file, options.defaultContentType) } // WriteField writes the given value as a form field. @@ -72,7 +76,7 @@ func (w *MultipartWriter) WriteField( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) + return w.writeField(field, value, options.defaultContentType) } // WriteJSON writes the given value as a JSON form field. @@ -109,11 +113,14 @@ func (w *MultipartWriter) writeField( func (w *MultipartWriter) writeFile( field string, file io.Reader, - contentType string, + defaultContentType string, ) error { - filename := getFilename(file) - if contentType == "" { + var ( + filename = getFilename(file) contentType = getContentType(file) + ) + if contentType == "" { + contentType = defaultContentType } part, err := w.newFormPart(field, filename, contentType) if err != nil { @@ -147,7 +154,7 @@ func (w *MultipartWriter) newFormPart( // writeMultipartOptions are options used to adapt the behavior of the multipart writer. type writeMultipartOptions struct { - contentType string + defaultContentType string } // newWriteMultipartOptions returns a new write multipart options. diff --git a/generators/go/internal/generator/sdk/internal/multipart_test.go b/generators/go/internal/generator/sdk/internal/multipart_test.go index 69bba7f90ad..07008dd8f42 100644 --- a/generators/go/internal/generator/sdk/internal/multipart_test.go +++ b/generators/go/internal/generator/sdk/internal/multipart_test.go @@ -75,7 +75,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) @@ -108,7 +108,7 @@ func TestMultipartWriter(t *testing.T) { }, }, { - desc: "override content type", + desc: "file content type takes precedence", giveField: "file", giveFile: &mockFile{ name: "test.txt", @@ -117,6 +117,15 @@ func TestMultipartWriter(t *testing.T) { }, giveContentType: "application/octet-stream", }, + { + desc: "default content type", + giveField: "file", + giveFile: &mockFile{ + name: "test.txt", + content: "hello world", + }, + giveContentType: "application/octet-stream", + }, } for _, tt := range tests { @@ -125,7 +134,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) @@ -154,9 +163,9 @@ func TestMultipartWriter(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.giveFile.content, string(content)) - expectedContentType := tt.giveContentType + expectedContentType := tt.giveFile.contentType if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType + expectedContentType = tt.giveContentType } if expectedContentType != "" { assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) diff --git a/generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/basic/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/bearer/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/bytes/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/client-options-core/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/client-options-filename/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/cycle/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/default/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/default/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/default/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/default/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/default/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/default/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/docs/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/download/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/download/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/download/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/download/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/download/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/download/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/empty/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/environments-core/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/environments/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/error-discrimination/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/error/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/error/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/error/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/error/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/error/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/error/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/headers/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/mergent/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/multi-environments/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/optional-core/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/optional-filename/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/optional-response/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/packages/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/path-and-query-params/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/path-params/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/platform-headers/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/pointer-core/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/pointer-filename/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/post-with-path-params-generics/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/post-with-path-params/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/query-params-complex/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/query-params-multiple/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/query-params/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/root/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/root/fixtures/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/generators/go/internal/testdata/sdk/root/fixtures/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/generators/go/internal/testdata/sdk/root/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/root/fixtures/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/generators/go/internal/testdata/sdk/root/fixtures/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/generators/go/internal/testdata/sdk/upload/fixtures/file/client.go b/generators/go/internal/testdata/sdk/upload/fixtures/file/client.go index dddf5346a62..b8023883b10 100644 --- a/generators/go/internal/testdata/sdk/upload/fixtures/file/client.go +++ b/generators/go/internal/testdata/sdk/upload/fixtures/file/client.go @@ -52,7 +52,6 @@ func (c *Client) Upload( headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) - var response string writer := internal.NewMultipartWriter() if err := writer.WriteFile("file", file); err != nil { return "", err @@ -68,6 +67,7 @@ func (c *Client) Upload( } headers.Set("Content-Type", writer.ContentType()) + var response string if err := c.caller.Call( ctx, &internal.CallParams{ @@ -105,7 +105,6 @@ func (c *Client) UploadSimple( headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) - var response string writer := internal.NewMultipartWriter() if err := writer.WriteFile("file", file); err != nil { return "", err @@ -115,6 +114,7 @@ func (c *Client) UploadSimple( } headers.Set("Content-Type", writer.ContentType()) + var response string if err := c.caller.Call( ctx, &internal.CallParams{ @@ -154,7 +154,6 @@ func (c *Client) UploadMultiple( headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) - var response string writer := internal.NewMultipartWriter() if err := writer.WriteFile("file", file); err != nil { return "", err @@ -172,6 +171,7 @@ func (c *Client) UploadMultiple( } headers.Set("Content-Type", writer.ContentType()) + var response string if err := c.caller.Call( ctx, &internal.CallParams{ diff --git a/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart.go b/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart.go index bb58a62ef57..67a2229dc86 100644 --- a/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart.go +++ b/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart.go @@ -23,10 +23,14 @@ type ContentTyped interface { // WriteMultipartOption adapts the behavior of the multipart writer. type WriteMultipartOption func(*writeMultipartOptions) -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { +// WithDefaultContentType sets the default Content-Type for the part +// written to the MultipartWriter. +// +// Note that if the part is a FileParam, the file's Content-Type takes +// precedence over the value provided here. +func WithDefaultContentType(contentType string) WriteMultipartOption { return func(options *writeMultipartOptions) { - options.contentType = contentType + options.defaultContentType = contentType } } @@ -62,7 +66,7 @@ func (w *MultipartWriter) WriteFile( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) + return w.writeFile(field, file, options.defaultContentType) } // WriteField writes the given value as a form field. @@ -72,7 +76,7 @@ func (w *MultipartWriter) WriteField( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) + return w.writeField(field, value, options.defaultContentType) } // WriteJSON writes the given value as a JSON form field. @@ -109,11 +113,14 @@ func (w *MultipartWriter) writeField( func (w *MultipartWriter) writeFile( field string, file io.Reader, - contentType string, + defaultContentType string, ) error { - filename := getFilename(file) - if contentType == "" { + var ( + filename = getFilename(file) contentType = getContentType(file) + ) + if contentType == "" { + contentType = defaultContentType } part, err := w.newFormPart(field, filename, contentType) if err != nil { @@ -147,7 +154,7 @@ func (w *MultipartWriter) newFormPart( // writeMultipartOptions are options used to adapt the behavior of the multipart writer. type writeMultipartOptions struct { - contentType string + defaultContentType string } // newWriteMultipartOptions returns a new write multipart options. diff --git a/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart_test.go b/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart_test.go index 69bba7f90ad..07008dd8f42 100644 --- a/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart_test.go +++ b/generators/go/internal/testdata/sdk/upload/fixtures/internal/multipart_test.go @@ -75,7 +75,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) @@ -108,7 +108,7 @@ func TestMultipartWriter(t *testing.T) { }, }, { - desc: "override content type", + desc: "file content type takes precedence", giveField: "file", giveFile: &mockFile{ name: "test.txt", @@ -117,6 +117,15 @@ func TestMultipartWriter(t *testing.T) { }, giveContentType: "application/octet-stream", }, + { + desc: "default content type", + giveField: "file", + giveFile: &mockFile{ + name: "test.txt", + content: "hello world", + }, + giveContentType: "application/octet-stream", + }, } for _, tt := range tests { @@ -125,7 +134,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) @@ -154,9 +163,9 @@ func TestMultipartWriter(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.giveFile.content, string(content)) - expectedContentType := tt.giveContentType + expectedContentType := tt.giveFile.contentType if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType + expectedContentType = tt.giveContentType } if expectedContentType != "" { assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) diff --git a/generators/go/sdk/versions.yml b/generators/go/sdk/versions.yml index 922bf5ff111..d7a9a86cb03 100644 --- a/generators/go/sdk/versions.yml +++ b/generators/go/sdk/versions.yml @@ -1,3 +1,18 @@ +- version: 0.32.0 + changelogEntry: + - type: feat + summary: >- + Add support for the `inlineFileProperties` configuration option, which generates file + properties in the generated request type instead of as separate positional parameters. + - type: fix + summary: >- + Fixes an issue where the new `core.MultipartWriter` was generated for SDKs that didn't + define any file upload endpoints. + - type: internal + summary: >- + Simplify the generated code from the new `core.MultipartWriter` introduced in 0.29.0 + by refactoring `internal.WithMultipartContentType` as `internal.WithDefaultContentType`. + irVersion: 53 - version: 0.31.3 changelogEntry: - type: fix diff --git a/seed/go-sdk/alias/internal/multipart.go b/seed/go-sdk/alias/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/alias/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/alias/internal/multipart_test.go b/seed/go-sdk/alias/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/alias/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/any-auth/internal/multipart.go b/seed/go-sdk/any-auth/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/any-auth/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/any-auth/internal/multipart_test.go b/seed/go-sdk/any-auth/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/any-auth/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/api-wide-base-path/internal/multipart.go b/seed/go-sdk/api-wide-base-path/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/api-wide-base-path/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/api-wide-base-path/internal/multipart_test.go b/seed/go-sdk/api-wide-base-path/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/api-wide-base-path/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/audiences/internal/multipart.go b/seed/go-sdk/audiences/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/audiences/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/audiences/internal/multipart_test.go b/seed/go-sdk/audiences/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/audiences/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/auth-environment-variables/internal/multipart.go b/seed/go-sdk/auth-environment-variables/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/auth-environment-variables/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/auth-environment-variables/internal/multipart_test.go b/seed/go-sdk/auth-environment-variables/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/auth-environment-variables/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/basic-auth-environment-variables/internal/multipart.go b/seed/go-sdk/basic-auth-environment-variables/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/basic-auth-environment-variables/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/basic-auth-environment-variables/internal/multipart_test.go b/seed/go-sdk/basic-auth-environment-variables/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/basic-auth-environment-variables/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/basic-auth/internal/multipart.go b/seed/go-sdk/basic-auth/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/basic-auth/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/basic-auth/internal/multipart_test.go b/seed/go-sdk/basic-auth/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/basic-auth/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/bearer-token-environment-variable/internal/multipart.go b/seed/go-sdk/bearer-token-environment-variable/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/bearer-token-environment-variable/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/bearer-token-environment-variable/internal/multipart_test.go b/seed/go-sdk/bearer-token-environment-variable/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/bearer-token-environment-variable/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/bytes/internal/multipart.go b/seed/go-sdk/bytes/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/bytes/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/bytes/internal/multipart_test.go b/seed/go-sdk/bytes/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/bytes/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/circular-references-advanced/internal/multipart.go b/seed/go-sdk/circular-references-advanced/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/circular-references-advanced/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/circular-references-advanced/internal/multipart_test.go b/seed/go-sdk/circular-references-advanced/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/circular-references-advanced/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/circular-references/internal/multipart.go b/seed/go-sdk/circular-references/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/circular-references/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/circular-references/internal/multipart_test.go b/seed/go-sdk/circular-references/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/circular-references/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/cross-package-type-names/internal/multipart.go b/seed/go-sdk/cross-package-type-names/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/cross-package-type-names/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/cross-package-type-names/internal/multipart_test.go b/seed/go-sdk/cross-package-type-names/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/cross-package-type-names/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/custom-auth/internal/multipart.go b/seed/go-sdk/custom-auth/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/custom-auth/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/custom-auth/internal/multipart_test.go b/seed/go-sdk/custom-auth/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/custom-auth/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/enum/internal/multipart.go b/seed/go-sdk/enum/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/enum/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/enum/internal/multipart_test.go b/seed/go-sdk/enum/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/enum/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/error-property/internal/multipart.go b/seed/go-sdk/error-property/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/error-property/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/error-property/internal/multipart_test.go b/seed/go-sdk/error-property/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/error-property/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/examples/always-send-required-properties/internal/multipart.go b/seed/go-sdk/examples/always-send-required-properties/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/examples/always-send-required-properties/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/examples/always-send-required-properties/internal/multipart_test.go b/seed/go-sdk/examples/always-send-required-properties/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/examples/always-send-required-properties/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/examples/exported-client-name/internal/multipart.go b/seed/go-sdk/examples/exported-client-name/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/examples/exported-client-name/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/examples/exported-client-name/internal/multipart_test.go b/seed/go-sdk/examples/exported-client-name/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/examples/exported-client-name/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/examples/no-custom-config/internal/multipart.go b/seed/go-sdk/examples/no-custom-config/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/examples/no-custom-config/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/examples/no-custom-config/internal/multipart_test.go b/seed/go-sdk/examples/no-custom-config/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/examples/no-custom-config/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/extends/internal/multipart.go b/seed/go-sdk/extends/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/extends/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/extends/internal/multipart_test.go b/seed/go-sdk/extends/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/extends/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/extra-properties/internal/multipart.go b/seed/go-sdk/extra-properties/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/extra-properties/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/extra-properties/internal/multipart_test.go b/seed/go-sdk/extra-properties/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/extra-properties/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/file-download/internal/multipart.go b/seed/go-sdk/file-download/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/file-download/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/file-download/internal/multipart_test.go b/seed/go-sdk/file-download/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/file-download/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/file-upload/.github/workflows/ci.yml b/seed/go-sdk/file-upload/inline-file-properties/.github/workflows/ci.yml similarity index 100% rename from seed/go-sdk/file-upload/.github/workflows/ci.yml rename to seed/go-sdk/file-upload/inline-file-properties/.github/workflows/ci.yml diff --git a/seed/go-sdk/file-upload/.mock/definition/api.yml b/seed/go-sdk/file-upload/inline-file-properties/.mock/definition/api.yml similarity index 100% rename from seed/go-sdk/file-upload/.mock/definition/api.yml rename to seed/go-sdk/file-upload/inline-file-properties/.mock/definition/api.yml diff --git a/seed/go-sdk/file-upload/.mock/definition/service.yml b/seed/go-sdk/file-upload/inline-file-properties/.mock/definition/service.yml similarity index 100% rename from seed/go-sdk/file-upload/.mock/definition/service.yml rename to seed/go-sdk/file-upload/inline-file-properties/.mock/definition/service.yml diff --git a/seed/go-sdk/file-upload/.mock/fern.config.json b/seed/go-sdk/file-upload/inline-file-properties/.mock/fern.config.json similarity index 100% rename from seed/go-sdk/file-upload/.mock/fern.config.json rename to seed/go-sdk/file-upload/inline-file-properties/.mock/fern.config.json diff --git a/seed/go-sdk/file-upload/.mock/generators.yml b/seed/go-sdk/file-upload/inline-file-properties/.mock/generators.yml similarity index 100% rename from seed/go-sdk/file-upload/.mock/generators.yml rename to seed/go-sdk/file-upload/inline-file-properties/.mock/generators.yml diff --git a/seed/go-sdk/file-upload/inline-file-properties/client/client.go b/seed/go-sdk/file-upload/inline-file-properties/client/client.go new file mode 100644 index 00000000000..cf856523c1e --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/client/client.go @@ -0,0 +1,34 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + core "github.com/fern-api/file-upload-go/core" + internal "github.com/fern-api/file-upload-go/internal" + option "github.com/fern-api/file-upload-go/option" + service "github.com/fern-api/file-upload-go/service" + http "net/http" +) + +type Client struct { + baseURL string + caller *internal.Caller + header http.Header + + Service *service.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Service: service.NewClient(opts...), + } +} diff --git a/seed/go-sdk/file-upload/inline-file-properties/client/client_test.go b/seed/go-sdk/file-upload/inline-file-properties/client/client_test.go new file mode 100644 index 00000000000..f420f9f1f6d --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/client/client_test.go @@ -0,0 +1,45 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + option "github.com/fern-api/file-upload-go/option" + assert "github.com/stretchr/testify/assert" + http "net/http" + testing "testing" + time "time" +) + +func TestNewClient(t *testing.T) { + t.Run("default", func(t *testing.T) { + c := NewClient() + assert.Empty(t, c.baseURL) + }) + + t.Run("base url", func(t *testing.T) { + c := NewClient( + option.WithBaseURL("test.co"), + ) + assert.Equal(t, "test.co", c.baseURL) + }) + + t.Run("http client", func(t *testing.T) { + httpClient := &http.Client{ + Timeout: 5 * time.Second, + } + c := NewClient( + option.WithHTTPClient(httpClient), + ) + assert.Empty(t, c.baseURL) + }) + + t.Run("http header", func(t *testing.T) { + header := make(http.Header) + header.Set("X-API-Tenancy", "test") + c := NewClient( + option.WithHTTPHeader(header), + ) + assert.Empty(t, c.baseURL) + assert.Equal(t, "test", c.header.Get("X-API-Tenancy")) + }) +} diff --git a/seed/go-sdk/file-upload/core/api_error.go b/seed/go-sdk/file-upload/inline-file-properties/core/api_error.go similarity index 100% rename from seed/go-sdk/file-upload/core/api_error.go rename to seed/go-sdk/file-upload/inline-file-properties/core/api_error.go diff --git a/seed/go-sdk/file-upload/core/http.go b/seed/go-sdk/file-upload/inline-file-properties/core/http.go similarity index 100% rename from seed/go-sdk/file-upload/core/http.go rename to seed/go-sdk/file-upload/inline-file-properties/core/http.go diff --git a/seed/go-sdk/file-upload/inline-file-properties/core/request_option.go b/seed/go-sdk/file-upload/inline-file-properties/core/request_option.go new file mode 100644 index 00000000000..a82f9442526 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/core/request_option.go @@ -0,0 +1,108 @@ +// This file was auto-generated by Fern from our API Definition. + +package core + +import ( + http "net/http" + url "net/url" +) + +// RequestOption adapts the behavior of the client or an individual request. +type RequestOption interface { + applyRequestOptions(*RequestOptions) +} + +// RequestOptions defines all of the possible request options. +// +// This type is primarily used by the generated code and is not meant +// to be used directly; use the option package instead. +type RequestOptions struct { + BaseURL string + HTTPClient HTTPClient + HTTPHeader http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + MaxAttempts uint +} + +// NewRequestOptions returns a new *RequestOptions value. +// +// This function is primarily used by the generated code and is not meant +// to be used directly; use RequestOption instead. +func NewRequestOptions(opts ...RequestOption) *RequestOptions { + options := &RequestOptions{ + HTTPHeader: make(http.Header), + BodyProperties: make(map[string]interface{}), + QueryParameters: make(url.Values), + } + for _, opt := range opts { + opt.applyRequestOptions(options) + } + return options +} + +// ToHeader maps the configured request options into a http.Header used +// for the request(s). +func (r *RequestOptions) ToHeader() http.Header { return r.cloneHeader() } + +func (r *RequestOptions) cloneHeader() http.Header { + headers := r.HTTPHeader.Clone() + headers.Set("X-Fern-Language", "Go") + headers.Set("X-Fern-SDK-Name", "github.com/fern-api/file-upload-go") + headers.Set("X-Fern-SDK-Version", "0.0.1") + return headers +} + +// BaseURLOption implements the RequestOption interface. +type BaseURLOption struct { + BaseURL string +} + +func (b *BaseURLOption) applyRequestOptions(opts *RequestOptions) { + opts.BaseURL = b.BaseURL +} + +// HTTPClientOption implements the RequestOption interface. +type HTTPClientOption struct { + HTTPClient HTTPClient +} + +func (h *HTTPClientOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPClient = h.HTTPClient +} + +// HTTPHeaderOption implements the RequestOption interface. +type HTTPHeaderOption struct { + HTTPHeader http.Header +} + +func (h *HTTPHeaderOption) applyRequestOptions(opts *RequestOptions) { + opts.HTTPHeader = h.HTTPHeader +} + +// BodyPropertiesOption implements the RequestOption interface. +type BodyPropertiesOption struct { + BodyProperties map[string]interface{} +} + +func (b *BodyPropertiesOption) applyRequestOptions(opts *RequestOptions) { + opts.BodyProperties = b.BodyProperties +} + +// QueryParametersOption implements the RequestOption interface. +type QueryParametersOption struct { + QueryParameters url.Values +} + +func (q *QueryParametersOption) applyRequestOptions(opts *RequestOptions) { + opts.QueryParameters = q.QueryParameters +} + +// MaxAttemptsOption implements the RequestOption interface. +type MaxAttemptsOption struct { + MaxAttempts uint +} + +func (m *MaxAttemptsOption) applyRequestOptions(opts *RequestOptions) { + opts.MaxAttempts = m.MaxAttempts +} diff --git a/seed/go-sdk/file-upload/inline-file-properties/file_param.go b/seed/go-sdk/file-upload/inline-file-properties/file_param.go new file mode 100644 index 00000000000..85a210d9292 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/file_param.go @@ -0,0 +1,41 @@ +package upload + +import ( + "io" +) + +// FileParam is a file type suitable for multipart/form-data uploads. +type FileParam struct { + io.Reader + filename string + contentType string +} + +// FileParamOption adapts the behavior of the FileParam. No options are +// implemented yet, but this interface allows for future extensibility. +type FileParamOption interface { + apply() +} + +// NewFileParam returns a *FileParam type suitable for multipart/form-data uploads. All file +// upload endpoints accept a simple io.Reader, which is usually created by opening a file +// via os.Open. +// +// However, some endpoints require additional metadata about the file such as a specific +// Content-Type or custom filename. FileParam makes it easier to create the correct type +// signature for these endpoints. +func NewFileParam( + reader io.Reader, + filename string, + contentType string, + opts ...FileParamOption, +) *FileParam { + return &FileParam{ + Reader: reader, + filename: filename, + contentType: contentType, + } +} + +func (f *FileParam) Name() string { return f.filename } +func (f *FileParam) ContentType() string { return f.contentType } diff --git a/seed/go-sdk/file-upload/inline-file-properties/go.mod b/seed/go-sdk/file-upload/inline-file-properties/go.mod new file mode 100644 index 00000000000..b19e7f9e95b --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/go.mod @@ -0,0 +1,9 @@ +module github.com/fern-api/file-upload-go + +go 1.13 + +require ( + github.com/google/uuid v1.4.0 + github.com/stretchr/testify v1.7.0 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/seed/go-sdk/file-upload/go.sum b/seed/go-sdk/file-upload/inline-file-properties/go.sum similarity index 100% rename from seed/go-sdk/file-upload/go.sum rename to seed/go-sdk/file-upload/inline-file-properties/go.sum diff --git a/seed/go-sdk/file-upload/inline-file-properties/internal/caller.go b/seed/go-sdk/file-upload/inline-file-properties/internal/caller.go new file mode 100644 index 00000000000..eb275dded26 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/internal/caller.go @@ -0,0 +1,242 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "reflect" + "strings" + + "github.com/fern-api/file-upload-go/core" +) + +const ( + // contentType specifies the JSON Content-Type header value. + contentType = "application/json" + contentTypeHeader = "Content-Type" +) + +// ErrorDecoder decodes *http.Response errors and returns a +// typed API error (e.g. *core.APIError). +type ErrorDecoder func(statusCode int, body io.Reader) error + +// Caller calls APIs and deserializes their response, if any. +type Caller struct { + client core.HTTPClient + retrier *Retrier +} + +// CallerParams represents the parameters used to constrcut a new *Caller. +type CallerParams struct { + Client core.HTTPClient + MaxAttempts uint +} + +// NewCaller returns a new *Caller backed by the given parameters. +func NewCaller(params *CallerParams) *Caller { + var httpClient core.HTTPClient = http.DefaultClient + if params.Client != nil { + httpClient = params.Client + } + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + return &Caller{ + client: httpClient, + retrier: NewRetrier(retryOptions...), + } +} + +// CallParams represents the parameters used to issue an API call. +type CallParams struct { + URL string + Method string + MaxAttempts uint + Headers http.Header + BodyProperties map[string]interface{} + QueryParameters url.Values + Client core.HTTPClient + Request interface{} + Response interface{} + ResponseIsOptional bool + ErrorDecoder ErrorDecoder +} + +// Call issues an API call according to the given call parameters. +func (c *Caller) Call(ctx context.Context, params *CallParams) error { + url := buildURL(params.URL, params.QueryParameters) + req, err := newRequest( + ctx, + url, + params.Method, + params.Headers, + params.Request, + params.BodyProperties, + ) + if err != nil { + return err + } + + // If the call has been cancelled, don't issue the request. + if err := ctx.Err(); err != nil { + return err + } + + client := c.client + if params.Client != nil { + // Use the HTTP client scoped to the request. + client = params.Client + } + + var retryOptions []RetryOption + if params.MaxAttempts > 0 { + retryOptions = append(retryOptions, WithMaxAttempts(params.MaxAttempts)) + } + + resp, err := c.retrier.Run( + client.Do, + req, + params.ErrorDecoder, + retryOptions..., + ) + if err != nil { + return err + } + + // Close the response body after we're done. + defer resp.Body.Close() + + // Check if the call was cancelled before we return the error + // associated with the call and/or unmarshal the response data. + if err := ctx.Err(); err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return decodeError(resp, params.ErrorDecoder) + } + + // Mutate the response parameter in-place. + if params.Response != nil { + if writer, ok := params.Response.(io.Writer); ok { + _, err = io.Copy(writer, resp.Body) + } else { + err = json.NewDecoder(resp.Body).Decode(params.Response) + } + if err != nil { + if err == io.EOF { + if params.ResponseIsOptional { + // The response is optional, so we should ignore the + // io.EOF error + return nil + } + return fmt.Errorf("expected a %T response, but the server responded with nothing", params.Response) + } + return err + } + } + + return nil +} + +// buildURL constructs the final URL by appending the given query parameters (if any). +func buildURL( + url string, + queryParameters url.Values, +) string { + if len(queryParameters) == 0 { + return url + } + if strings.ContainsRune(url, '?') { + url += "&" + } else { + url += "?" + } + url += queryParameters.Encode() + return url +} + +// newRequest returns a new *http.Request with all of the fields +// required to issue the call. +func newRequest( + ctx context.Context, + url string, + method string, + endpointHeaders http.Header, + request interface{}, + bodyProperties map[string]interface{}, +) (*http.Request, error) { + requestBody, err := newRequestBody(request, bodyProperties) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, method, url, requestBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + req.Header.Set(contentTypeHeader, contentType) + for name, values := range endpointHeaders { + req.Header[name] = values + } + return req, nil +} + +// newRequestBody returns a new io.Reader that represents the HTTP request body. +func newRequestBody(request interface{}, bodyProperties map[string]interface{}) (io.Reader, error) { + if isNil(request) { + if len(bodyProperties) == 0 { + return nil, nil + } + requestBytes, err := json.Marshal(bodyProperties) + if err != nil { + return nil, err + } + return bytes.NewReader(requestBytes), nil + } + if body, ok := request.(io.Reader); ok { + return body, nil + } + requestBytes, err := MarshalJSONWithExtraProperties(request, bodyProperties) + if err != nil { + return nil, err + } + return bytes.NewReader(requestBytes), nil +} + +// decodeError decodes the error from the given HTTP response. Note that +// it's the caller's responsibility to close the response body. +func decodeError(response *http.Response, errorDecoder ErrorDecoder) error { + if errorDecoder != nil { + // This endpoint has custom errors, so we'll + // attempt to unmarshal the error into a structured + // type based on the status code. + return errorDecoder(response.StatusCode, response.Body) + } + // This endpoint doesn't have any custom error + // types, so we just read the body as-is, and + // put it into a normal error. + bytes, err := io.ReadAll(response.Body) + if err != nil && err != io.EOF { + return err + } + if err == io.EOF { + // The error didn't have a response body, + // so all we can do is return an error + // with the status code. + return core.NewAPIError(response.StatusCode, nil) + } + return core.NewAPIError(response.StatusCode, errors.New(string(bytes))) +} + +// isNil is used to determine if the request value is equal to nil (i.e. an interface +// value that holds a nil concrete value is itself non-nil). +func isNil(value interface{}) bool { + return value == nil || reflect.ValueOf(value).IsNil() +} diff --git a/seed/go-sdk/file-upload/inline-file-properties/internal/caller_test.go b/seed/go-sdk/file-upload/inline-file-properties/internal/caller_test.go new file mode 100644 index 00000000000..cb76acdaf90 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/internal/caller_test.go @@ -0,0 +1,391 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "github.com/fern-api/file-upload-go/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCase represents a single test case. +type TestCase struct { + description string + + // Server-side assertions. + givePathSuffix string + giveMethod string + giveResponseIsOptional bool + giveHeader http.Header + giveErrorDecoder ErrorDecoder + giveRequest *Request + giveQueryParams url.Values + giveBodyProperties map[string]interface{} + + // Client-side assertions. + wantResponse *Response + wantError error +} + +// Request a simple request body. +type Request struct { + Id string `json:"id"` +} + +// Response a simple response body. +type Response struct { + Id string `json:"id"` + ExtraBodyProperties map[string]interface{} `json:"extraBodyProperties,omitempty"` + QueryParameters url.Values `json:"queryParameters,omitempty"` +} + +// NotFoundError represents a 404. +type NotFoundError struct { + *core.APIError + + Message string `json:"message"` +} + +func TestCall(t *testing.T) { + tests := []*TestCase{ + { + description: "GET success", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + wantResponse: &Response{ + Id: "123", + }, + }, + { + description: "GET success with query", + givePathSuffix: "?limit=1", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + wantResponse: &Response{ + Id: "123", + QueryParameters: url.Values{ + "limit": []string{"1"}, + }, + }, + }, + { + description: "GET not found", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &Request{ + Id: strconv.Itoa(http.StatusNotFound), + }, + giveErrorDecoder: newTestErrorDecoder(t), + wantError: &NotFoundError{ + APIError: core.NewAPIError( + http.StatusNotFound, + errors.New(`{"message":"ID \"404\" not found"}`), + ), + }, + }, + { + description: "POST empty body", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: nil, + wantError: core.NewAPIError( + http.StatusBadRequest, + errors.New("invalid request"), + ), + }, + { + description: "POST optional response", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + giveResponseIsOptional: true, + }, + { + description: "POST API error", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"fail"}, + }, + giveRequest: &Request{ + Id: strconv.Itoa(http.StatusInternalServerError), + }, + wantError: core.NewAPIError( + http.StatusInternalServerError, + errors.New("failed to process request"), + ), + }, + { + description: "POST extra properties", + giveMethod: http.MethodPost, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: new(Request), + giveBodyProperties: map[string]interface{}{ + "key": "value", + }, + wantResponse: &Response{ + ExtraBodyProperties: map[string]interface{}{ + "key": "value", + }, + }, + }, + { + description: "GET extra query parameters", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveQueryParams: url.Values{ + "extra": []string{"true"}, + }, + giveRequest: &Request{ + Id: "123", + }, + wantResponse: &Response{ + Id: "123", + QueryParameters: url.Values{ + "extra": []string{"true"}, + }, + }, + }, + { + description: "GET merge extra query parameters", + givePathSuffix: "?limit=1", + giveMethod: http.MethodGet, + giveHeader: http.Header{ + "X-API-Status": []string{"success"}, + }, + giveRequest: &Request{ + Id: "123", + }, + giveQueryParams: url.Values{ + "extra": []string{"true"}, + }, + wantResponse: &Response{ + Id: "123", + QueryParameters: url.Values{ + "limit": []string{"1"}, + "extra": []string{"true"}, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + var ( + server = newTestServer(t, test) + client = server.Client() + ) + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + var response *Response + err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL + test.givePathSuffix, + Method: test.giveMethod, + Headers: test.giveHeader, + BodyProperties: test.giveBodyProperties, + QueryParameters: test.giveQueryParams, + Request: test.giveRequest, + Response: &response, + ResponseIsOptional: test.giveResponseIsOptional, + ErrorDecoder: test.giveErrorDecoder, + }, + ) + if test.wantError != nil { + assert.EqualError(t, err, test.wantError.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +func TestMergeHeaders(t *testing.T) { + t.Run("both empty", func(t *testing.T) { + merged := MergeHeaders(make(http.Header), make(http.Header)) + assert.Empty(t, merged) + }) + + t.Run("empty left", func(t *testing.T) { + left := make(http.Header) + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("empty right", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.1") + + right := make(http.Header) + + merged := MergeHeaders(left, right) + assert.Equal(t, "0.0.1", merged.Get("X-API-Version")) + }) + + t.Run("single value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Version", "0.0.0") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) + + t.Run("multiple value override", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Versions", "0.0.0") + + right := make(http.Header) + right.Add("X-API-Versions", "0.0.1") + right.Add("X-API-Versions", "0.0.2") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"0.0.1", "0.0.2"}, merged.Values("X-API-Versions")) + }) + + t.Run("disjoint merge", func(t *testing.T) { + left := make(http.Header) + left.Set("X-API-Tenancy", "test") + + right := make(http.Header) + right.Set("X-API-Version", "0.0.1") + + merged := MergeHeaders(left, right) + assert.Equal(t, []string{"test"}, merged.Values("X-API-Tenancy")) + assert.Equal(t, []string{"0.0.1"}, merged.Values("X-API-Version")) + }) +} + +// newTestServer returns a new *httptest.Server configured with the +// given test parameters. +func newTestServer(t *testing.T, tc *TestCase) *httptest.Server { + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, tc.giveMethod, r.Method) + assert.Equal(t, contentType, r.Header.Get(contentTypeHeader)) + for header, value := range tc.giveHeader { + assert.Equal(t, value, r.Header.Values(header)) + } + + request := new(Request) + + bytes, err := io.ReadAll(r.Body) + if tc.giveRequest == nil { + require.Empty(t, bytes) + w.WriteHeader(http.StatusBadRequest) + _, err = w.Write([]byte("invalid request")) + require.NoError(t, err) + return + } + require.NoError(t, err) + require.NoError(t, json.Unmarshal(bytes, request)) + + switch request.Id { + case strconv.Itoa(http.StatusNotFound): + notFoundError := &NotFoundError{ + APIError: &core.APIError{ + StatusCode: http.StatusNotFound, + }, + Message: fmt.Sprintf("ID %q not found", request.Id), + } + bytes, err = json.Marshal(notFoundError) + require.NoError(t, err) + + w.WriteHeader(http.StatusNotFound) + _, err = w.Write(bytes) + require.NoError(t, err) + return + + case strconv.Itoa(http.StatusInternalServerError): + w.WriteHeader(http.StatusInternalServerError) + _, err = w.Write([]byte("failed to process request")) + require.NoError(t, err) + return + } + + if tc.giveResponseIsOptional { + w.WriteHeader(http.StatusOK) + return + } + + extraBodyProperties := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &extraBodyProperties)) + delete(extraBodyProperties, "id") + + response := &Response{ + Id: request.Id, + ExtraBodyProperties: extraBodyProperties, + QueryParameters: r.URL.Query(), + } + bytes, err = json.Marshal(response) + require.NoError(t, err) + + _, err = w.Write(bytes) + require.NoError(t, err) + }, + ), + ) +} + +// newTestErrorDecoder returns an error decoder suitable for tests. +func newTestErrorDecoder(t *testing.T) func(int, io.Reader) error { + return func(statusCode int, body io.Reader) error { + raw, err := io.ReadAll(body) + require.NoError(t, err) + + var ( + apiError = core.NewAPIError(statusCode, errors.New(string(raw))) + decoder = json.NewDecoder(bytes.NewReader(raw)) + ) + if statusCode == http.StatusNotFound { + value := new(NotFoundError) + value.APIError = apiError + require.NoError(t, decoder.Decode(value)) + + return value + } + return apiError + } +} diff --git a/seed/go-sdk/file-upload/internal/extra_properties.go b/seed/go-sdk/file-upload/inline-file-properties/internal/extra_properties.go similarity index 100% rename from seed/go-sdk/file-upload/internal/extra_properties.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/extra_properties.go diff --git a/seed/go-sdk/file-upload/internal/extra_properties_test.go b/seed/go-sdk/file-upload/inline-file-properties/internal/extra_properties_test.go similarity index 100% rename from seed/go-sdk/file-upload/internal/extra_properties_test.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/extra_properties_test.go diff --git a/seed/go-sdk/file-upload/internal/http.go b/seed/go-sdk/file-upload/inline-file-properties/internal/http.go similarity index 100% rename from seed/go-sdk/file-upload/internal/http.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/http.go diff --git a/generators/go/internal/testdata/sdk/auth/fixtures/internal/multipart.go b/seed/go-sdk/file-upload/inline-file-properties/internal/multipart.go similarity index 88% rename from generators/go/internal/testdata/sdk/auth/fixtures/internal/multipart.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/multipart.go index bb58a62ef57..67a2229dc86 100644 --- a/generators/go/internal/testdata/sdk/auth/fixtures/internal/multipart.go +++ b/seed/go-sdk/file-upload/inline-file-properties/internal/multipart.go @@ -23,10 +23,14 @@ type ContentTyped interface { // WriteMultipartOption adapts the behavior of the multipart writer. type WriteMultipartOption func(*writeMultipartOptions) -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { +// WithDefaultContentType sets the default Content-Type for the part +// written to the MultipartWriter. +// +// Note that if the part is a FileParam, the file's Content-Type takes +// precedence over the value provided here. +func WithDefaultContentType(contentType string) WriteMultipartOption { return func(options *writeMultipartOptions) { - options.contentType = contentType + options.defaultContentType = contentType } } @@ -62,7 +66,7 @@ func (w *MultipartWriter) WriteFile( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) + return w.writeFile(field, file, options.defaultContentType) } // WriteField writes the given value as a form field. @@ -72,7 +76,7 @@ func (w *MultipartWriter) WriteField( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) + return w.writeField(field, value, options.defaultContentType) } // WriteJSON writes the given value as a JSON form field. @@ -109,11 +113,14 @@ func (w *MultipartWriter) writeField( func (w *MultipartWriter) writeFile( field string, file io.Reader, - contentType string, + defaultContentType string, ) error { - filename := getFilename(file) - if contentType == "" { + var ( + filename = getFilename(file) contentType = getContentType(file) + ) + if contentType == "" { + contentType = defaultContentType } part, err := w.newFormPart(field, filename, contentType) if err != nil { @@ -147,7 +154,7 @@ func (w *MultipartWriter) newFormPart( // writeMultipartOptions are options used to adapt the behavior of the multipart writer. type writeMultipartOptions struct { - contentType string + defaultContentType string } // newWriteMultipartOptions returns a new write multipart options. diff --git a/generators/go/internal/testdata/sdk/bearer-token-name/fixtures/internal/multipart_test.go b/seed/go-sdk/file-upload/inline-file-properties/internal/multipart_test.go similarity index 92% rename from generators/go/internal/testdata/sdk/bearer-token-name/fixtures/internal/multipart_test.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/multipart_test.go index 69bba7f90ad..07008dd8f42 100644 --- a/generators/go/internal/testdata/sdk/bearer-token-name/fixtures/internal/multipart_test.go +++ b/seed/go-sdk/file-upload/inline-file-properties/internal/multipart_test.go @@ -75,7 +75,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) @@ -108,7 +108,7 @@ func TestMultipartWriter(t *testing.T) { }, }, { - desc: "override content type", + desc: "file content type takes precedence", giveField: "file", giveFile: &mockFile{ name: "test.txt", @@ -117,6 +117,15 @@ func TestMultipartWriter(t *testing.T) { }, giveContentType: "application/octet-stream", }, + { + desc: "default content type", + giveField: "file", + giveFile: &mockFile{ + name: "test.txt", + content: "hello world", + }, + giveContentType: "application/octet-stream", + }, } for _, tt := range tests { @@ -125,7 +134,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) @@ -154,9 +163,9 @@ func TestMultipartWriter(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.giveFile.content, string(content)) - expectedContentType := tt.giveContentType + expectedContentType := tt.giveFile.contentType if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType + expectedContentType = tt.giveContentType } if expectedContentType != "" { assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) diff --git a/seed/go-sdk/file-upload/internal/query.go b/seed/go-sdk/file-upload/inline-file-properties/internal/query.go similarity index 100% rename from seed/go-sdk/file-upload/internal/query.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/query.go diff --git a/seed/go-sdk/file-upload/internal/query_test.go b/seed/go-sdk/file-upload/inline-file-properties/internal/query_test.go similarity index 100% rename from seed/go-sdk/file-upload/internal/query_test.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/query_test.go diff --git a/seed/go-sdk/file-upload/internal/retrier.go b/seed/go-sdk/file-upload/inline-file-properties/internal/retrier.go similarity index 100% rename from seed/go-sdk/file-upload/internal/retrier.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/retrier.go diff --git a/seed/go-sdk/file-upload/inline-file-properties/internal/retrier_test.go b/seed/go-sdk/file-upload/inline-file-properties/internal/retrier_test.go new file mode 100644 index 00000000000..0c97daaf674 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/internal/retrier_test.go @@ -0,0 +1,211 @@ +package internal + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/fern-api/file-upload-go/core" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type RetryTestCase struct { + description string + + giveAttempts uint + giveStatusCodes []int + giveResponse *Response + + wantResponse *Response + wantError *core.APIError +} + +func TestRetrier(t *testing.T) { + tests := []*RetryTestCase{ + { + description: "retry request succeeds after multiple failures", + giveAttempts: 3, + giveStatusCodes: []int{ + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusOK, + }, + giveResponse: &Response{ + Id: "1", + }, + wantResponse: &Response{ + Id: "1", + }, + }, + { + description: "retry request fails if MaxAttempts is exceeded", + giveAttempts: 3, + giveStatusCodes: []int{ + http.StatusRequestTimeout, + http.StatusRequestTimeout, + http.StatusRequestTimeout, + http.StatusOK, + }, + wantError: &core.APIError{ + StatusCode: http.StatusRequestTimeout, + }, + }, + { + description: "retry durations increase exponentially and stay within the min and max delay values", + giveAttempts: 4, + giveStatusCodes: []int{ + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusServiceUnavailable, + http.StatusOK, + }, + }, + { + description: "retry does not occur on status code 404", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusNotFound, http.StatusOK}, + wantError: &core.APIError{ + StatusCode: http.StatusNotFound, + }, + }, + { + description: "retries occur on status code 429", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusTooManyRequests, http.StatusOK}, + }, + { + description: "retries occur on status code 408", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusRequestTimeout, http.StatusOK}, + }, + { + description: "retries occur on status code 500", + giveAttempts: 2, + giveStatusCodes: []int{http.StatusInternalServerError, http.StatusOK}, + }, + } + + for _, tc := range tests { + t.Run(tc.description, func(t *testing.T) { + var ( + test = tc + server = newTestRetryServer(t, test) + client = server.Client() + ) + + t.Parallel() + + caller := NewCaller( + &CallerParams{ + Client: client, + }, + ) + + var response *Response + err := caller.Call( + context.Background(), + &CallParams{ + URL: server.URL, + Method: http.MethodGet, + Request: &Request{}, + Response: &response, + MaxAttempts: test.giveAttempts, + ResponseIsOptional: true, + }, + ) + + if test.wantError != nil { + require.IsType(t, err, &core.APIError{}) + expectedErrorCode := test.wantError.StatusCode + actualErrorCode := err.(*core.APIError).StatusCode + assert.Equal(t, expectedErrorCode, actualErrorCode) + return + } + + require.NoError(t, err) + assert.Equal(t, test.wantResponse, response) + }) + } +} + +// newTestRetryServer returns a new *httptest.Server configured with the +// given test parameters, suitable for testing retries. +func newTestRetryServer(t *testing.T, tc *RetryTestCase) *httptest.Server { + var index int + timestamps := make([]time.Time, 0, len(tc.giveStatusCodes)) + + return httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + timestamps = append(timestamps, time.Now()) + if index > 0 && index < len(expectedRetryDurations) { + // Ensure that the duration between retries increases exponentially, + // and that it is within the minimum and maximum retry delay values. + actualDuration := timestamps[index].Sub(timestamps[index-1]) + expectedDurationMin := expectedRetryDurations[index-1] * 75 / 100 + expectedDurationMax := expectedRetryDurations[index-1] * 125 / 100 + assert.True( + t, + actualDuration >= expectedDurationMin && actualDuration <= expectedDurationMax, + "expected duration to be in range [%v, %v], got %v", + expectedDurationMin, + expectedDurationMax, + actualDuration, + ) + assert.LessOrEqual( + t, + actualDuration, + maxRetryDelay, + "expected duration to be less than the maxRetryDelay (%v), got %v", + maxRetryDelay, + actualDuration, + ) + assert.GreaterOrEqual( + t, + actualDuration, + minRetryDelay, + "expected duration to be greater than the minRetryDelay (%v), got %v", + minRetryDelay, + actualDuration, + ) + } + + request := new(Request) + bytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(bytes, request)) + require.LessOrEqual(t, index, len(tc.giveStatusCodes)) + + statusCode := tc.giveStatusCodes[index] + w.WriteHeader(statusCode) + + if tc.giveResponse != nil && statusCode == http.StatusOK { + bytes, err = json.Marshal(tc.giveResponse) + require.NoError(t, err) + _, err = w.Write(bytes) + require.NoError(t, err) + } + + index++ + }, + ), + ) +} + +// expectedRetryDurations holds an array of calculated retry durations, +// where the index of the array should correspond to the retry attempt. +// +// Values are calculated based off of `minRetryDelay + minRetryDelay*i*i`, with +// a max and min value of 5000ms and 500ms respectively. +var expectedRetryDurations = []time.Duration{ + 500 * time.Millisecond, + 1000 * time.Millisecond, + 2500 * time.Millisecond, + 5000 * time.Millisecond, + 5000 * time.Millisecond, +} diff --git a/seed/go-sdk/file-upload/internal/stringer.go b/seed/go-sdk/file-upload/inline-file-properties/internal/stringer.go similarity index 100% rename from seed/go-sdk/file-upload/internal/stringer.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/stringer.go diff --git a/seed/go-sdk/file-upload/internal/time.go b/seed/go-sdk/file-upload/inline-file-properties/internal/time.go similarity index 100% rename from seed/go-sdk/file-upload/internal/time.go rename to seed/go-sdk/file-upload/inline-file-properties/internal/time.go diff --git a/seed/go-sdk/file-upload/inline-file-properties/option/request_option.go b/seed/go-sdk/file-upload/inline-file-properties/option/request_option.go new file mode 100644 index 00000000000..740059a6b54 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/option/request_option.go @@ -0,0 +1,64 @@ +// This file was auto-generated by Fern from our API Definition. + +package option + +import ( + core "github.com/fern-api/file-upload-go/core" + http "net/http" + url "net/url" +) + +// RequestOption adapts the behavior of an indivdual request. +type RequestOption = core.RequestOption + +// WithBaseURL sets the base URL, overriding the default +// environment, if any. +func WithBaseURL(baseURL string) *core.BaseURLOption { + return &core.BaseURLOption{ + BaseURL: baseURL, + } +} + +// WithHTTPClient uses the given HTTPClient to issue the request. +func WithHTTPClient(httpClient core.HTTPClient) *core.HTTPClientOption { + return &core.HTTPClientOption{ + HTTPClient: httpClient, + } +} + +// WithHTTPHeader adds the given http.Header to the request. +func WithHTTPHeader(httpHeader http.Header) *core.HTTPHeaderOption { + return &core.HTTPHeaderOption{ + // Clone the headers so they can't be modified after the option call. + HTTPHeader: httpHeader.Clone(), + } +} + +// WithBodyProperties adds the given body properties to the request. +func WithBodyProperties(bodyProperties map[string]interface{}) *core.BodyPropertiesOption { + copiedBodyProperties := make(map[string]interface{}, len(bodyProperties)) + for key, value := range bodyProperties { + copiedBodyProperties[key] = value + } + return &core.BodyPropertiesOption{ + BodyProperties: copiedBodyProperties, + } +} + +// WithQueryParameters adds the given query parameters to the request. +func WithQueryParameters(queryParameters url.Values) *core.QueryParametersOption { + copiedQueryParameters := make(url.Values, len(queryParameters)) + for key, values := range queryParameters { + copiedQueryParameters[key] = values + } + return &core.QueryParametersOption{ + QueryParameters: copiedQueryParameters, + } +} + +// WithMaxAttempts configures the maximum number of retry attempts. +func WithMaxAttempts(attempts uint) *core.MaxAttemptsOption { + return &core.MaxAttemptsOption{ + MaxAttempts: attempts, + } +} diff --git a/seed/go-sdk/file-upload/inline-file-properties/pointer.go b/seed/go-sdk/file-upload/inline-file-properties/pointer.go new file mode 100644 index 00000000000..c9605c983ec --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/pointer.go @@ -0,0 +1,132 @@ +package upload + +import ( + "time" + + "github.com/google/uuid" +) + +// Bool returns a pointer to the given bool value. +func Bool(b bool) *bool { + return &b +} + +// Byte returns a pointer to the given byte value. +func Byte(b byte) *byte { + return &b +} + +// Complex64 returns a pointer to the given complex64 value. +func Complex64(c complex64) *complex64 { + return &c +} + +// Complex128 returns a pointer to the given complex128 value. +func Complex128(c complex128) *complex128 { + return &c +} + +// Float32 returns a pointer to the given float32 value. +func Float32(f float32) *float32 { + return &f +} + +// Float64 returns a pointer to the given float64 value. +func Float64(f float64) *float64 { + return &f +} + +// Int returns a pointer to the given int value. +func Int(i int) *int { + return &i +} + +// Int8 returns a pointer to the given int8 value. +func Int8(i int8) *int8 { + return &i +} + +// Int16 returns a pointer to the given int16 value. +func Int16(i int16) *int16 { + return &i +} + +// Int32 returns a pointer to the given int32 value. +func Int32(i int32) *int32 { + return &i +} + +// Int64 returns a pointer to the given int64 value. +func Int64(i int64) *int64 { + return &i +} + +// Rune returns a pointer to the given rune value. +func Rune(r rune) *rune { + return &r +} + +// String returns a pointer to the given string value. +func String(s string) *string { + return &s +} + +// Uint returns a pointer to the given uint value. +func Uint(u uint) *uint { + return &u +} + +// Uint8 returns a pointer to the given uint8 value. +func Uint8(u uint8) *uint8 { + return &u +} + +// Uint16 returns a pointer to the given uint16 value. +func Uint16(u uint16) *uint16 { + return &u +} + +// Uint32 returns a pointer to the given uint32 value. +func Uint32(u uint32) *uint32 { + return &u +} + +// Uint64 returns a pointer to the given uint64 value. +func Uint64(u uint64) *uint64 { + return &u +} + +// Uintptr returns a pointer to the given uintptr value. +func Uintptr(u uintptr) *uintptr { + return &u +} + +// UUID returns a pointer to the given uuid.UUID value. +func UUID(u uuid.UUID) *uuid.UUID { + return &u +} + +// Time returns a pointer to the given time.Time value. +func Time(t time.Time) *time.Time { + return &t +} + +// MustParseDate attempts to parse the given string as a +// date time.Time, and panics upon failure. +func MustParseDate(date string) time.Time { + t, err := time.Parse("2006-01-02", date) + if err != nil { + panic(err) + } + return t +} + +// MustParseDateTime attempts to parse the given string as a +// datetime time.Time, and panics upon failure. +func MustParseDateTime(datetime string) time.Time { + t, err := time.Parse(time.RFC3339, datetime) + if err != nil { + panic(err) + } + return t +} diff --git a/seed/go-sdk/file-upload/inline-file-properties/service.go b/seed/go-sdk/file-upload/inline-file-properties/service.go new file mode 100644 index 00000000000..0efd41c250a --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/service.go @@ -0,0 +1,116 @@ +// This file was auto-generated by Fern from our API Definition. + +package upload + +import ( + json "encoding/json" + fmt "fmt" + internal "github.com/fern-api/file-upload-go/internal" + io "io" +) + +type JustFileRequet struct { + File io.Reader `json:"-" url:"-"` +} + +type JustFileWithQueryParamsRequet struct { + MaybeString *string `json:"-" url:"maybeString,omitempty"` + Integer int `json:"-" url:"integer"` + MaybeInteger *int `json:"-" url:"maybeInteger,omitempty"` + ListOfStrings []string `json:"-" url:"listOfStrings"` + OptionalListOfStrings []*string `json:"-" url:"optionalListOfStrings,omitempty"` + File io.Reader `json:"-" url:"-"` +} + +type MyRequest struct { + File io.Reader `json:"-" url:"-"` + FileList []io.Reader `json:"-" url:"-"` + MaybeFile io.Reader `json:"-" url:"-"` + MaybeFileList []io.Reader `json:"-" url:"-"` + MaybeString *string `json:"maybeString,omitempty" url:"-"` + Integer int `json:"integer" url:"-"` + MaybeInteger *int `json:"maybeInteger,omitempty" url:"-"` + OptionalListOfStrings []string `json:"optionalListOfStrings,omitempty" url:"-"` + ListOfObjects []*MyObject `json:"listOfObjects,omitempty" url:"-"` + OptionalMetadata interface{} `json:"optionalMetadata,omitempty" url:"-"` + OptionalObjectType *ObjectType `json:"optionalObjectType,omitempty" url:"-"` + OptionalId *Id `json:"optionalId,omitempty" url:"-"` +} + +type Id = string + +type MyObject struct { + Foo string `json:"foo" url:"foo"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *MyObject) GetFoo() string { + if m == nil { + return "" + } + return m.Foo +} + +func (m *MyObject) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *MyObject) UnmarshalJSON(data []byte) error { + type unmarshaler MyObject + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = MyObject(value) + + extraProperties, err := internal.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *MyObject) String() string { + if len(m._rawJSON) > 0 { + if value, err := internal.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := internal.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} + +type ObjectType string + +const ( + ObjectTypeFoo ObjectType = "FOO" + ObjectTypeBar ObjectType = "BAR" +) + +func NewObjectTypeFromString(s string) (ObjectType, error) { + switch s { + case "FOO": + return ObjectTypeFoo, nil + case "BAR": + return ObjectTypeBar, nil + } + var t ObjectType + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (o ObjectType) Ptr() *ObjectType { + return &o +} + +type WithContentTypeRequest struct { + File io.Reader `json:"-" url:"-"` + Foo string `json:"foo" url:"-"` + Bar *MyObject `json:"bar,omitempty" url:"-"` +} diff --git a/seed/go-sdk/file-upload/inline-file-properties/service/client.go b/seed/go-sdk/file-upload/inline-file-properties/service/client.go new file mode 100644 index 00000000000..2a51b655839 --- /dev/null +++ b/seed/go-sdk/file-upload/inline-file-properties/service/client.go @@ -0,0 +1,280 @@ +// This file was auto-generated by Fern from our API Definition. + +package service + +import ( + context "context" + fmt "fmt" + fileuploadgo "github.com/fern-api/file-upload-go" + core "github.com/fern-api/file-upload-go/core" + internal "github.com/fern-api/file-upload-go/internal" + option "github.com/fern-api/file-upload-go/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *internal.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: internal.NewCaller( + &internal.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +func (c *Client) Post( + ctx context.Context, + request *fileuploadgo.MyRequest, + opts ...option.RequestOption, +) error { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) + + writer := internal.NewMultipartWriter() + if err := writer.WriteFile("file", request.File); err != nil { + return err + } + for _, f := range request.FileList { + if err := writer.WriteFile("fileList", f); err != nil { + return err + } + } + if request.MaybeFile != nil { + if err := writer.WriteFile("maybeFile", request.MaybeFile); err != nil { + return err + } + } + for _, f := range request.MaybeFileList { + if err := writer.WriteFile("maybeFileList", f); err != nil { + return err + } + } + if request.MaybeString != nil { + if err := writer.WriteField("maybeString", fmt.Sprintf("%v", *request.MaybeString)); err != nil { + return err + } + } + if err := writer.WriteField("integer", fmt.Sprintf("%v", request.Integer)); err != nil { + return err + } + if request.MaybeInteger != nil { + if err := writer.WriteField("maybeInteger", fmt.Sprintf("%v", *request.MaybeInteger)); err != nil { + return err + } + } + for _, part := range request.OptionalListOfStrings { + if err := writer.WriteField("optionalListOfStrings", fmt.Sprintf("%v", part)); err != nil { + return err + } + } + for _, part := range request.ListOfObjects { + if err := writer.WriteJSON("listOfObjects", part); err != nil { + return err + } + } + if request.OptionalMetadata != nil { + if err := writer.WriteJSON("optionalMetadata", request.OptionalMetadata); err != nil { + return err + } + } + if request.OptionalObjectType != nil { + if err := writer.WriteJSON("optionalObjectType", *request.OptionalObjectType); err != nil { + return err + } + } + if request.OptionalId != nil { + if err := writer.WriteJSON("optionalId", *request.OptionalId); err != nil { + return err + } + } + if err := writer.Close(); err != nil { + return err + } + headers.Set("Content-Type", writer.ContentType()) + + if err := c.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: writer.Buffer(), + }, + ); err != nil { + return err + } + return nil +} + +func (c *Client) JustFile( + ctx context.Context, + request *fileuploadgo.JustFileRequet, + opts ...option.RequestOption, +) error { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/just-file" + + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) + + writer := internal.NewMultipartWriter() + if err := writer.WriteFile("file", request.File); err != nil { + return err + } + if err := writer.Close(); err != nil { + return err + } + headers.Set("Content-Type", writer.ContentType()) + + if err := c.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: writer.Buffer(), + }, + ); err != nil { + return err + } + return nil +} + +func (c *Client) JustFileWithQueryParams( + ctx context.Context, + request *fileuploadgo.JustFileWithQueryParamsRequet, + opts ...option.RequestOption, +) error { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/just-file-with-query-params" + + queryParams, err := internal.QueryValues(request) + if err != nil { + return err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) + + writer := internal.NewMultipartWriter() + if err := writer.WriteFile("file", request.File); err != nil { + return err + } + if err := writer.Close(); err != nil { + return err + } + headers.Set("Content-Type", writer.ContentType()) + + if err := c.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: writer.Buffer(), + }, + ); err != nil { + return err + } + return nil +} + +func (c *Client) WithContentType( + ctx context.Context, + request *fileuploadgo.WithContentTypeRequest, + opts ...option.RequestOption, +) error { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/with-content-type" + + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) + + writer := internal.NewMultipartWriter() + if err := writer.WriteFile("file", request.File, internal.WithDefaultContentType("application/octet-stream")); err != nil { + return err + } + if err := writer.WriteField("foo", fmt.Sprintf("%v", request.Foo)); err != nil { + return err + } + if err := writer.WriteJSON("bar", request.Bar, internal.WithDefaultContentType("application/json")); err != nil { + return err + } + if err := writer.Close(); err != nil { + return err + } + headers.Set("Content-Type", writer.ContentType()) + + if err := c.caller.Call( + ctx, + &internal.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: writer.Buffer(), + }, + ); err != nil { + return err + } + return nil +} diff --git a/seed/go-sdk/file-upload/snippet-templates.json b/seed/go-sdk/file-upload/inline-file-properties/snippet-templates.json similarity index 100% rename from seed/go-sdk/file-upload/snippet-templates.json rename to seed/go-sdk/file-upload/inline-file-properties/snippet-templates.json diff --git a/seed/go-sdk/file-upload/snippet.json b/seed/go-sdk/file-upload/inline-file-properties/snippet.json similarity index 100% rename from seed/go-sdk/file-upload/snippet.json rename to seed/go-sdk/file-upload/inline-file-properties/snippet.json diff --git a/seed/go-sdk/file-upload/internal/multipart.go b/seed/go-sdk/file-upload/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/file-upload/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/file-upload/internal/multipart_test.go b/seed/go-sdk/file-upload/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/file-upload/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/file-upload/no-custom-config/.github/workflows/ci.yml b/seed/go-sdk/file-upload/no-custom-config/.github/workflows/ci.yml new file mode 100644 index 00000000000..d4c0a5dcd95 --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Compile + run: go build ./... + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + + - name: Set up go + uses: actions/setup-go@v4 + + - name: Test + run: go test ./... diff --git a/seed/go-sdk/file-upload/no-custom-config/.mock/definition/api.yml b/seed/go-sdk/file-upload/no-custom-config/.mock/definition/api.yml new file mode 100644 index 00000000000..b7f420003fb --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/.mock/definition/api.yml @@ -0,0 +1 @@ +name: file-upload diff --git a/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml b/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml new file mode 100644 index 00000000000..5ae95021c2d --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml @@ -0,0 +1,78 @@ +service: + auth: false + base-path: / + endpoints: + post: + path: "" + method: POST + request: + name: MyRequest + body: + properties: + maybeString: optional + integer: integer + file: file + fileList: list + maybeFile: optional + maybeFileList: optional> + maybeInteger: optional + optionalListOfStrings: optional> + listOfObjects: list + optionalMetadata: optional + optionalObjectType: optional + optionalId: optional + + justFile: + path: /just-file + method: POST + request: + name: JustFileRequet + body: + properties: + file: file + + justFileWithQueryParams: + path: /just-file-with-query-params + method: POST + request: + name: JustFileWithQueryParamsRequet + query-parameters: + maybeString: optional + integer: integer + maybeInteger: optional + listOfStrings: + type: string + allow-multiple: true + optionalListOfStrings: + type: optional + allow-multiple: true + body: + properties: + file: file + + withContentType: + path: "/with-content-type" + method: POST + request: + name: WithContentTypeRequest + body: + properties: + file: + type: file + content-type: application/octet-stream + foo: string + bar: + type: MyObject + content-type: application/json + +types: + Id: string + + MyObject: + properties: + foo: string + + ObjectType: + enum: + - FOO + - BAR \ No newline at end of file diff --git a/seed/go-sdk/file-upload/no-custom-config/.mock/fern.config.json b/seed/go-sdk/file-upload/no-custom-config/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/go-sdk/file-upload/no-custom-config/.mock/generators.yml b/seed/go-sdk/file-upload/no-custom-config/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/go-sdk/file-upload/client/client.go b/seed/go-sdk/file-upload/no-custom-config/client/client.go similarity index 100% rename from seed/go-sdk/file-upload/client/client.go rename to seed/go-sdk/file-upload/no-custom-config/client/client.go diff --git a/seed/go-sdk/file-upload/client/client_test.go b/seed/go-sdk/file-upload/no-custom-config/client/client_test.go similarity index 100% rename from seed/go-sdk/file-upload/client/client_test.go rename to seed/go-sdk/file-upload/no-custom-config/client/client_test.go diff --git a/seed/go-sdk/file-upload/no-custom-config/core/api_error.go b/seed/go-sdk/file-upload/no-custom-config/core/api_error.go new file mode 100644 index 00000000000..dc4190ca1cd --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/core/api_error.go @@ -0,0 +1,42 @@ +package core + +import "fmt" + +// APIError is a lightweight wrapper around the standard error +// interface that preserves the status code from the RPC, if any. +type APIError struct { + err error + + StatusCode int `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, err error) *APIError { + return &APIError{ + err: err, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} diff --git a/seed/go-sdk/file-upload/no-custom-config/core/http.go b/seed/go-sdk/file-upload/no-custom-config/core/http.go new file mode 100644 index 00000000000..b553350b84e --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/core/http.go @@ -0,0 +1,8 @@ +package core + +import "net/http" + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/seed/go-sdk/file-upload/core/request_option.go b/seed/go-sdk/file-upload/no-custom-config/core/request_option.go similarity index 100% rename from seed/go-sdk/file-upload/core/request_option.go rename to seed/go-sdk/file-upload/no-custom-config/core/request_option.go diff --git a/seed/go-sdk/file-upload/file_param.go b/seed/go-sdk/file-upload/no-custom-config/file_param.go similarity index 100% rename from seed/go-sdk/file-upload/file_param.go rename to seed/go-sdk/file-upload/no-custom-config/file_param.go diff --git a/seed/go-sdk/file-upload/go.mod b/seed/go-sdk/file-upload/no-custom-config/go.mod similarity index 100% rename from seed/go-sdk/file-upload/go.mod rename to seed/go-sdk/file-upload/no-custom-config/go.mod diff --git a/seed/go-sdk/file-upload/no-custom-config/go.sum b/seed/go-sdk/file-upload/no-custom-config/go.sum new file mode 100644 index 00000000000..b3766d4366b --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/seed/go-sdk/file-upload/internal/caller.go b/seed/go-sdk/file-upload/no-custom-config/internal/caller.go similarity index 100% rename from seed/go-sdk/file-upload/internal/caller.go rename to seed/go-sdk/file-upload/no-custom-config/internal/caller.go diff --git a/seed/go-sdk/file-upload/internal/caller_test.go b/seed/go-sdk/file-upload/no-custom-config/internal/caller_test.go similarity index 100% rename from seed/go-sdk/file-upload/internal/caller_test.go rename to seed/go-sdk/file-upload/no-custom-config/internal/caller_test.go diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/extra_properties.go b/seed/go-sdk/file-upload/no-custom-config/internal/extra_properties.go new file mode 100644 index 00000000000..540c3fd89ee --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/extra_properties.go @@ -0,0 +1,141 @@ +package internal + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "strings" +) + +// MarshalJSONWithExtraProperty marshals the given value to JSON, including the extra property. +func MarshalJSONWithExtraProperty(marshaler interface{}, key string, value interface{}) ([]byte, error) { + return MarshalJSONWithExtraProperties(marshaler, map[string]interface{}{key: value}) +} + +// MarshalJSONWithExtraProperties marshals the given value to JSON, including any extra properties. +func MarshalJSONWithExtraProperties(marshaler interface{}, extraProperties map[string]interface{}) ([]byte, error) { + bytes, err := json.Marshal(marshaler) + if err != nil { + return nil, err + } + if len(extraProperties) == 0 { + return bytes, nil + } + keys, err := getKeys(marshaler) + if err != nil { + return nil, err + } + for _, key := range keys { + if _, ok := extraProperties[key]; ok { + return nil, fmt.Errorf("cannot add extra property %q because it is already defined on the type", key) + } + } + extraBytes, err := json.Marshal(extraProperties) + if err != nil { + return nil, err + } + if isEmptyJSON(bytes) { + if isEmptyJSON(extraBytes) { + return bytes, nil + } + return extraBytes, nil + } + result := bytes[:len(bytes)-1] + result = append(result, ',') + result = append(result, extraBytes[1:len(extraBytes)-1]...) + result = append(result, '}') + return result, nil +} + +// ExtractExtraProperties extracts any extra properties from the given value. +func ExtractExtraProperties(bytes []byte, value interface{}, exclude ...string) (map[string]interface{}, error) { + val := reflect.ValueOf(value) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return nil, fmt.Errorf("value must be non-nil to extract extra properties") + } + val = val.Elem() + } + if err := json.Unmarshal(bytes, &value); err != nil { + return nil, err + } + var extraProperties map[string]interface{} + if err := json.Unmarshal(bytes, &extraProperties); err != nil { + return nil, err + } + for i := 0; i < val.Type().NumField(); i++ { + key := jsonKey(val.Type().Field(i)) + if key == "" || key == "-" { + continue + } + delete(extraProperties, key) + } + for _, key := range exclude { + delete(extraProperties, key) + } + if len(extraProperties) == 0 { + return nil, nil + } + return extraProperties, nil +} + +// getKeys returns the keys associated with the given value. The value must be a +// a struct or a map with string keys. +func getKeys(value interface{}) ([]string, error) { + val := reflect.ValueOf(value) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return nil, nil + } + switch val.Kind() { + case reflect.Struct: + return getKeysForStructType(val.Type()), nil + case reflect.Map: + var keys []string + if val.Type().Key().Kind() != reflect.String { + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } + for _, key := range val.MapKeys() { + keys = append(keys, key.String()) + } + return keys, nil + default: + return nil, fmt.Errorf("cannot extract keys from %T; only structs and maps with string keys are supported", value) + } +} + +// getKeysForStructType returns all the keys associated with the given struct type, +// visiting embedded fields recursively. +func getKeysForStructType(structType reflect.Type) []string { + if structType.Kind() == reflect.Pointer { + structType = structType.Elem() + } + if structType.Kind() != reflect.Struct { + return nil + } + var keys []string + for i := 0; i < structType.NumField(); i++ { + field := structType.Field(i) + if field.Anonymous { + keys = append(keys, getKeysForStructType(field.Type)...) + continue + } + keys = append(keys, jsonKey(field)) + } + return keys +} + +// jsonKey returns the JSON key from the struct tag of the given field, +// excluding the omitempty flag (if any). +func jsonKey(field reflect.StructField) string { + return strings.TrimSuffix(field.Tag.Get("json"), ",omitempty") +} + +// isEmptyJSON returns true if the given data is empty, the empty JSON object, or +// an explicit null. +func isEmptyJSON(data []byte) bool { + return len(data) <= 2 || bytes.Equal(data, []byte("null")) +} diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/extra_properties_test.go b/seed/go-sdk/file-upload/no-custom-config/internal/extra_properties_test.go new file mode 100644 index 00000000000..aa2510ee512 --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/extra_properties_test.go @@ -0,0 +1,228 @@ +package internal + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type testMarshaler struct { + Name string `json:"name"` + BirthDate time.Time `json:"birthDate"` + CreatedAt time.Time `json:"created_at"` +} + +func (t *testMarshaler) MarshalJSON() ([]byte, error) { + type embed testMarshaler + var marshaler = struct { + embed + BirthDate string `json:"birthDate"` + CreatedAt string `json:"created_at"` + }{ + embed: embed(*t), + BirthDate: t.BirthDate.Format("2006-01-02"), + CreatedAt: t.CreatedAt.Format(time.RFC3339), + } + return MarshalJSONWithExtraProperty(marshaler, "type", "test") +} + +func TestMarshalJSONWithExtraProperties(t *testing.T) { + tests := []struct { + desc string + giveMarshaler interface{} + giveExtraProperties map[string]interface{} + wantBytes []byte + wantError string + }{ + { + desc: "invalid type", + giveMarshaler: []string{"invalid"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from []string; only structs and maps with string keys are supported`, + }, + { + desc: "invalid key type", + giveMarshaler: map[int]interface{}{42: "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot extract keys from map[int]interface {}; only structs and maps with string keys are supported`, + }, + { + desc: "invalid map overwrite", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"key": "overwrite"}, + wantError: `cannot add extra property "key" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"birthDate": "2000-01-01"}, + wantError: `cannot add extra property "birthDate" because it is already defined on the type`, + }, + { + desc: "invalid struct overwrite embedded type", + giveMarshaler: new(testMarshaler), + giveExtraProperties: map[string]interface{}{"name": "bob"}, + wantError: `cannot add extra property "name" because it is already defined on the type`, + }, + { + desc: "nil", + giveMarshaler: nil, + giveExtraProperties: nil, + wantBytes: []byte(`null`), + }, + { + desc: "empty", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{}`), + }, + { + desc: "no extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "only extra properties", + giveMarshaler: map[string]interface{}{}, + giveExtraProperties: map[string]interface{}{"key": "value"}, + wantBytes: []byte(`{"key":"value"}`), + }, + { + desc: "single extra property", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"extra": "property"}, + wantBytes: []byte(`{"key":"value","extra":"property"}`), + }, + { + desc: "multiple extra properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{"one": 1, "two": 2}, + wantBytes: []byte(`{"key":"value","one":1,"two":2}`), + }, + { + desc: "nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","user":{"age":42,"name":"alice"}}`), + }, + { + desc: "multiple nested properties", + giveMarshaler: map[string]interface{}{"key": "value"}, + giveExtraProperties: map[string]interface{}{ + "metadata": map[string]interface{}{ + "ip": "127.0.0.1", + }, + "user": map[string]interface{}{ + "age": 42, + "name": "alice", + }, + }, + wantBytes: []byte(`{"key":"value","metadata":{"ip":"127.0.0.1"},"user":{"age":42,"name":"alice"}}`), + }, + { + desc: "custom marshaler", + giveMarshaler: &testMarshaler{ + Name: "alice", + BirthDate: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + }, + giveExtraProperties: map[string]interface{}{ + "extra": "property", + }, + wantBytes: []byte(`{"name":"alice","birthDate":"2000-01-01","created_at":"2024-01-01T00:00:00Z","type":"test","extra":"property"}`), + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + bytes, err := MarshalJSONWithExtraProperties(tt.giveMarshaler, tt.giveExtraProperties) + if tt.wantError != "" { + require.EqualError(t, err, tt.wantError) + assert.Nil(t, tt.wantBytes) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantBytes, bytes) + + value := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(bytes, &value)) + }) + } +} + +func TestExtractExtraProperties(t *testing.T) { + t.Run("none", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice"}`), value) + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) + + t.Run("non-nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("nil pointer", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value *user + _, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + assert.EqualError(t, err, "value must be non-nil to extract extra properties") + }) + + t.Run("non-zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("zero value", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + var value user + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value) + require.NoError(t, err) + assert.Equal(t, map[string]interface{}{"age": float64(42)}, extraProperties) + }) + + t.Run("exclude", func(t *testing.T) { + type user struct { + Name string `json:"name"` + } + value := &user{ + Name: "alice", + } + extraProperties, err := ExtractExtraProperties([]byte(`{"name": "alice", "age": 42}`), value, "age") + require.NoError(t, err) + assert.Nil(t, extraProperties) + }) +} diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/http.go b/seed/go-sdk/file-upload/no-custom-config/internal/http.go new file mode 100644 index 00000000000..2be0805a8be --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/http.go @@ -0,0 +1,37 @@ +package internal + +import ( + "fmt" + "net/http" + "net/url" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} diff --git a/generators/go/internal/testdata/sdk/bearer-token-name/fixtures/internal/multipart.go b/seed/go-sdk/file-upload/no-custom-config/internal/multipart.go similarity index 88% rename from generators/go/internal/testdata/sdk/bearer-token-name/fixtures/internal/multipart.go rename to seed/go-sdk/file-upload/no-custom-config/internal/multipart.go index bb58a62ef57..67a2229dc86 100644 --- a/generators/go/internal/testdata/sdk/bearer-token-name/fixtures/internal/multipart.go +++ b/seed/go-sdk/file-upload/no-custom-config/internal/multipart.go @@ -23,10 +23,14 @@ type ContentTyped interface { // WriteMultipartOption adapts the behavior of the multipart writer. type WriteMultipartOption func(*writeMultipartOptions) -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { +// WithDefaultContentType sets the default Content-Type for the part +// written to the MultipartWriter. +// +// Note that if the part is a FileParam, the file's Content-Type takes +// precedence over the value provided here. +func WithDefaultContentType(contentType string) WriteMultipartOption { return func(options *writeMultipartOptions) { - options.contentType = contentType + options.defaultContentType = contentType } } @@ -62,7 +66,7 @@ func (w *MultipartWriter) WriteFile( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) + return w.writeFile(field, file, options.defaultContentType) } // WriteField writes the given value as a form field. @@ -72,7 +76,7 @@ func (w *MultipartWriter) WriteField( opts ...WriteMultipartOption, ) error { options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) + return w.writeField(field, value, options.defaultContentType) } // WriteJSON writes the given value as a JSON form field. @@ -109,11 +113,14 @@ func (w *MultipartWriter) writeField( func (w *MultipartWriter) writeFile( field string, file io.Reader, - contentType string, + defaultContentType string, ) error { - filename := getFilename(file) - if contentType == "" { + var ( + filename = getFilename(file) contentType = getContentType(file) + ) + if contentType == "" { + contentType = defaultContentType } part, err := w.newFormPart(field, filename, contentType) if err != nil { @@ -147,7 +154,7 @@ func (w *MultipartWriter) newFormPart( // writeMultipartOptions are options used to adapt the behavior of the multipart writer. type writeMultipartOptions struct { - contentType string + defaultContentType string } // newWriteMultipartOptions returns a new write multipart options. diff --git a/generators/go/internal/testdata/sdk/auth/fixtures/internal/multipart_test.go b/seed/go-sdk/file-upload/no-custom-config/internal/multipart_test.go similarity index 92% rename from generators/go/internal/testdata/sdk/auth/fixtures/internal/multipart_test.go rename to seed/go-sdk/file-upload/no-custom-config/internal/multipart_test.go index 69bba7f90ad..07008dd8f42 100644 --- a/generators/go/internal/testdata/sdk/auth/fixtures/internal/multipart_test.go +++ b/seed/go-sdk/file-upload/no-custom-config/internal/multipart_test.go @@ -75,7 +75,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) @@ -108,7 +108,7 @@ func TestMultipartWriter(t *testing.T) { }, }, { - desc: "override content type", + desc: "file content type takes precedence", giveField: "file", giveFile: &mockFile{ name: "test.txt", @@ -117,6 +117,15 @@ func TestMultipartWriter(t *testing.T) { }, giveContentType: "application/octet-stream", }, + { + desc: "default content type", + giveField: "file", + giveFile: &mockFile{ + name: "test.txt", + content: "hello world", + }, + giveContentType: "application/octet-stream", + }, } for _, tt := range tests { @@ -125,7 +134,7 @@ func TestMultipartWriter(t *testing.T) { var opts []WriteMultipartOption if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) + opts = append(opts, WithDefaultContentType(tt.giveContentType)) } require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) @@ -154,9 +163,9 @@ func TestMultipartWriter(t *testing.T) { require.NoError(t, err) assert.Equal(t, tt.giveFile.content, string(content)) - expectedContentType := tt.giveContentType + expectedContentType := tt.giveFile.contentType if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType + expectedContentType = tt.giveContentType } if expectedContentType != "" { assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/query.go b/seed/go-sdk/file-upload/no-custom-config/internal/query.go new file mode 100644 index 00000000000..6129e71ffe5 --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/query.go @@ -0,0 +1,231 @@ +package internal + +import ( + "encoding/base64" + "fmt" + "net/url" + "reflect" + "strings" + "time" + + "github.com/google/uuid" +) + +var ( + bytesType = reflect.TypeOf([]byte{}) + queryEncoderType = reflect.TypeOf(new(QueryEncoder)).Elem() + timeType = reflect.TypeOf(time.Time{}) + uuidType = reflect.TypeOf(uuid.UUID{}) +) + +// QueryEncoder is an interface implemented by any type that wishes to encode +// itself into URL values in a non-standard way. +type QueryEncoder interface { + EncodeQueryValues(key string, v *url.Values) error +} + +// QueryValues encodes url.Values from request objects. +// +// Note: This type is inspired by Google's query encoding library, but +// supports far less customization and is tailored to fit this SDK's use case. +// +// Ref: https://github.com/google/go-querystring +func QueryValues(v interface{}) (url.Values, error) { + values := make(url.Values) + val := reflect.ValueOf(v) + for val.Kind() == reflect.Ptr { + if val.IsNil() { + return values, nil + } + val = val.Elem() + } + + if v == nil { + return values, nil + } + + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) + } + + err := reflectValue(values, val, "") + return values, err +} + +// reflectValue populates the values parameter from the struct fields in val. +// Embedded structs are followed recursively (using the rules defined in the +// Values function documentation) breadth-first. +func reflectValue(values url.Values, val reflect.Value, scope string) error { + typ := val.Type() + for i := 0; i < typ.NumField(); i++ { + sf := typ.Field(i) + if sf.PkgPath != "" && !sf.Anonymous { + // Skip unexported fields. + continue + } + + sv := val.Field(i) + tag := sf.Tag.Get("url") + if tag == "" || tag == "-" { + continue + } + + name, opts := parseTag(tag) + if name == "" { + name = sf.Name + } + + if scope != "" { + name = scope + "[" + name + "]" + } + + if opts.Contains("omitempty") && isEmptyValue(sv) { + continue + } + + if sv.Type().Implements(queryEncoderType) { + // If sv is a nil pointer and the custom encoder is defined on a non-pointer + // method receiver, set sv to the zero value of the underlying type + if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(queryEncoderType) { + sv = reflect.New(sv.Type().Elem()) + } + + m := sv.Interface().(QueryEncoder) + if err := m.EncodeQueryValues(name, &values); err != nil { + return err + } + continue + } + + // Recursively dereference pointers, but stop at nil pointers. + for sv.Kind() == reflect.Ptr { + if sv.IsNil() { + break + } + sv = sv.Elem() + } + + if sv.Type() == uuidType || sv.Type() == bytesType || sv.Type() == timeType { + values.Add(name, valueString(sv, opts, sf)) + continue + } + + if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { + if sv.Len() == 0 { + // Skip if slice or array is empty. + continue + } + for i := 0; i < sv.Len(); i++ { + value := sv.Index(i) + if isStructPointer(value) && !value.IsNil() { + if err := reflectValue(values, value.Elem(), name); err != nil { + return err + } + } else { + values.Add(name, valueString(value, opts, sf)) + } + } + continue + } + + if sv.Kind() == reflect.Struct { + if err := reflectValue(values, sv, name); err != nil { + return err + } + continue + } + + values.Add(name, valueString(sv, opts, sf)) + } + + return nil +} + +// valueString returns the string representation of a value. +func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { + for v.Kind() == reflect.Ptr { + if v.IsNil() { + return "" + } + v = v.Elem() + } + + if v.Type() == timeType { + t := v.Interface().(time.Time) + if format := sf.Tag.Get("format"); format == "date" { + return t.Format("2006-01-02") + } + return t.Format(time.RFC3339) + } + + if v.Type() == uuidType { + u := v.Interface().(uuid.UUID) + return u.String() + } + + if v.Type() == bytesType { + b := v.Interface().([]byte) + return base64.StdEncoding.EncodeToString(b) + } + + return fmt.Sprint(v.Interface()) +} + +// isEmptyValue checks if a value should be considered empty for the purposes +// of omitting fields with the "omitempty" option. +func isEmptyValue(v reflect.Value) bool { + type zeroable interface { + IsZero() bool + } + + if !v.IsZero() { + if z, ok := v.Interface().(zeroable); ok { + return z.IsZero() + } + } + + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Struct, reflect.UnsafePointer: + return false + } + + return false +} + +// isStructPointer returns true if the given reflect.Value is a pointer to a struct. +func isStructPointer(v reflect.Value) bool { + return v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct +} + +// tagOptions is the string following a comma in a struct field's "url" tag, or +// the empty string. It does not include the leading comma. +type tagOptions []string + +// parseTag splits a struct field's url tag into its name and comma-separated +// options. +func parseTag(tag string) (string, tagOptions) { + s := strings.Split(tag, ",") + return s[0], s[1:] +} + +// Contains checks whether the tagOptions contains the specified option. +func (o tagOptions) Contains(option string) bool { + for _, s := range o { + if s == option { + return true + } + } + return false +} diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go b/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go new file mode 100644 index 00000000000..2e58ccadde1 --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/query_test.go @@ -0,0 +1,187 @@ +package internal + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestQueryValues(t *testing.T) { + t.Run("empty optional", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Empty(t, values) + }) + + t.Run("empty required", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + values, err := QueryValues(&example{}) + require.NoError(t, err) + assert.Equal(t, "required=", values.Encode()) + }) + + t.Run("allow multiple", func(t *testing.T) { + type example struct { + Values []string `json:"values" url:"values"` + } + + values, err := QueryValues( + &example{ + Values: []string{"foo", "bar", "baz"}, + }, + ) + require.NoError(t, err) + assert.Equal(t, "values=foo&values=bar&values=baz", values.Encode()) + }) + + t.Run("nested object", func(t *testing.T) { + type nested struct { + Value *string `json:"value,omitempty" url:"value,omitempty"` + } + type example struct { + Required string `json:"required" url:"required"` + Nested *nested `json:"nested,omitempty" url:"nested,omitempty"` + } + + nestedValue := "nestedValue" + values, err := QueryValues( + &example{ + Required: "requiredValue", + Nested: &nested{ + Value: &nestedValue, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "nested%5Bvalue%5D=nestedValue&required=requiredValue", values.Encode()) + }) + + t.Run("url unspecified", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("url ignored", func(t *testing.T) { + type example struct { + Required string `json:"required" url:"required"` + NotFound string `json:"notFound" url:"-"` + } + + values, err := QueryValues( + &example{ + Required: "requiredValue", + NotFound: "notFound", + }, + ) + require.NoError(t, err) + assert.Equal(t, "required=requiredValue", values.Encode()) + }) + + t.Run("datetime", func(t *testing.T) { + type example struct { + DateTime time.Time `json:"dateTime" url:"dateTime"` + } + + values, err := QueryValues( + &example{ + DateTime: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "dateTime=1994-03-16T12%3A34%3A56Z", values.Encode()) + }) + + t.Run("date", func(t *testing.T) { + type example struct { + Date time.Time `json:"date" url:"date" format:"date"` + } + + values, err := QueryValues( + &example{ + Date: time.Date(1994, 3, 16, 12, 34, 56, 0, time.UTC), + }, + ) + require.NoError(t, err) + assert.Equal(t, "date=1994-03-16", values.Encode()) + }) + + t.Run("optional time", func(t *testing.T) { + type example struct { + Date *time.Time `json:"date,omitempty" url:"date,omitempty" format:"date"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("omitempty with non-pointer zero value", func(t *testing.T) { + type enum string + + type example struct { + Enum enum `json:"enum,omitempty" url:"enum,omitempty"` + } + + values, err := QueryValues( + &example{}, + ) + require.NoError(t, err) + assert.Empty(t, values.Encode()) + }) + + t.Run("object array", func(t *testing.T) { + type object struct { + Key string `json:"key" url:"key"` + Value string `json:"value" url:"value"` + } + type example struct { + Objects []*object `json:"objects,omitempty" url:"objects,omitempty"` + } + + values, err := QueryValues( + &example{ + Objects: []*object{ + { + Key: "hello", + Value: "world", + }, + { + Key: "foo", + Value: "bar", + }, + }, + }, + ) + require.NoError(t, err) + assert.Equal(t, "objects%5Bkey%5D=hello&objects%5Bkey%5D=foo&objects%5Bvalue%5D=world&objects%5Bvalue%5D=bar", values.Encode()) + }) +} diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/retrier.go b/seed/go-sdk/file-upload/no-custom-config/internal/retrier.go new file mode 100644 index 00000000000..6040147154b --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/retrier.go @@ -0,0 +1,165 @@ +package internal + +import ( + "crypto/rand" + "math/big" + "net/http" + "time" +) + +const ( + defaultRetryAttempts = 2 + minRetryDelay = 500 * time.Millisecond + maxRetryDelay = 5000 * time.Millisecond +) + +// RetryOption adapts the behavior the *Retrier. +type RetryOption func(*retryOptions) + +// RetryFunc is a retriable HTTP function call (i.e. *http.Client.Do). +type RetryFunc func(*http.Request) (*http.Response, error) + +// WithMaxAttempts configures the maximum number of attempts +// of the *Retrier. +func WithMaxAttempts(attempts uint) RetryOption { + return func(opts *retryOptions) { + opts.attempts = attempts + } +} + +// Retrier retries failed requests a configurable number of times with an +// exponential back-off between each retry. +type Retrier struct { + attempts uint +} + +// NewRetrier constructs a new *Retrier with the given options, if any. +func NewRetrier(opts ...RetryOption) *Retrier { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + attempts := uint(defaultRetryAttempts) + if options.attempts > 0 { + attempts = options.attempts + } + return &Retrier{ + attempts: attempts, + } +} + +// Run issues the request and, upon failure, retries the request if possible. +// +// The request will be retried as long as the request is deemed retriable and the +// number of retry attempts has not grown larger than the configured retry limit. +func (r *Retrier) Run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + opts ...RetryOption, +) (*http.Response, error) { + options := new(retryOptions) + for _, opt := range opts { + opt(options) + } + maxRetryAttempts := r.attempts + if options.attempts > 0 { + maxRetryAttempts = options.attempts + } + var ( + retryAttempt uint + previousError error + ) + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt, + previousError, + ) +} + +func (r *Retrier) run( + fn RetryFunc, + request *http.Request, + errorDecoder ErrorDecoder, + maxRetryAttempts uint, + retryAttempt uint, + previousError error, +) (*http.Response, error) { + if retryAttempt >= maxRetryAttempts { + return nil, previousError + } + + // If the call has been cancelled, don't issue the request. + if err := request.Context().Err(); err != nil { + return nil, err + } + + response, err := fn(request) + if err != nil { + return nil, err + } + + if r.shouldRetry(response) { + defer response.Body.Close() + + delay, err := r.retryDelay(retryAttempt) + if err != nil { + return nil, err + } + + time.Sleep(delay) + + return r.run( + fn, + request, + errorDecoder, + maxRetryAttempts, + retryAttempt+1, + decodeError(response, errorDecoder), + ) + } + + return response, nil +} + +// shouldRetry returns true if the request should be retried based on the given +// response status code. +func (r *Retrier) shouldRetry(response *http.Response) bool { + return response.StatusCode == http.StatusTooManyRequests || + response.StatusCode == http.StatusRequestTimeout || + response.StatusCode >= http.StatusInternalServerError +} + +// retryDelay calculates the delay time in milliseconds based on the retry attempt. +func (r *Retrier) retryDelay(retryAttempt uint) (time.Duration, error) { + // Apply exponential backoff. + delay := minRetryDelay + minRetryDelay*time.Duration(retryAttempt*retryAttempt) + + // Do not allow the number to exceed maxRetryDelay. + if delay > maxRetryDelay { + delay = maxRetryDelay + } + + // Apply some itter by randomizing the value in the range of 75%-100%. + max := big.NewInt(int64(delay / 4)) + jitter, err := rand.Int(rand.Reader, max) + if err != nil { + return 0, err + } + + delay -= time.Duration(jitter.Int64()) + + // Never sleep less than the base sleep seconds. + if delay < minRetryDelay { + delay = minRetryDelay + } + + return delay, nil +} + +type retryOptions struct { + attempts uint +} diff --git a/seed/go-sdk/file-upload/internal/retrier_test.go b/seed/go-sdk/file-upload/no-custom-config/internal/retrier_test.go similarity index 100% rename from seed/go-sdk/file-upload/internal/retrier_test.go rename to seed/go-sdk/file-upload/no-custom-config/internal/retrier_test.go diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/stringer.go b/seed/go-sdk/file-upload/no-custom-config/internal/stringer.go new file mode 100644 index 00000000000..312801851e0 --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/stringer.go @@ -0,0 +1,13 @@ +package internal + +import "encoding/json" + +// StringifyJSON returns a pretty JSON string representation of +// the given value. +func StringifyJSON(value interface{}) (string, error) { + bytes, err := json.MarshalIndent(value, "", " ") + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/seed/go-sdk/file-upload/no-custom-config/internal/time.go b/seed/go-sdk/file-upload/no-custom-config/internal/time.go new file mode 100644 index 00000000000..ab0e269fade --- /dev/null +++ b/seed/go-sdk/file-upload/no-custom-config/internal/time.go @@ -0,0 +1,137 @@ +package internal + +import ( + "encoding/json" + "time" +) + +const dateFormat = "2006-01-02" + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date (e.g. 2006-01-02). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type Date struct { + t *time.Time +} + +// NewDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewDate(t time.Time) *Date { + return &Date{t: &t} +} + +// NewOptionalDate returns a new *Date. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDate(t *time.Time) *Date { + if t == nil { + return nil + } + return &Date{t: t} +} + +// Time returns the Date's underlying time, if any. If the +// date is nil, the zero value is returned. +func (d *Date) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the Date's underlying time.Time, if any. +func (d *Date) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *Date) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(dateFormat)) +} + +func (d *Date) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(dateFormat, raw) + if err != nil { + return err + } + + *d = Date{t: &parsedTime} + return nil +} + +// DateTime wraps time.Time and adapts its JSON representation +// to conform to a RFC3339 date-time (e.g. 2017-07-21T17:32:28Z). +// +// Ref: https://ijmacd.github.io/rfc3339-iso8601 +type DateTime struct { + t *time.Time +} + +// NewDateTime returns a new *DateTime. +func NewDateTime(t time.Time) *DateTime { + return &DateTime{t: &t} +} + +// NewOptionalDateTime returns a new *DateTime. If the given time.Time +// is nil, nil will be returned. +func NewOptionalDateTime(t *time.Time) *DateTime { + if t == nil { + return nil + } + return &DateTime{t: t} +} + +// Time returns the DateTime's underlying time, if any. If the +// date-time is nil, the zero value is returned. +func (d *DateTime) Time() time.Time { + if d == nil || d.t == nil { + return time.Time{} + } + return *d.t +} + +// TimePtr returns a pointer to the DateTime's underlying time.Time, if any. +func (d *DateTime) TimePtr() *time.Time { + if d == nil || d.t == nil { + return nil + } + if d.t.IsZero() { + return nil + } + return d.t +} + +func (d *DateTime) MarshalJSON() ([]byte, error) { + if d == nil || d.t == nil { + return nil, nil + } + return json.Marshal(d.t.Format(time.RFC3339)) +} + +func (d *DateTime) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + parsedTime, err := time.Parse(time.RFC3339, raw) + if err != nil { + return err + } + + *d = DateTime{t: &parsedTime} + return nil +} diff --git a/seed/go-sdk/file-upload/option/request_option.go b/seed/go-sdk/file-upload/no-custom-config/option/request_option.go similarity index 100% rename from seed/go-sdk/file-upload/option/request_option.go rename to seed/go-sdk/file-upload/no-custom-config/option/request_option.go diff --git a/seed/go-sdk/file-upload/pointer.go b/seed/go-sdk/file-upload/no-custom-config/pointer.go similarity index 100% rename from seed/go-sdk/file-upload/pointer.go rename to seed/go-sdk/file-upload/no-custom-config/pointer.go diff --git a/seed/go-sdk/file-upload/service.go b/seed/go-sdk/file-upload/no-custom-config/service.go similarity index 100% rename from seed/go-sdk/file-upload/service.go rename to seed/go-sdk/file-upload/no-custom-config/service.go diff --git a/seed/go-sdk/file-upload/service/client.go b/seed/go-sdk/file-upload/no-custom-config/service/client.go similarity index 94% rename from seed/go-sdk/file-upload/service/client.go rename to seed/go-sdk/file-upload/no-custom-config/service/client.go index 07bb9687f10..fb2c35f2abc 100644 --- a/seed/go-sdk/file-upload/service/client.go +++ b/seed/go-sdk/file-upload/no-custom-config/service/client.go @@ -254,17 +254,13 @@ func (c *Client) WithContentType( headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) writer := internal.NewMultipartWriter() - fileContentType := "application/octet-stream" - if contentTyped, ok := file.(internal.ContentTyped); ok { - fileContentType = contentTyped.ContentType() - } - if err := writer.WriteFile("file", file, internal.WithMultipartContentType(fileContentType)); err != nil { + if err := writer.WriteFile("file", file, internal.WithDefaultContentType("application/octet-stream")); err != nil { return err } if err := writer.WriteField("foo", fmt.Sprintf("%v", request.Foo)); err != nil { return err } - if err := writer.WriteJSON("bar", request.Bar, internal.WithMultipartContentType("application/json")); err != nil { + if err := writer.WriteJSON("bar", request.Bar, internal.WithDefaultContentType("application/json")); err != nil { return err } if err := writer.Close(); err != nil { diff --git a/seed/go-sdk/trace b/seed/go-sdk/file-upload/no-custom-config/snippet-templates.json similarity index 100% rename from seed/go-sdk/trace rename to seed/go-sdk/file-upload/no-custom-config/snippet-templates.json diff --git a/seed/go-sdk/file-upload/no-custom-config/snippet.json b/seed/go-sdk/file-upload/no-custom-config/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-sdk/folders/internal/multipart.go b/seed/go-sdk/folders/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/folders/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/folders/internal/multipart_test.go b/seed/go-sdk/folders/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/folders/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/go-content-type/internal/multipart.go b/seed/go-sdk/go-content-type/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/go-content-type/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/go-content-type/internal/multipart_test.go b/seed/go-sdk/go-content-type/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/go-content-type/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/idempotency-headers/internal/multipart.go b/seed/go-sdk/idempotency-headers/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/idempotency-headers/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/idempotency-headers/internal/multipart_test.go b/seed/go-sdk/idempotency-headers/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/idempotency-headers/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/imdb/internal/multipart.go b/seed/go-sdk/imdb/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/imdb/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/imdb/internal/multipart_test.go b/seed/go-sdk/imdb/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/imdb/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/license/internal/multipart.go b/seed/go-sdk/license/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/license/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/license/internal/multipart_test.go b/seed/go-sdk/license/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/license/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/literal/internal/multipart.go b/seed/go-sdk/literal/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/literal/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/literal/internal/multipart_test.go b/seed/go-sdk/literal/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/literal/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/mixed-case/internal/multipart.go b/seed/go-sdk/mixed-case/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/mixed-case/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/mixed-case/internal/multipart_test.go b/seed/go-sdk/mixed-case/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/mixed-case/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/mixed-file-directory/internal/multipart.go b/seed/go-sdk/mixed-file-directory/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/mixed-file-directory/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/mixed-file-directory/internal/multipart_test.go b/seed/go-sdk/mixed-file-directory/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/mixed-file-directory/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/multi-line-docs/internal/multipart.go b/seed/go-sdk/multi-line-docs/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/multi-line-docs/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/multi-line-docs/internal/multipart_test.go b/seed/go-sdk/multi-line-docs/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/multi-line-docs/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/multi-url-environment-no-default/internal/multipart.go b/seed/go-sdk/multi-url-environment-no-default/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/multi-url-environment-no-default/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/multi-url-environment-no-default/internal/multipart_test.go b/seed/go-sdk/multi-url-environment-no-default/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/multi-url-environment-no-default/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/multi-url-environment/internal/multipart.go b/seed/go-sdk/multi-url-environment/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/multi-url-environment/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/multi-url-environment/internal/multipart_test.go b/seed/go-sdk/multi-url-environment/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/multi-url-environment/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/no-environment/internal/multipart.go b/seed/go-sdk/no-environment/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/no-environment/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/no-environment/internal/multipart_test.go b/seed/go-sdk/no-environment/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/no-environment/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/oauth-client-credentials-default/internal/multipart.go b/seed/go-sdk/oauth-client-credentials-default/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/oauth-client-credentials-default/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/oauth-client-credentials-default/internal/multipart_test.go b/seed/go-sdk/oauth-client-credentials-default/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/oauth-client-credentials-default/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart.go b/seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart_test.go b/seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/oauth-client-credentials-environment-variables/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart.go b/seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart_test.go b/seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/oauth-client-credentials-nested-root/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/oauth-client-credentials/internal/multipart.go b/seed/go-sdk/oauth-client-credentials/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/oauth-client-credentials/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/oauth-client-credentials/internal/multipart_test.go b/seed/go-sdk/oauth-client-credentials/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/oauth-client-credentials/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/object/internal/multipart.go b/seed/go-sdk/object/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/object/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/object/internal/multipart_test.go b/seed/go-sdk/object/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/object/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/objects-with-imports/internal/multipart.go b/seed/go-sdk/objects-with-imports/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/objects-with-imports/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/objects-with-imports/internal/multipart_test.go b/seed/go-sdk/objects-with-imports/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/objects-with-imports/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/optional/internal/multipart.go b/seed/go-sdk/optional/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/optional/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/optional/internal/multipart_test.go b/seed/go-sdk/optional/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/optional/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/package-yml/internal/multipart.go b/seed/go-sdk/package-yml/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/package-yml/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/package-yml/internal/multipart_test.go b/seed/go-sdk/package-yml/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/package-yml/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/pagination/internal/multipart.go b/seed/go-sdk/pagination/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/pagination/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/pagination/internal/multipart_test.go b/seed/go-sdk/pagination/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/pagination/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/plain-text/internal/multipart.go b/seed/go-sdk/plain-text/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/plain-text/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/plain-text/internal/multipart_test.go b/seed/go-sdk/plain-text/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/plain-text/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/query-parameters/internal/multipart.go b/seed/go-sdk/query-parameters/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/query-parameters/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/query-parameters/internal/multipart_test.go b/seed/go-sdk/query-parameters/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/query-parameters/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/response-property/internal/multipart.go b/seed/go-sdk/response-property/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/response-property/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/response-property/internal/multipart_test.go b/seed/go-sdk/response-property/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/response-property/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/seed.yml b/seed/go-sdk/seed.yml index d2a3aa4e903..f418ec4696e 100644 --- a/seed/go-sdk/seed.yml +++ b/seed/go-sdk/seed.yml @@ -52,6 +52,17 @@ fixtures: union: v1 module: path: github.com/fern-api/unions-go + file-upload: + - outputFolder: no-custom-config + customConfig: null + - outputFolder: inline-file-properties + outputVersion: 0.0.1 + customConfig: + packageName: upload + union: v1 + module: + path: github.com/fern-api/file-upload-go + inlineFileProperties: true streaming: - outputFolder: . outputVersion: v2.0.0 @@ -78,4 +89,4 @@ allowedFailures: - reserved-keywords - server-sent-events - streaming-parameter - - trace \ No newline at end of file + - trace diff --git a/seed/go-sdk/server-sent-event-examples/internal/multipart.go b/seed/go-sdk/server-sent-event-examples/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/server-sent-event-examples/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/server-sent-event-examples/internal/multipart_test.go b/seed/go-sdk/server-sent-event-examples/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/server-sent-event-examples/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/server-sent-events/internal/multipart.go b/seed/go-sdk/server-sent-events/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/server-sent-events/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/server-sent-events/internal/multipart_test.go b/seed/go-sdk/server-sent-events/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/server-sent-events/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/simple-fhir/internal/multipart.go b/seed/go-sdk/simple-fhir/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/simple-fhir/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/simple-fhir/internal/multipart_test.go b/seed/go-sdk/simple-fhir/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/simple-fhir/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/single-url-environment-default/internal/multipart.go b/seed/go-sdk/single-url-environment-default/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/single-url-environment-default/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/single-url-environment-default/internal/multipart_test.go b/seed/go-sdk/single-url-environment-default/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/single-url-environment-default/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/single-url-environment-no-default/internal/multipart.go b/seed/go-sdk/single-url-environment-no-default/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/single-url-environment-no-default/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/single-url-environment-no-default/internal/multipart_test.go b/seed/go-sdk/single-url-environment-no-default/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/single-url-environment-no-default/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/streaming/internal/multipart.go b/seed/go-sdk/streaming/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/streaming/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/streaming/internal/multipart_test.go b/seed/go-sdk/streaming/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/streaming/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/undiscriminated-unions/internal/multipart.go b/seed/go-sdk/undiscriminated-unions/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/undiscriminated-unions/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/undiscriminated-unions/internal/multipart_test.go b/seed/go-sdk/undiscriminated-unions/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/undiscriminated-unions/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/unions/internal/multipart.go b/seed/go-sdk/unions/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/unions/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/unions/internal/multipart_test.go b/seed/go-sdk/unions/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/unions/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/unknown/internal/multipart.go b/seed/go-sdk/unknown/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/unknown/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/unknown/internal/multipart_test.go b/seed/go-sdk/unknown/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/unknown/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/validation/internal/multipart.go b/seed/go-sdk/validation/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/validation/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/validation/internal/multipart_test.go b/seed/go-sdk/validation/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/validation/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/variables/internal/multipart.go b/seed/go-sdk/variables/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/variables/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/variables/internal/multipart_test.go b/seed/go-sdk/variables/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/variables/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/version-no-default/internal/multipart.go b/seed/go-sdk/version-no-default/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/version-no-default/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/version-no-default/internal/multipart_test.go b/seed/go-sdk/version-no-default/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/version-no-default/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/version/internal/multipart.go b/seed/go-sdk/version/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/version/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/version/internal/multipart_test.go b/seed/go-sdk/version/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/version/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/websocket/internal/multipart.go b/seed/go-sdk/websocket/internal/multipart.go deleted file mode 100644 index bb58a62ef57..00000000000 --- a/seed/go-sdk/websocket/internal/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package internal - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/websocket/internal/multipart_test.go b/seed/go-sdk/websocket/internal/multipart_test.go deleted file mode 100644 index 69bba7f90ad..00000000000 --- a/seed/go-sdk/websocket/internal/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package internal - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -}