diff --git a/fern/pages/changelogs/go-sdk/2024-10-25.mdx b/fern/pages/changelogs/go-sdk/2024-10-25.mdx new file mode 100644 index 00000000000..018ab7ee523 --- /dev/null +++ b/fern/pages/changelogs/go-sdk/2024-10-25.mdx @@ -0,0 +1,4 @@ +## 0.28.0 +**`(feat):`** Add support for the exportedClientName configuration, which can be used to customize the generated client name and constructor included in snippets. +Note that this configuration option assumes that the SDK includes a hand-written client constructor defined in the client package. + diff --git a/generators/go/cmd/fern-go-fiber/main.go b/generators/go/cmd/fern-go-fiber/main.go index 78203dc3ad5..8bbdec2101a 100644 --- a/generators/go/cmd/fern-go-fiber/main.go +++ b/generators/go/cmd/fern-go-fiber/main.go @@ -35,6 +35,7 @@ func run(config *cmd.Config, coordinator *coordinator.Client) ([]*generator.File config.SnippetFilepath, config.ImportPath, config.PackageName, + config.ExportedClientName, config.UnionVersion, config.Module, ) diff --git a/generators/go/cmd/fern-go-model/main.go b/generators/go/cmd/fern-go-model/main.go index f2abc2b8bd7..efb90e34058 100644 --- a/generators/go/cmd/fern-go-model/main.go +++ b/generators/go/cmd/fern-go-model/main.go @@ -35,6 +35,7 @@ func run(config *cmd.Config, coordinator *coordinator.Client) ([]*generator.File config.SnippetFilepath, config.ImportPath, config.PackageName, + config.ExportedClientName, config.UnionVersion, config.Module, ) diff --git a/generators/go/cmd/fern-go-sdk/main.go b/generators/go/cmd/fern-go-sdk/main.go index ffe3ff5b39e..f85695ffbc0 100644 --- a/generators/go/cmd/fern-go-sdk/main.go +++ b/generators/go/cmd/fern-go-sdk/main.go @@ -35,6 +35,7 @@ func run(config *cmd.Config, coordinator *coordinator.Client) ([]*generator.File config.SnippetFilepath, config.ImportPath, config.PackageName, + config.ExportedClientName, config.UnionVersion, config.Module, ) diff --git a/generators/go/internal/cmd/cmd.go b/generators/go/internal/cmd/cmd.go index 1fb428e74a5..924ae62f4cb 100644 --- a/generators/go/internal/cmd/cmd.go +++ b/generators/go/internal/cmd/cmd.go @@ -63,6 +63,7 @@ type Config struct { IrFilepath string SnippetFilepath string ImportPath string + ExportedClientName string PackageName string UnionVersion string Module *generator.ModuleConfig @@ -211,6 +212,7 @@ func newConfig(configFilename string) (*Config, error) { SnippetFilepath: snippetFilepath, ImportPath: customConfig.ImportPath, PackageName: customConfig.PackageName, + ExportedClientName: customConfig.ExportedClientName, UnionVersion: customConfig.UnionVersion, Module: moduleConfig, Writer: writerConfig, @@ -257,6 +259,7 @@ type customConfig struct { AlwaysSendRequiredProperties bool `json:"alwaysSendRequiredProperties,omitempty"` ImportPath string `json:"importPath,omitempty"` PackageName string `json:"packageName,omitempty"` + ExportedClientName string `json:"exportedClientName,omitempty"` UnionVersion string `json:"union,omitempty"` Module *moduleConfig `json:"module,omitempty"` } diff --git a/generators/go/internal/generator/config.go b/generators/go/internal/generator/config.go index 405324818d1..4770b6287bd 100644 --- a/generators/go/internal/generator/config.go +++ b/generators/go/internal/generator/config.go @@ -25,8 +25,9 @@ type Config struct { IRFilepath string SnippetFilepath string ImportPath string - UnionVersion UnionVersion PackageName string + ExportedClientName string + UnionVersion UnionVersion // If not specified, a go.mod and go.sum will not be generated. ModuleConfig *ModuleConfig @@ -62,6 +63,7 @@ func NewConfig( snippetFilepath string, importPath string, packageName string, + exportedClientName string, unionVersion string, moduleConfig *ModuleConfig, ) (*Config, error) { @@ -82,6 +84,7 @@ func NewConfig( SnippetFilepath: snippetFilepath, ImportPath: importPath, PackageName: packageName, + ExportedClientName: exportedClientName, UnionVersion: uv, ModuleConfig: moduleConfig, }, nil diff --git a/generators/go/internal/generator/generator.go b/generators/go/internal/generator/generator.go index 6bf886241ad..277700f0577 100644 --- a/generators/go/internal/generator/generator.go +++ b/generators/go/internal/generator/generator.go @@ -19,6 +19,9 @@ import ( const ( // packageDocsFilename represents the standard package documentation filename. packageDocsFilename = "doc.go" + + // defaultExportedClientName is the default name for the generated client. + defaultExportedClientName = "Client" ) // Mode is an enum for different generator modes (i.e. types, client, etc). @@ -189,6 +192,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( } } } + exportedClientName := getExportedClientName(ir, g.config.ExportedClientName) rootPackageName := getRootPackageName(ir, g.config.PackageName) cycleInfo, err := cycleInfoFromIR(ir, g.config.ImportPath) if err != nil { @@ -370,6 +374,7 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( g.config.ImportPath, generatedAuth, generatedEnvironment, + exportedClientName, ) if len(ir.IdempotencyHeaders) > 0 { fileInfo = fileInfoForIdempotentRequestOptionsDefinition() @@ -1491,6 +1496,15 @@ func getRootPackageName(ir *fernir.IntermediateRepresentation, packageNameOverri return strings.ToLower(ir.ApiName.CamelCase.SafeName) } +// getExportedClientName returns the exported client name. This is configurable so that +// users can customize how snippets are rendered, but it has no impact on the generated code. +func getExportedClientName(ir *fernir.IntermediateRepresentation, exportedClientNameOverride string) string { + if exportedClientNameOverride != "" { + return exportedClientNameOverride + } + return defaultExportedClientName +} + // generatorexecEndpointSnippetToString returns the string representation of the given // endpoint snippet. // diff --git a/generators/go/internal/generator/sdk.go b/generators/go/internal/generator/sdk.go index 4f102ab58b6..8d4790be7ad 100644 --- a/generators/go/internal/generator/sdk.go +++ b/generators/go/internal/generator/sdk.go @@ -1974,6 +1974,7 @@ func generatedClientInstantiation( baseImportPath string, generatedAuth *GeneratedAuth, generatedEnvironment *GeneratedEnvironment, + exportedClientName string, ) *ast.AssignStmt { var parameters []ast.Expr if generatedAuth != nil { @@ -1989,7 +1990,7 @@ func generatedClientInstantiation( Right: []ast.Expr{ ast.NewCallExpr( ast.NewImportedReference( - "NewClient", + fmt.Sprintf("New%s", exportedClientName), packagePathToImportPath(baseImportPath, packagePathForClient(new(ir.FernFilepath))), ), parameters, diff --git a/generators/go/sdk/versions.yml b/generators/go/sdk/versions.yml index 9da459e2a9e..4f96fe09cb5 100644 --- a/generators/go/sdk/versions.yml +++ b/generators/go/sdk/versions.yml @@ -1,3 +1,13 @@ +- version: 0.28.0 + changelogEntry: + - type: feat + summary: >- + Add support for the exportedClientName configuration, which can be used to customize + the generated client name and constructor included in snippets. + + Note that this configuration option assumes that the SDK includes a hand-written client + constructor defined in the client package. + irVersion: 40 - version: 0.27.0 changelogEntry: - type: feat diff --git a/seed/go-sdk/examples/exported-client-name/.github/workflows/ci.yml b/seed/go-sdk/examples/exported-client-name/.github/workflows/ci.yml new file mode 100644 index 00000000000..d4c0a5dcd95 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.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/examples/exported-client-name/.mock/definition/__package__.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/__package__.yml new file mode 100644 index 00000000000..5f3e6bfe1b1 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/__package__.yml @@ -0,0 +1,42 @@ +types: + Type: + discriminated: false + union: + - BasicType + - ComplexType + + Identifier: + properties: + type: Type + value: string + label: string + + BasicType: + enum: + - name: Primitive + value: primitive + - name: Literal + value: literal + + ComplexType: + enum: + - name: Object + value: object + - name: Union + value: union + - name: unknown + value: unknown + +service: + auth: false + base-path: / + endpoints: + echo: + method: POST + path: "" + request: string + response: string + examples: + - request: Hello world!\n\nwith\n\tnewlines + response: + body: Hello world!\n\nwith\n\tnewlines diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/api.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/api.yml new file mode 100644 index 00000000000..3f3cb4540de --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/api.yml @@ -0,0 +1,8 @@ +name: examples +auth: bearer +error-discrimination: + strategy: status-code +environments: + Production: https://production.com/api + Staging: https://staging.com/api +default-environment: null diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/commons/types.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/commons/types.yml new file mode 100644 index 00000000000..33f95a4ab1b --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/commons/types.yml @@ -0,0 +1,43 @@ +types: + Tag: + type: string + examples: + - name: One + value: tag-wf9as23d + + Metadata: + properties: + id: string + data: optional> + jsonString: optional + examples: + - name: One + value: + id: metadata-js8dg24b + data: + foo: bar + baz: qux + jsonString: '{"foo": "bar", "baz": "qux"}' + + EventInfo: + union: + metadata: Metadata + tag: Tag + examples: + - name: Metadata + value: + type: metadata + id: metadata-alskjfg8 + data: + one: two + jsonString: '{"one": "two"}' + + Data: + union: + string: string + base64: base64 + examples: + - name: String + value: + type: string + value: data diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/file/notification/service.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/file/notification/service.yml new file mode 100644 index 00000000000..7f518565dcf --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/file/notification/service.yml @@ -0,0 +1,18 @@ +imports: + types: ../../types.yml + +service: + auth: true + base-path: /file/notification/{notificationId} + path-parameters: + notificationId: string + endpoints: + getException: + method: GET + path: "" + response: types.Exception + examples: + - path-parameters: + notificationId: notification-hsy129x + response: + body: $types.Exception.One diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/file/service.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/file/service.yml new file mode 100644 index 00000000000..349d80d6543 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/file/service.yml @@ -0,0 +1,37 @@ +imports: + types: ../types.yml + +types: + Filename: + type: string + examples: + - name: Example0 + value: file.txt + +service: + auth: true + base-path: /file + headers: + X-File-API-Version: string + endpoints: + getFile: + docs: "This endpoint returns a file by its name." + method: GET + path: /{filename} + path-parameters: + filename: + type: string + docs: This is a filename + request: + name: GetFileRequest + response: types.File + errors: + - types.NotFoundError + examples: + - path-parameters: + filename: $Filename.Example0 + headers: + X-File-API-Version: 0.0.2 + response: + error: types.NotFoundError + body: A file with that name was not found! diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/health/service.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/health/service.yml new file mode 100644 index 00000000000..46ba829d120 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/health/service.yml @@ -0,0 +1,32 @@ +imports: + types: ../types.yml + +service: + auth: true + base-path: / + endpoints: + check: + docs: "This endpoint checks the health of a resource." + method: GET + path: /check/{id} + path-parameters: + id: + type: string + docs: The id to check + examples: + - name: Example0 + path-parameters: + id: id-2sdx82h + - name: Example2 + path-parameters: + id: id-3tey93i + + ping: + docs: "This endpoint checks the health of the service." + method: GET + path: /ping + response: boolean + examples: + - name: Example0 + response: + body: true diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/service.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/service.yml new file mode 100644 index 00000000000..dbf858df8a2 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/service.yml @@ -0,0 +1,58 @@ +imports: + types: types.yml + +service: + auth: false + base-path: / + endpoints: + getMovie: + method: GET + path: /movie/{movieId} + path-parameters: + movieId: types.MovieId + response: types.Movie + examples: + - path-parameters: + movieId: $types.MovieId.One + response: + body: $types.Movie.One + + createMovie: + method: POST + path: /movie + request: types.Movie + response: types.MovieId + examples: + - request: $types.Movie.One + response: + body: $types.MovieId.One + + getMetadata: + method: GET + path: /metadata + request: + name: GetMetadataRequest + query-parameters: + shallow: optional + tag: + type: optional + allow-multiple: true + headers: + X-API-Version: string + response: types.Metadata + examples: + - query-parameters: + shallow: false + tag: development + headers: + X-API-Version: 0.0.1 + response: + body: $types.Metadata.One + + getResponse: + method: POST + path: /response + response: types.Response + examples: + - response: + body: $types.Response.String \ No newline at end of file diff --git a/seed/go-sdk/examples/exported-client-name/.mock/definition/types.yml b/seed/go-sdk/examples/exported-client-name/.mock/definition/types.yml new file mode 100644 index 00000000000..cc1a9b845ba --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/definition/types.yml @@ -0,0 +1,298 @@ +imports: + commons: commons/types.yml + root: __package__.yml + +errors: + NotFoundError: + status-code: 404 + type: string + +types: + MovieId: + type: string + examples: + - name: One + value: movie-c06a4ad7 + + Movie: + properties: + id: MovieId + prequel: optional + title: string + from: string + rating: + type: double + docs: The rating scale is one to five stars + type: literal<"movie"> + tag: commons.Tag + book: optional + metadata: map + revenue: long + examples: + - name: One + value: + id: $MovieId.One + prequel: "movie-cv9b914f" + title: The Boy and the Heron + from: Hayao Miyazaki + rating: 8.0 + type: movie + tag: $commons.Tag.One + metadata: + actors: + - Christian Bale + - Florence Pugh + - Willem Dafoe + releaseDate: "2023-12-08" + ratings: + rottenTomatoes: 97 + imdb: 7.6 + revenue: 1000000 + + CastMember: + discriminated: false + union: + - Actor + - Actress + - StuntDouble + examples: + - name: Example0 + value: + id: actor_123 + name: "Brad Pitt" + - name: Example1 + value: $Actress.Example0 + + Actor: + properties: + name: string + id: string + + Actress: + properties: + name: string + id: string + examples: + - name: Example0 + value: + name: Jennifer Lawrence + id: actor_456 + + StuntDouble: + properties: + name: string + actorOrActressId: string + + ExtendedMovie: + extends: Movie + properties: + cast: list + examples: + - value: + id: movie-sda231x + title: Pulp Fiction + from: Quentin Tarantino + rating: 8.5 + type: movie + tag: tag-12efs9dv + cast: + - John Travolta + - Samuel L. Jackson + - Uma Thurman + - Bruce Willis + metadata: + academyAward: true + releaseDate: "2023-12-08" + ratings: + rottenTomatoes: 97 + imdb: 7.6 + revenue: 1000000 + + Moment: + properties: + id: uuid + date: date + datetime: datetime + examples: + - value: + id: 656f12d6-f592-444c-a1d3-a3cfd46d5b39 + date: 1994-01-01 + datetime: 1994-01-01T01:01:01Z + + File: + properties: + name: string + contents: string + examples: + - name: One + value: + name: file.txt + contents: ... + - name: Two + value: + name: another_file.txt + contents: ... + + Directory: + properties: + name: string + files: optional> + directories: optional> + examples: + - name: One + value: + name: root + files: + - $File.One + directories: + - name: tmp + files: + - $File.Two + + Node: + properties: + name: string + nodes: optional> + trees: optional> + examples: + - name: Tree + value: + name: root + nodes: + - $Node.Left + - $Node.Right + trees: + - $Tree.Root + - name: Left + value: + name: left + - name: Right + value: + name: right + + Tree: + properties: + nodes: optional> + examples: + - name: Root + value: + nodes: + - $Node.Left + - $Node.Right + + Metadata: + base-properties: + extra: map + tags: set + union: + html: string + markdown: string + examples: + - name: One + value: + type: html + extra: + version: 0.0.1 + tenancy: test + tags: + - development + - public + value: ... + + Exception: + union: + generic: ExceptionInfo + timeout: {} + examples: + - name: One + value: + type: generic + exceptionType: Unavailable + exceptionMessage: This component is unavailable! + exceptionStacktrace: + + ExceptionInfo: + properties: + exceptionType: string + exceptionMessage: string + exceptionStacktrace: string + examples: + - name: One + value: + exceptionType: Unavailable + exceptionMessage: This component is unavailable! + exceptionStacktrace: + + MigrationStatus: + enum: + - value: RUNNING + docs: The migration is running. + - value: FAILED + docs: The migration failed. + - FINISHED + examples: + - name: Running + value: RUNNING + - name: Failed + value: FAILED + + Migration: + properties: + name: string + status: MigrationStatus + examples: + - value: + name: 001_init + status: $MigrationStatus.Running + + Request: + properties: + request: unknown + examples: + - name: Empty + value: + request: {} + + Response: + properties: + response: unknown + identifiers: list + examples: + - name: String + value: + response: "Initializing..." + identifiers: + - type: primitive + value: 'example' + label: Primitive + - type: unknown + value: '{}' + label: Unknown + + ResponseType: + properties: + type: root.Type + + Test: + union: + and: boolean + or: boolean + examples: + - name: And + value: + type: and + value: true + - name: Or + value: + type: or + value: true + + Entity: + properties: + type: root.Type + name: string + examples: + - name: One + value: + type: unknown + name: unknown diff --git a/seed/go-sdk/examples/exported-client-name/.mock/fern.config.json b/seed/go-sdk/examples/exported-client-name/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/go-sdk/examples/exported-client-name/.mock/generators.yml b/seed/go-sdk/examples/exported-client-name/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/go-sdk/examples/exported-client-name/client/client.go b/seed/go-sdk/examples/exported-client-name/client/client.go new file mode 100644 index 00000000000..a63457ff3e1 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/client/client.go @@ -0,0 +1,78 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + context "context" + core "github.com/examples/fern/core" + fileclient "github.com/examples/fern/file/client" + healthclient "github.com/examples/fern/health/client" + option "github.com/examples/fern/option" + service "github.com/examples/fern/service" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + File *fileclient.Client + Health *healthclient.Client + Service *service.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + File: fileclient.NewClient(opts...), + Health: healthclient.NewClient(opts...), + Service: service.NewClient(opts...), + } +} + +func (c *Client) Echo( + ctx context.Context, + request string, + opts ...option.RequestOption, +) (string, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response string + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ); err != nil { + return "", err + } + return response, nil +} diff --git a/seed/go-sdk/examples/exported-client-name/client/client_test.go b/seed/go-sdk/examples/exported-client-name/client/client_test.go new file mode 100644 index 00000000000..ace2a08796d --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/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/examples/fern/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/examples/exported-client-name/commons/types.go b/seed/go-sdk/examples/exported-client-name/commons/types.go new file mode 100644 index 00000000000..8ac5b836e91 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/commons/types.go @@ -0,0 +1,219 @@ +// This file was auto-generated by Fern from our API Definition. + +package commons + +import ( + json "encoding/json" + fmt "fmt" + core "github.com/examples/fern/core" +) + +type Data struct { + Type string + String string + Base64 []byte +} + +func NewDataFromString(value string) *Data { + return &Data{Type: "string", String: value} +} + +func NewDataFromBase64(value []byte) *Data { + return &Data{Type: "base64", Base64: value} +} + +func (d *Data) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + d.Type = unmarshaler.Type + if unmarshaler.Type == "" { + return fmt.Errorf("%T did not include discriminant type", d) + } + switch unmarshaler.Type { + case "string": + var valueUnmarshaler struct { + String string `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + d.String = valueUnmarshaler.String + case "base64": + var valueUnmarshaler struct { + Base64 []byte `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + d.Base64 = valueUnmarshaler.Base64 + } + return nil +} + +func (d Data) MarshalJSON() ([]byte, error) { + switch d.Type { + default: + return nil, fmt.Errorf("invalid type %s in %T", d.Type, d) + case "string": + var marshaler = struct { + Type string `json:"type"` + String string `json:"value"` + }{ + Type: "string", + String: d.String, + } + return json.Marshal(marshaler) + case "base64": + var marshaler = struct { + Type string `json:"type"` + Base64 []byte `json:"value"` + }{ + Type: "base64", + Base64: d.Base64, + } + return json.Marshal(marshaler) + } +} + +type DataVisitor interface { + VisitString(string) error + VisitBase64([]byte) error +} + +func (d *Data) Accept(visitor DataVisitor) error { + switch d.Type { + default: + return fmt.Errorf("invalid type %s in %T", d.Type, d) + case "string": + return visitor.VisitString(d.String) + case "base64": + return visitor.VisitBase64(d.Base64) + } +} + +type EventInfo struct { + Type string + Metadata *Metadata + Tag Tag +} + +func NewEventInfoFromMetadata(value *Metadata) *EventInfo { + return &EventInfo{Type: "metadata", Metadata: value} +} + +func NewEventInfoFromTag(value Tag) *EventInfo { + return &EventInfo{Type: "tag", Tag: value} +} + +func (e *EventInfo) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + e.Type = unmarshaler.Type + if unmarshaler.Type == "" { + return fmt.Errorf("%T did not include discriminant type", e) + } + switch unmarshaler.Type { + case "metadata": + value := new(Metadata) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + e.Metadata = value + case "tag": + var valueUnmarshaler struct { + Tag Tag `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + e.Tag = valueUnmarshaler.Tag + } + return nil +} + +func (e EventInfo) MarshalJSON() ([]byte, error) { + switch e.Type { + default: + return nil, fmt.Errorf("invalid type %s in %T", e.Type, e) + case "metadata": + return core.MarshalJSONWithExtraProperty(e.Metadata, "type", "metadata") + case "tag": + var marshaler = struct { + Type string `json:"type"` + Tag Tag `json:"value"` + }{ + Type: "tag", + Tag: e.Tag, + } + return json.Marshal(marshaler) + } +} + +type EventInfoVisitor interface { + VisitMetadata(*Metadata) error + VisitTag(Tag) error +} + +func (e *EventInfo) Accept(visitor EventInfoVisitor) error { + switch e.Type { + default: + return fmt.Errorf("invalid type %s in %T", e.Type, e) + case "metadata": + return visitor.VisitMetadata(e.Metadata) + case "tag": + return visitor.VisitTag(e.Tag) + } +} + +type Metadata struct { + Id string `json:"id" url:"id"` + Data map[string]string `json:"data,omitempty" url:"data,omitempty"` + JsonString *string `json:"jsonString,omitempty" url:"jsonString,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *Metadata) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Metadata) UnmarshalJSON(data []byte) error { + type unmarshaler Metadata + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = Metadata(value) + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *Metadata) String() string { + if len(m._rawJSON) > 0 { + if value, err := core.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} + +type Tag = string diff --git a/seed/go-sdk/examples/exported-client-name/core/core.go b/seed/go-sdk/examples/exported-client-name/core/core.go new file mode 100644 index 00000000000..6b5a8f3c011 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/core.go @@ -0,0 +1,321 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "reflect" + "strings" +) + +const ( + // contentType specifies the JSON Content-Type header value. + contentType = "application/json" + contentTypeHeader = "Content-Type" +) + +// 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 +} + +// WriteMultipartJSON writes the given value as a JSON part. +// This is used to serialize non-primitive multipart properties +// (i.e. lists, objects, etc). +func WriteMultipartJSON(writer *multipart.Writer, field string, value interface{}) error { + bytes, err := json.Marshal(value) + if err != nil { + return err + } + return writer.WriteField(field, string(bytes)) +} + +// 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()) +} + +// ErrorDecoder decodes *http.Response errors and returns a +// typed API error (e.g. *APIError). +type ErrorDecoder func(statusCode int, body io.Reader) error + +// Caller calls APIs and deserializes their response, if any. +type Caller struct { + client HTTPClient + retrier *Retrier +} + +// CallerParams represents the parameters used to constrcut a new *Caller. +type CallerParams struct { + Client HTTPClient + MaxAttempts uint +} + +// NewCaller returns a new *Caller backed by the given parameters. +func NewCaller(params *CallerParams) *Caller { + var httpClient 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 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 NewAPIError(response.StatusCode, nil) + } + return 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/examples/exported-client-name/core/core_test.go b/seed/go-sdk/examples/exported-client-name/core/core_test.go new file mode 100644 index 00000000000..e6eaef3a86a --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/core_test.go @@ -0,0 +1,390 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strconv" + "testing" + + "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 { + *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: 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: 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: 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: &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 = 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/examples/exported-client-name/core/extra_properties.go b/seed/go-sdk/examples/exported-client-name/core/extra_properties.go new file mode 100644 index 00000000000..a6af3e12410 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/extra_properties.go @@ -0,0 +1,141 @@ +package core + +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/examples/exported-client-name/core/extra_properties_test.go b/seed/go-sdk/examples/exported-client-name/core/extra_properties_test.go new file mode 100644 index 00000000000..dc66fccd7f1 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/extra_properties_test.go @@ -0,0 +1,228 @@ +package core + +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/examples/exported-client-name/core/query.go b/seed/go-sdk/examples/exported-client-name/core/query.go new file mode 100644 index 00000000000..2670ff7feda --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/query.go @@ -0,0 +1,231 @@ +package core + +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/examples/exported-client-name/core/query_test.go b/seed/go-sdk/examples/exported-client-name/core/query_test.go new file mode 100644 index 00000000000..5498fa92aa5 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/query_test.go @@ -0,0 +1,187 @@ +package core + +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/examples/exported-client-name/core/request_option.go b/seed/go-sdk/examples/exported-client-name/core/request_option.go new file mode 100644 index 00000000000..94c9c735344 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/request_option.go @@ -0,0 +1,124 @@ +// 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 + Token string +} + +// 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 { + header := r.cloneHeader() + if r.Token != "" { + header.Set("Authorization", "Bearer "+r.Token) + } + return header +} + +func (r *RequestOptions) cloneHeader() http.Header { + headers := r.HTTPHeader.Clone() + headers.Set("X-Fern-Language", "Go") + headers.Set("X-Fern-SDK-Name", "github.com/examples/fern") + 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 +} + +// TokenOption implements the RequestOption interface. +type TokenOption struct { + Token string +} + +func (t *TokenOption) applyRequestOptions(opts *RequestOptions) { + opts.Token = t.Token +} diff --git a/seed/go-sdk/examples/exported-client-name/core/retrier.go b/seed/go-sdk/examples/exported-client-name/core/retrier.go new file mode 100644 index 00000000000..ea24916b786 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/retrier.go @@ -0,0 +1,166 @@ +package core + +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.StatusConflict || + 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/examples/exported-client-name/core/stringer.go b/seed/go-sdk/examples/exported-client-name/core/stringer.go new file mode 100644 index 00000000000..000cf448641 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/stringer.go @@ -0,0 +1,13 @@ +package core + +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/examples/exported-client-name/core/time.go b/seed/go-sdk/examples/exported-client-name/core/time.go new file mode 100644 index 00000000000..d009ab30c90 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/core/time.go @@ -0,0 +1,137 @@ +package core + +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/examples/exported-client-name/environments.go b/seed/go-sdk/examples/exported-client-name/environments.go new file mode 100644 index 00000000000..e46c475de38 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/environments.go @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +package examples + +// Environments defines all of the API environments. +// These values can be used with the WithBaseURL +// RequestOption to override the client's default environment, +// if any. +var Environments = struct { + Production string + Staging string +}{ + Production: "https://production.com/api", + Staging: "https://staging.com/api", +} diff --git a/seed/go-sdk/examples/exported-client-name/errors.go b/seed/go-sdk/examples/exported-client-name/errors.go new file mode 100644 index 00000000000..e9063f8f7f3 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/errors.go @@ -0,0 +1,31 @@ +// This file was auto-generated by Fern from our API Definition. + +package examples + +import ( + json "encoding/json" + core "github.com/examples/fern/core" +) + +type NotFoundError struct { + *core.APIError + Body string +} + +func (n *NotFoundError) UnmarshalJSON(data []byte) error { + var body string + if err := json.Unmarshal(data, &body); err != nil { + return err + } + n.StatusCode = 404 + n.Body = body + return nil +} + +func (n *NotFoundError) MarshalJSON() ([]byte, error) { + return json.Marshal(n.Body) +} + +func (n *NotFoundError) Unwrap() error { + return n.APIError +} diff --git a/seed/go-sdk/examples/exported-client-name/file/client/client.go b/seed/go-sdk/examples/exported-client-name/file/client/client.go new file mode 100644 index 00000000000..57acb00ff8b --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/file/client/client.go @@ -0,0 +1,36 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + core "github.com/examples/fern/core" + notificationclient "github.com/examples/fern/file/notification/client" + service "github.com/examples/fern/file/service" + option "github.com/examples/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + Notification *notificationclient.Client + Service *service.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Notification: notificationclient.NewClient(opts...), + Service: service.NewClient(opts...), + } +} diff --git a/seed/go-sdk/examples/exported-client-name/file/notification/client/client.go b/seed/go-sdk/examples/exported-client-name/file/notification/client/client.go new file mode 100644 index 00000000000..045636bb64d --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/file/notification/client/client.go @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + core "github.com/examples/fern/core" + service "github.com/examples/fern/file/notification/service" + option "github.com/examples/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + Service *service.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Service: service.NewClient(opts...), + } +} diff --git a/seed/go-sdk/examples/exported-client-name/file/notification/service/client.go b/seed/go-sdk/examples/exported-client-name/file/notification/service/client.go new file mode 100644 index 00000000000..82f4db54dbd --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/file/notification/service/client.go @@ -0,0 +1,68 @@ +// This file was auto-generated by Fern from our API Definition. + +package service + +import ( + context "context" + fern "github.com/examples/fern" + core "github.com/examples/fern/core" + option "github.com/examples/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +func (c *Client) GetException( + ctx context.Context, + notificationId string, + opts ...option.RequestOption, +) (*fern.Exception, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := core.EncodeURL(baseURL+"/file/notification/%v", notificationId) + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response *fern.Exception + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/go-sdk/examples/exported-client-name/file/service.go b/seed/go-sdk/examples/exported-client-name/file/service.go new file mode 100644 index 00000000000..5253d0234d9 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/file/service.go @@ -0,0 +1,6 @@ +// This file was auto-generated by Fern from our API Definition. + +package file + +type GetFileRequest struct { +} diff --git a/seed/go-sdk/examples/exported-client-name/file/service/client.go b/seed/go-sdk/examples/exported-client-name/file/service/client.go new file mode 100644 index 00000000000..d6f48e75e8a --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/file/service/client.go @@ -0,0 +1,96 @@ +// This file was auto-generated by Fern from our API Definition. + +package service + +import ( + bytes "bytes" + context "context" + json "encoding/json" + errors "errors" + fern "github.com/examples/fern" + core "github.com/examples/fern/core" + file "github.com/examples/fern/file" + option "github.com/examples/fern/option" + io "io" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +// This endpoint returns a file by its name. +func (c *Client) GetFile( + ctx context.Context, + // This is a filename + filename string, + request *file.GetFileRequest, + opts ...option.RequestOption, +) (*fern.File, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := core.EncodeURL(baseURL+"/file/%v", filename) + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + errorDecoder := func(statusCode int, body io.Reader) error { + raw, err := io.ReadAll(body) + if err != nil { + return err + } + apiError := core.NewAPIError(statusCode, errors.New(string(raw))) + decoder := json.NewDecoder(bytes.NewReader(raw)) + switch statusCode { + case 404: + value := new(fern.NotFoundError) + value.APIError = apiError + if err := decoder.Decode(value); err != nil { + return apiError + } + return value + } + return apiError + } + + var response *fern.File + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + ErrorDecoder: errorDecoder, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/go-sdk/examples/exported-client-name/file/types.go b/seed/go-sdk/examples/exported-client-name/file/types.go new file mode 100644 index 00000000000..edc509f893e --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/file/types.go @@ -0,0 +1,5 @@ +// This file was auto-generated by Fern from our API Definition. + +package file + +type Filename = string diff --git a/seed/go-sdk/examples/exported-client-name/go.mod b/seed/go-sdk/examples/exported-client-name/go.mod new file mode 100644 index 00000000000..f75913fe054 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/go.mod @@ -0,0 +1,9 @@ +module github.com/examples/fern + +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/examples/exported-client-name/go.sum b/seed/go-sdk/examples/exported-client-name/go.sum new file mode 100644 index 00000000000..b3766d4366b --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/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/examples/exported-client-name/health/client/client.go b/seed/go-sdk/examples/exported-client-name/health/client/client.go new file mode 100644 index 00000000000..1fb05e0b7d5 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/health/client/client.go @@ -0,0 +1,33 @@ +// This file was auto-generated by Fern from our API Definition. + +package client + +import ( + core "github.com/examples/fern/core" + service "github.com/examples/fern/health/service" + option "github.com/examples/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header + + Service *service.Client +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + Service: service.NewClient(opts...), + } +} diff --git a/seed/go-sdk/examples/exported-client-name/health/service/client.go b/seed/go-sdk/examples/exported-client-name/health/service/client.go new file mode 100644 index 00000000000..06caa5c7e80 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/health/service/client.go @@ -0,0 +1,104 @@ +// This file was auto-generated by Fern from our API Definition. + +package service + +import ( + context "context" + core "github.com/examples/fern/core" + option "github.com/examples/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +// This endpoint checks the health of a resource. +func (c *Client) Check( + ctx context.Context, + // The id to check + id string, + opts ...option.RequestOption, +) error { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := core.EncodeURL(baseURL+"/check/%v", id) + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + }, + ); err != nil { + return err + } + return nil +} + +// This endpoint checks the health of the service. +func (c *Client) Ping( + ctx context.Context, + opts ...option.RequestOption, +) (bool, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/ping" + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response bool + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return false, err + } + return response, nil +} diff --git a/seed/go-sdk/examples/exported-client-name/option/request_option.go b/seed/go-sdk/examples/exported-client-name/option/request_option.go new file mode 100644 index 00000000000..684e14bf7d0 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/option/request_option.go @@ -0,0 +1,71 @@ +// This file was auto-generated by Fern from our API Definition. + +package option + +import ( + core "github.com/examples/fern/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, + } +} + +// WithToken sets the 'Authorization: Bearer ' request header. +func WithToken(token string) *core.TokenOption { + return &core.TokenOption{ + Token: token, + } +} diff --git a/seed/go-sdk/examples/exported-client-name/pointer.go b/seed/go-sdk/examples/exported-client-name/pointer.go new file mode 100644 index 00000000000..6938b531c04 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/pointer.go @@ -0,0 +1,132 @@ +package examples + +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/examples/exported-client-name/service.go b/seed/go-sdk/examples/exported-client-name/service.go new file mode 100644 index 00000000000..c04c639bfd8 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/service.go @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +package examples + +type GetMetadataRequest struct { + XApiVersion string `json:"-" url:"-"` + Shallow *bool `json:"-" url:"shallow,omitempty"` + Tag []*string `json:"-" url:"tag,omitempty"` +} diff --git a/seed/go-sdk/examples/exported-client-name/service/client.go b/seed/go-sdk/examples/exported-client-name/service/client.go new file mode 100644 index 00000000000..41effa5687a --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/service/client.go @@ -0,0 +1,189 @@ +// This file was auto-generated by Fern from our API Definition. + +package service + +import ( + context "context" + fmt "fmt" + fern "github.com/examples/fern" + core "github.com/examples/fern/core" + option "github.com/examples/fern/option" + http "net/http" +) + +type Client struct { + baseURL string + caller *core.Caller + header http.Header +} + +func NewClient(opts ...option.RequestOption) *Client { + options := core.NewRequestOptions(opts...) + return &Client{ + baseURL: options.BaseURL, + caller: core.NewCaller( + &core.CallerParams{ + Client: options.HTTPClient, + MaxAttempts: options.MaxAttempts, + }, + ), + header: options.ToHeader(), + } +} + +func (c *Client) GetMovie( + ctx context.Context, + movieId fern.MovieId, + opts ...option.RequestOption, +) (*fern.Movie, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := core.EncodeURL(baseURL+"/movie/%v", movieId) + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response *fern.Movie + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) CreateMovie( + ctx context.Context, + request *fern.Movie, + opts ...option.RequestOption, +) (fern.MovieId, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/movie" + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response fern.MovieId + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Request: request, + Response: &response, + }, + ); err != nil { + return "", err + } + return response, nil +} + +func (c *Client) GetMetadata( + ctx context.Context, + request *fern.GetMetadataRequest, + opts ...option.RequestOption, +) (*fern.Metadata, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/metadata" + + queryParams, err := core.QueryValues(request) + if err != nil { + return nil, err + } + if len(queryParams) > 0 { + endpointURL += "?" + queryParams.Encode() + } + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers.Add("X-API-Version", fmt.Sprintf("%v", request.XApiVersion)) + + var response *fern.Metadata + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodGet, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} + +func (c *Client) GetResponse( + ctx context.Context, + opts ...option.RequestOption, +) (*fern.Response, error) { + options := core.NewRequestOptions(opts...) + + baseURL := "" + if c.baseURL != "" { + baseURL = c.baseURL + } + if options.BaseURL != "" { + baseURL = options.BaseURL + } + endpointURL := baseURL + "/response" + + headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + + var response *fern.Response + if err := c.caller.Call( + ctx, + &core.CallParams{ + URL: endpointURL, + Method: http.MethodPost, + MaxAttempts: options.MaxAttempts, + Headers: headers, + BodyProperties: options.BodyProperties, + QueryParameters: options.QueryParameters, + Client: options.HTTPClient, + Response: &response, + }, + ); err != nil { + return nil, err + } + return response, nil +} diff --git a/seed/go-sdk/examples/exported-client-name/snippet-templates.json b/seed/go-sdk/examples/exported-client-name/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/go-sdk/examples/exported-client-name/snippet.json b/seed/go-sdk/examples/exported-client-name/snippet.json new file mode 100644 index 00000000000..9db416afe01 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/snippet.json @@ -0,0 +1,103 @@ +{ + "endpoints": [ + { + "id": { + "path": "/", + "method": "POST", + "identifier_override": "endpoint_.echo" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.Echo(\n\tcontext.TODO(),\n\t\"Hello world!\\\\n\\\\nwith\\\\n\\\\tnewlines\",\n)\n" + } + }, + { + "id": { + "path": "/check/{id}", + "method": "GET", + "identifier_override": "endpoint_health/service.check" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nerr := client.Health.Service.Check(\n\tcontext.TODO(),\n\t\"id-2sdx82h\",\n)\n" + } + }, + { + "id": { + "path": "/file/notification/{notificationId}", + "method": "GET", + "identifier_override": "endpoint_file/notification/service.getException" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.File.Notification.Service.GetException(\n\tcontext.TODO(),\n\t\"notification-hsy129x\",\n)\n" + } + }, + { + "id": { + "path": "/file/{filename}", + "method": "GET", + "identifier_override": "endpoint_file/service.getFile" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\tfile \"github.com/examples/fern/file\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.File.Service.GetFile(\n\tcontext.TODO(),\n\t\"file.txt\",\n\t\u0026file.GetFileRequest{\n\t\tXFileApiVersion: \"0.0.2\",\n\t},\n)\n" + } + }, + { + "id": { + "path": "/metadata", + "method": "GET", + "identifier_override": "endpoint_service.getMetadata" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.Service.GetMetadata(\n\tcontext.TODO(),\n\t\u0026fern.GetMetadataRequest{\n\t\tXApiVersion: \"0.0.1\",\n\t\tShallow: fern.Bool(\n\t\t\tfalse,\n\t\t),\n\t\tTag: []*string{\n\t\t\tfern.String(\n\t\t\t\t\"development\",\n\t\t\t),\n\t\t},\n\t},\n)\n" + } + }, + { + "id": { + "path": "/movie", + "method": "POST", + "identifier_override": "endpoint_service.createMovie" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.Service.CreateMovie(\n\tcontext.TODO(),\n\t\u0026fern.Movie{\n\t\tId: \"movie-c06a4ad7\",\n\t\tPrequel: fern.String(\n\t\t\t\"movie-cv9b914f\",\n\t\t),\n\t\tTitle: \"The Boy and the Heron\",\n\t\tFrom: \"Hayao Miyazaki\",\n\t\tRating: 8,\n\t\tTag: \"tag-wf9as23d\",\n\t\tMetadata: map[string]interface{}{\n\t\t\t\"actors\": []interface{}{\n\t\t\t\t\"Christian Bale\",\n\t\t\t\t\"Florence Pugh\",\n\t\t\t\t\"Willem Dafoe\",\n\t\t\t},\n\t\t\t\"releaseDate\": \"2023-12-08\",\n\t\t\t\"ratings\": map[string]interface{}{\n\t\t\t\t\"imdb\": 7.6,\n\t\t\t\t\"rottenTomatoes\": 97,\n\t\t\t},\n\t\t},\n\t\tRevenue: 1000000,\n\t},\n)\n" + } + }, + { + "id": { + "path": "/movie/{movieId}", + "method": "GET", + "identifier_override": "endpoint_service.getMovie" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.Service.GetMovie(\n\tcontext.TODO(),\n\t\"movie-c06a4ad7\",\n)\n" + } + }, + { + "id": { + "path": "/ping", + "method": "GET", + "identifier_override": "endpoint_health/service.ping" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.Health.Service.Ping(\n\tcontext.TODO(),\n)\n" + } + }, + { + "id": { + "path": "/response", + "method": "POST", + "identifier_override": "endpoint_service.getResponse" + }, + "snippet": { + "type": "go", + "client": "import (\n\tcontext \"context\"\n\tfern \"github.com/examples/fern\"\n\tfernclient \"github.com/examples/fern/client\"\n\toption \"github.com/examples/fern/option\"\n)\n\nclient := fernclient.NewAcmeClient(\n\toption.WithToken(\n\t\t\"\u003cYOUR_AUTH_TOKEN\u003e\",\n\t),\n\toption.WithBaseURL(\n\t\tfern.Environments.Production,\n\t),\n)\nresponse, err := client.Service.GetResponse(\n\tcontext.TODO(),\n)\n" + } + } + ] +} \ No newline at end of file diff --git a/seed/go-sdk/examples/exported-client-name/types.go b/seed/go-sdk/examples/exported-client-name/types.go new file mode 100644 index 00000000000..bfb2b875d83 --- /dev/null +++ b/seed/go-sdk/examples/exported-client-name/types.go @@ -0,0 +1,1280 @@ +// This file was auto-generated by Fern from our API Definition. + +package examples + +import ( + json "encoding/json" + fmt "fmt" + commons "github.com/examples/fern/commons" + core "github.com/examples/fern/core" + uuid "github.com/google/uuid" + time "time" +) + +type BasicType string + +const ( + BasicTypePrimitive BasicType = "primitive" + BasicTypeLiteral BasicType = "literal" +) + +func NewBasicTypeFromString(s string) (BasicType, error) { + switch s { + case "primitive": + return BasicTypePrimitive, nil + case "literal": + return BasicTypeLiteral, nil + } + var t BasicType + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (b BasicType) Ptr() *BasicType { + return &b +} + +type ComplexType string + +const ( + ComplexTypeObject ComplexType = "object" + ComplexTypeUnion ComplexType = "union" + ComplexTypeUnknown ComplexType = "unknown" +) + +func NewComplexTypeFromString(s string) (ComplexType, error) { + switch s { + case "object": + return ComplexTypeObject, nil + case "union": + return ComplexTypeUnion, nil + case "unknown": + return ComplexTypeUnknown, nil + } + var t ComplexType + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (c ComplexType) Ptr() *ComplexType { + return &c +} + +type Identifier struct { + Type *Type `json:"type,omitempty" url:"type,omitempty"` + Value string `json:"value" url:"value"` + Label string `json:"label" url:"label"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (i *Identifier) GetExtraProperties() map[string]interface{} { + return i.extraProperties +} + +func (i *Identifier) UnmarshalJSON(data []byte) error { + type unmarshaler Identifier + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *i = Identifier(value) + + extraProperties, err := core.ExtractExtraProperties(data, *i) + if err != nil { + return err + } + i.extraProperties = extraProperties + + i._rawJSON = json.RawMessage(data) + return nil +} + +func (i *Identifier) String() string { + if len(i._rawJSON) > 0 { + if value, err := core.StringifyJSON(i._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(i); err == nil { + return value + } + return fmt.Sprintf("%#v", i) +} + +type Type struct { + BasicType BasicType + ComplexType ComplexType +} + +func NewTypeFromBasicType(value BasicType) *Type { + return &Type{BasicType: value} +} + +func NewTypeFromComplexType(value ComplexType) *Type { + return &Type{ComplexType: value} +} + +func (t *Type) UnmarshalJSON(data []byte) error { + var valueBasicType BasicType + if err := json.Unmarshal(data, &valueBasicType); err == nil { + t.BasicType = valueBasicType + return nil + } + var valueComplexType ComplexType + if err := json.Unmarshal(data, &valueComplexType); err == nil { + t.ComplexType = valueComplexType + return nil + } + return fmt.Errorf("%s cannot be deserialized as a %T", data, t) +} + +func (t Type) MarshalJSON() ([]byte, error) { + if t.BasicType != "" { + return json.Marshal(t.BasicType) + } + if t.ComplexType != "" { + return json.Marshal(t.ComplexType) + } + return nil, fmt.Errorf("type %T does not include a non-empty union type", t) +} + +type TypeVisitor interface { + VisitBasicType(BasicType) error + VisitComplexType(ComplexType) error +} + +func (t *Type) Accept(visitor TypeVisitor) error { + if t.BasicType != "" { + return visitor.VisitBasicType(t.BasicType) + } + if t.ComplexType != "" { + return visitor.VisitComplexType(t.ComplexType) + } + return fmt.Errorf("type %T does not include a non-empty union type", t) +} + +type Actor struct { + Name string `json:"name" url:"name"` + Id string `json:"id" url:"id"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (a *Actor) GetExtraProperties() map[string]interface{} { + return a.extraProperties +} + +func (a *Actor) UnmarshalJSON(data []byte) error { + type unmarshaler Actor + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *a = Actor(value) + + extraProperties, err := core.ExtractExtraProperties(data, *a) + if err != nil { + return err + } + a.extraProperties = extraProperties + + a._rawJSON = json.RawMessage(data) + return nil +} + +func (a *Actor) String() string { + if len(a._rawJSON) > 0 { + if value, err := core.StringifyJSON(a._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(a); err == nil { + return value + } + return fmt.Sprintf("%#v", a) +} + +type Actress struct { + Name string `json:"name" url:"name"` + Id string `json:"id" url:"id"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (a *Actress) GetExtraProperties() map[string]interface{} { + return a.extraProperties +} + +func (a *Actress) UnmarshalJSON(data []byte) error { + type unmarshaler Actress + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *a = Actress(value) + + extraProperties, err := core.ExtractExtraProperties(data, *a) + if err != nil { + return err + } + a.extraProperties = extraProperties + + a._rawJSON = json.RawMessage(data) + return nil +} + +func (a *Actress) String() string { + if len(a._rawJSON) > 0 { + if value, err := core.StringifyJSON(a._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(a); err == nil { + return value + } + return fmt.Sprintf("%#v", a) +} + +type CastMember struct { + Actor *Actor + Actress *Actress + StuntDouble *StuntDouble +} + +func NewCastMemberFromActor(value *Actor) *CastMember { + return &CastMember{Actor: value} +} + +func NewCastMemberFromActress(value *Actress) *CastMember { + return &CastMember{Actress: value} +} + +func NewCastMemberFromStuntDouble(value *StuntDouble) *CastMember { + return &CastMember{StuntDouble: value} +} + +func (c *CastMember) UnmarshalJSON(data []byte) error { + valueActor := new(Actor) + if err := json.Unmarshal(data, &valueActor); err == nil { + c.Actor = valueActor + return nil + } + valueActress := new(Actress) + if err := json.Unmarshal(data, &valueActress); err == nil { + c.Actress = valueActress + return nil + } + valueStuntDouble := new(StuntDouble) + if err := json.Unmarshal(data, &valueStuntDouble); err == nil { + c.StuntDouble = valueStuntDouble + return nil + } + return fmt.Errorf("%s cannot be deserialized as a %T", data, c) +} + +func (c CastMember) MarshalJSON() ([]byte, error) { + if c.Actor != nil { + return json.Marshal(c.Actor) + } + if c.Actress != nil { + return json.Marshal(c.Actress) + } + if c.StuntDouble != nil { + return json.Marshal(c.StuntDouble) + } + return nil, fmt.Errorf("type %T does not include a non-empty union type", c) +} + +type CastMemberVisitor interface { + VisitActor(*Actor) error + VisitActress(*Actress) error + VisitStuntDouble(*StuntDouble) error +} + +func (c *CastMember) Accept(visitor CastMemberVisitor) error { + if c.Actor != nil { + return visitor.VisitActor(c.Actor) + } + if c.Actress != nil { + return visitor.VisitActress(c.Actress) + } + if c.StuntDouble != nil { + return visitor.VisitStuntDouble(c.StuntDouble) + } + return fmt.Errorf("type %T does not include a non-empty union type", c) +} + +type Directory struct { + Name string `json:"name" url:"name"` + Files []*File `json:"files,omitempty" url:"files,omitempty"` + Directories []*Directory `json:"directories,omitempty" url:"directories,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (d *Directory) GetExtraProperties() map[string]interface{} { + return d.extraProperties +} + +func (d *Directory) UnmarshalJSON(data []byte) error { + type unmarshaler Directory + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *d = Directory(value) + + extraProperties, err := core.ExtractExtraProperties(data, *d) + if err != nil { + return err + } + d.extraProperties = extraProperties + + d._rawJSON = json.RawMessage(data) + return nil +} + +func (d *Directory) String() string { + if len(d._rawJSON) > 0 { + if value, err := core.StringifyJSON(d._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(d); err == nil { + return value + } + return fmt.Sprintf("%#v", d) +} + +type Entity struct { + Type *Type `json:"type,omitempty" url:"type,omitempty"` + Name string `json:"name" url:"name"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (e *Entity) GetExtraProperties() map[string]interface{} { + return e.extraProperties +} + +func (e *Entity) UnmarshalJSON(data []byte) error { + type unmarshaler Entity + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = Entity(value) + + extraProperties, err := core.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + + e._rawJSON = json.RawMessage(data) + return nil +} + +func (e *Entity) String() string { + if len(e._rawJSON) > 0 { + if value, err := core.StringifyJSON(e._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} + +type Exception struct { + Type string + Generic *ExceptionInfo + Timeout interface{} +} + +func NewExceptionFromGeneric(value *ExceptionInfo) *Exception { + return &Exception{Type: "generic", Generic: value} +} + +func NewExceptionFromTimeout(value interface{}) *Exception { + return &Exception{Type: "timeout", Timeout: value} +} + +func (e *Exception) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + e.Type = unmarshaler.Type + if unmarshaler.Type == "" { + return fmt.Errorf("%T did not include discriminant type", e) + } + switch unmarshaler.Type { + case "generic": + value := new(ExceptionInfo) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + e.Generic = value + case "timeout": + value := make(map[string]interface{}) + if err := json.Unmarshal(data, &value); err != nil { + return err + } + e.Timeout = value + } + return nil +} + +func (e Exception) MarshalJSON() ([]byte, error) { + switch e.Type { + default: + return nil, fmt.Errorf("invalid type %s in %T", e.Type, e) + case "generic": + return core.MarshalJSONWithExtraProperty(e.Generic, "type", "generic") + case "timeout": + var marshaler = struct { + Type string `json:"type"` + Timeout interface{} `json:"timeout,omitempty"` + }{ + Type: "timeout", + Timeout: e.Timeout, + } + return json.Marshal(marshaler) + } +} + +type ExceptionVisitor interface { + VisitGeneric(*ExceptionInfo) error + VisitTimeout(interface{}) error +} + +func (e *Exception) Accept(visitor ExceptionVisitor) error { + switch e.Type { + default: + return fmt.Errorf("invalid type %s in %T", e.Type, e) + case "generic": + return visitor.VisitGeneric(e.Generic) + case "timeout": + return visitor.VisitTimeout(e.Timeout) + } +} + +type ExceptionInfo struct { + ExceptionType string `json:"exceptionType" url:"exceptionType"` + ExceptionMessage string `json:"exceptionMessage" url:"exceptionMessage"` + ExceptionStacktrace string `json:"exceptionStacktrace" url:"exceptionStacktrace"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (e *ExceptionInfo) GetExtraProperties() map[string]interface{} { + return e.extraProperties +} + +func (e *ExceptionInfo) UnmarshalJSON(data []byte) error { + type unmarshaler ExceptionInfo + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *e = ExceptionInfo(value) + + extraProperties, err := core.ExtractExtraProperties(data, *e) + if err != nil { + return err + } + e.extraProperties = extraProperties + + e._rawJSON = json.RawMessage(data) + return nil +} + +func (e *ExceptionInfo) String() string { + if len(e._rawJSON) > 0 { + if value, err := core.StringifyJSON(e._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} + +type ExtendedMovie struct { + Id MovieId `json:"id" url:"id"` + Prequel *MovieId `json:"prequel,omitempty" url:"prequel,omitempty"` + Title string `json:"title" url:"title"` + From string `json:"from" url:"from"` + // The rating scale is one to five stars + Rating float64 `json:"rating" url:"rating"` + Tag commons.Tag `json:"tag" url:"tag"` + Book *string `json:"book,omitempty" url:"book,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty" url:"metadata,omitempty"` + Revenue int64 `json:"revenue" url:"revenue"` + Cast []string `json:"cast,omitempty" url:"cast,omitempty"` + type_ string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (e *ExtendedMovie) GetExtraProperties() map[string]interface{} { + return e.extraProperties +} + +func (e *ExtendedMovie) Type() string { + return e.type_ +} + +func (e *ExtendedMovie) UnmarshalJSON(data []byte) error { + type embed ExtendedMovie + var unmarshaler = struct { + embed + Type string `json:"type"` + }{ + embed: embed(*e), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *e = ExtendedMovie(unmarshaler.embed) + if unmarshaler.Type != "movie" { + return fmt.Errorf("unexpected value for literal on type %T; expected %v got %v", e, "movie", unmarshaler.Type) + } + e.type_ = unmarshaler.Type + + extraProperties, err := core.ExtractExtraProperties(data, *e, "type") + if err != nil { + return err + } + e.extraProperties = extraProperties + + e._rawJSON = json.RawMessage(data) + return nil +} + +func (e *ExtendedMovie) MarshalJSON() ([]byte, error) { + type embed ExtendedMovie + var marshaler = struct { + embed + Type string `json:"type"` + }{ + embed: embed(*e), + Type: "movie", + } + return json.Marshal(marshaler) +} + +func (e *ExtendedMovie) String() string { + if len(e._rawJSON) > 0 { + if value, err := core.StringifyJSON(e._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(e); err == nil { + return value + } + return fmt.Sprintf("%#v", e) +} + +type File struct { + Name string `json:"name" url:"name"` + Contents string `json:"contents" url:"contents"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (f *File) GetExtraProperties() map[string]interface{} { + return f.extraProperties +} + +func (f *File) UnmarshalJSON(data []byte) error { + type unmarshaler File + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *f = File(value) + + extraProperties, err := core.ExtractExtraProperties(data, *f) + if err != nil { + return err + } + f.extraProperties = extraProperties + + f._rawJSON = json.RawMessage(data) + return nil +} + +func (f *File) String() string { + if len(f._rawJSON) > 0 { + if value, err := core.StringifyJSON(f._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(f); err == nil { + return value + } + return fmt.Sprintf("%#v", f) +} + +type Metadata struct { + Type string + Extra map[string]string + Tags []string + Html string + Markdown string +} + +func NewMetadataFromHtml(value string) *Metadata { + return &Metadata{Type: "html", Html: value} +} + +func NewMetadataFromMarkdown(value string) *Metadata { + return &Metadata{Type: "markdown", Markdown: value} +} + +func (m *Metadata) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Type string `json:"type"` + Extra map[string]string `json:"extra,omitempty"` + Tags []string `json:"tags,omitempty"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + m.Type = unmarshaler.Type + m.Extra = unmarshaler.Extra + m.Tags = unmarshaler.Tags + if unmarshaler.Type == "" { + return fmt.Errorf("%T did not include discriminant type", m) + } + switch unmarshaler.Type { + case "html": + var valueUnmarshaler struct { + Html string `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + m.Html = valueUnmarshaler.Html + case "markdown": + var valueUnmarshaler struct { + Markdown string `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + m.Markdown = valueUnmarshaler.Markdown + } + return nil +} + +func (m Metadata) MarshalJSON() ([]byte, error) { + switch m.Type { + default: + return nil, fmt.Errorf("invalid type %s in %T", m.Type, m) + case "html": + var marshaler = struct { + Type string `json:"type"` + Extra map[string]string `json:"extra,omitempty"` + Tags []string `json:"tags,omitempty"` + Html string `json:"value"` + }{ + Type: "html", + Extra: m.Extra, + Tags: m.Tags, + Html: m.Html, + } + return json.Marshal(marshaler) + case "markdown": + var marshaler = struct { + Type string `json:"type"` + Extra map[string]string `json:"extra,omitempty"` + Tags []string `json:"tags,omitempty"` + Markdown string `json:"value"` + }{ + Type: "markdown", + Extra: m.Extra, + Tags: m.Tags, + Markdown: m.Markdown, + } + return json.Marshal(marshaler) + } +} + +type MetadataVisitor interface { + VisitHtml(string) error + VisitMarkdown(string) error +} + +func (m *Metadata) Accept(visitor MetadataVisitor) error { + switch m.Type { + default: + return fmt.Errorf("invalid type %s in %T", m.Type, m) + case "html": + return visitor.VisitHtml(m.Html) + case "markdown": + return visitor.VisitMarkdown(m.Markdown) + } +} + +type Migration struct { + Name string `json:"name" url:"name"` + Status MigrationStatus `json:"status" url:"status"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *Migration) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Migration) UnmarshalJSON(data []byte) error { + type unmarshaler Migration + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *m = Migration(value) + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *Migration) String() string { + if len(m._rawJSON) > 0 { + if value, err := core.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} + +type MigrationStatus string + +const ( + // The migration is running. + MigrationStatusRunning MigrationStatus = "RUNNING" + // The migration failed. + MigrationStatusFailed MigrationStatus = "FAILED" + MigrationStatusFinished MigrationStatus = "FINISHED" +) + +func NewMigrationStatusFromString(s string) (MigrationStatus, error) { + switch s { + case "RUNNING": + return MigrationStatusRunning, nil + case "FAILED": + return MigrationStatusFailed, nil + case "FINISHED": + return MigrationStatusFinished, nil + } + var t MigrationStatus + return "", fmt.Errorf("%s is not a valid %T", s, t) +} + +func (m MigrationStatus) Ptr() *MigrationStatus { + return &m +} + +type Moment struct { + Id uuid.UUID `json:"id" url:"id"` + Date time.Time `json:"date" url:"date" format:"date"` + Datetime time.Time `json:"datetime" url:"datetime"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *Moment) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Moment) UnmarshalJSON(data []byte) error { + type embed Moment + var unmarshaler = struct { + embed + Date *core.Date `json:"date"` + Datetime *core.DateTime `json:"datetime"` + }{ + embed: embed(*m), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *m = Moment(unmarshaler.embed) + m.Date = unmarshaler.Date.Time() + m.Datetime = unmarshaler.Datetime.Time() + + extraProperties, err := core.ExtractExtraProperties(data, *m) + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *Moment) MarshalJSON() ([]byte, error) { + type embed Moment + var marshaler = struct { + embed + Date *core.Date `json:"date"` + Datetime *core.DateTime `json:"datetime"` + }{ + embed: embed(*m), + Date: core.NewDate(m.Date), + Datetime: core.NewDateTime(m.Datetime), + } + return json.Marshal(marshaler) +} + +func (m *Moment) String() string { + if len(m._rawJSON) > 0 { + if value, err := core.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} + +type Movie struct { + Id MovieId `json:"id" url:"id"` + Prequel *MovieId `json:"prequel,omitempty" url:"prequel,omitempty"` + Title string `json:"title" url:"title"` + From string `json:"from" url:"from"` + // The rating scale is one to five stars + Rating float64 `json:"rating" url:"rating"` + Tag commons.Tag `json:"tag" url:"tag"` + Book *string `json:"book,omitempty" url:"book,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty" url:"metadata,omitempty"` + Revenue int64 `json:"revenue" url:"revenue"` + type_ string + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (m *Movie) GetExtraProperties() map[string]interface{} { + return m.extraProperties +} + +func (m *Movie) Type() string { + return m.type_ +} + +func (m *Movie) UnmarshalJSON(data []byte) error { + type embed Movie + var unmarshaler = struct { + embed + Type string `json:"type"` + }{ + embed: embed(*m), + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + *m = Movie(unmarshaler.embed) + if unmarshaler.Type != "movie" { + return fmt.Errorf("unexpected value for literal on type %T; expected %v got %v", m, "movie", unmarshaler.Type) + } + m.type_ = unmarshaler.Type + + extraProperties, err := core.ExtractExtraProperties(data, *m, "type") + if err != nil { + return err + } + m.extraProperties = extraProperties + + m._rawJSON = json.RawMessage(data) + return nil +} + +func (m *Movie) MarshalJSON() ([]byte, error) { + type embed Movie + var marshaler = struct { + embed + Type string `json:"type"` + }{ + embed: embed(*m), + Type: "movie", + } + return json.Marshal(marshaler) +} + +func (m *Movie) String() string { + if len(m._rawJSON) > 0 { + if value, err := core.StringifyJSON(m._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(m); err == nil { + return value + } + return fmt.Sprintf("%#v", m) +} + +type MovieId = string + +type Node struct { + Name string `json:"name" url:"name"` + Nodes []*Node `json:"nodes,omitempty" url:"nodes,omitempty"` + Trees []*Tree `json:"trees,omitempty" url:"trees,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (n *Node) GetExtraProperties() map[string]interface{} { + return n.extraProperties +} + +func (n *Node) UnmarshalJSON(data []byte) error { + type unmarshaler Node + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *n = Node(value) + + extraProperties, err := core.ExtractExtraProperties(data, *n) + if err != nil { + return err + } + n.extraProperties = extraProperties + + n._rawJSON = json.RawMessage(data) + return nil +} + +func (n *Node) String() string { + if len(n._rawJSON) > 0 { + if value, err := core.StringifyJSON(n._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(n); err == nil { + return value + } + return fmt.Sprintf("%#v", n) +} + +type Request struct { + Request interface{} `json:"request,omitempty" url:"request,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *Request) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *Request) UnmarshalJSON(data []byte) error { + type unmarshaler Request + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = Request(value) + + extraProperties, err := core.ExtractExtraProperties(data, *r) + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *Request) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +type Response struct { + Response interface{} `json:"response,omitempty" url:"response,omitempty"` + Identifiers []*Identifier `json:"identifiers,omitempty" url:"identifiers,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *Response) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *Response) UnmarshalJSON(data []byte) error { + type unmarshaler Response + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = Response(value) + + extraProperties, err := core.ExtractExtraProperties(data, *r) + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *Response) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +type ResponseType struct { + Type *Type `json:"type,omitempty" url:"type,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (r *ResponseType) GetExtraProperties() map[string]interface{} { + return r.extraProperties +} + +func (r *ResponseType) UnmarshalJSON(data []byte) error { + type unmarshaler ResponseType + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *r = ResponseType(value) + + extraProperties, err := core.ExtractExtraProperties(data, *r) + if err != nil { + return err + } + r.extraProperties = extraProperties + + r._rawJSON = json.RawMessage(data) + return nil +} + +func (r *ResponseType) String() string { + if len(r._rawJSON) > 0 { + if value, err := core.StringifyJSON(r._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(r); err == nil { + return value + } + return fmt.Sprintf("%#v", r) +} + +type StuntDouble struct { + Name string `json:"name" url:"name"` + ActorOrActressId string `json:"actorOrActressId" url:"actorOrActressId"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (s *StuntDouble) GetExtraProperties() map[string]interface{} { + return s.extraProperties +} + +func (s *StuntDouble) UnmarshalJSON(data []byte) error { + type unmarshaler StuntDouble + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *s = StuntDouble(value) + + extraProperties, err := core.ExtractExtraProperties(data, *s) + if err != nil { + return err + } + s.extraProperties = extraProperties + + s._rawJSON = json.RawMessage(data) + return nil +} + +func (s *StuntDouble) String() string { + if len(s._rawJSON) > 0 { + if value, err := core.StringifyJSON(s._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(s); err == nil { + return value + } + return fmt.Sprintf("%#v", s) +} + +type Test struct { + Type string + And bool + Or bool +} + +func NewTestFromAnd(value bool) *Test { + return &Test{Type: "and", And: value} +} + +func NewTestFromOr(value bool) *Test { + return &Test{Type: "or", Or: value} +} + +func (t *Test) UnmarshalJSON(data []byte) error { + var unmarshaler struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &unmarshaler); err != nil { + return err + } + t.Type = unmarshaler.Type + if unmarshaler.Type == "" { + return fmt.Errorf("%T did not include discriminant type", t) + } + switch unmarshaler.Type { + case "and": + var valueUnmarshaler struct { + And bool `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + t.And = valueUnmarshaler.And + case "or": + var valueUnmarshaler struct { + Or bool `json:"value"` + } + if err := json.Unmarshal(data, &valueUnmarshaler); err != nil { + return err + } + t.Or = valueUnmarshaler.Or + } + return nil +} + +func (t Test) MarshalJSON() ([]byte, error) { + switch t.Type { + default: + return nil, fmt.Errorf("invalid type %s in %T", t.Type, t) + case "and": + var marshaler = struct { + Type string `json:"type"` + And bool `json:"value"` + }{ + Type: "and", + And: t.And, + } + return json.Marshal(marshaler) + case "or": + var marshaler = struct { + Type string `json:"type"` + Or bool `json:"value"` + }{ + Type: "or", + Or: t.Or, + } + return json.Marshal(marshaler) + } +} + +type TestVisitor interface { + VisitAnd(bool) error + VisitOr(bool) error +} + +func (t *Test) Accept(visitor TestVisitor) error { + switch t.Type { + default: + return fmt.Errorf("invalid type %s in %T", t.Type, t) + case "and": + return visitor.VisitAnd(t.And) + case "or": + return visitor.VisitOr(t.Or) + } +} + +type Tree struct { + Nodes []*Node `json:"nodes,omitempty" url:"nodes,omitempty"` + + extraProperties map[string]interface{} + _rawJSON json.RawMessage +} + +func (t *Tree) GetExtraProperties() map[string]interface{} { + return t.extraProperties +} + +func (t *Tree) UnmarshalJSON(data []byte) error { + type unmarshaler Tree + var value unmarshaler + if err := json.Unmarshal(data, &value); err != nil { + return err + } + *t = Tree(value) + + extraProperties, err := core.ExtractExtraProperties(data, *t) + if err != nil { + return err + } + t.extraProperties = extraProperties + + t._rawJSON = json.RawMessage(data) + return nil +} + +func (t *Tree) String() string { + if len(t._rawJSON) > 0 { + if value, err := core.StringifyJSON(t._rawJSON); err == nil { + return value + } + } + if value, err := core.StringifyJSON(t); err == nil { + return value + } + return fmt.Sprintf("%#v", t) +} diff --git a/seed/go-sdk/seed.yml b/seed/go-sdk/seed.yml index 764880c05a2..82307418836 100644 --- a/seed/go-sdk/seed.yml +++ b/seed/go-sdk/seed.yml @@ -33,6 +33,9 @@ fixtures: - outputFolder: always-send-required-properties customConfig: alwaysSendRequiredProperties: true + - outputFolder: exported-client-name + customConfig: + exportedClientName: AcmeClient undiscriminated-unions: - outputFolder: . outputVersion: 0.0.1